Related
I'm trying to implement a WebGL app according to the documentation in the Mozilla Docs.
My code generates a sphere with is shaped by a scalefactor. The colors are generated according to the scalefactor. The shape is ok, but the colors are wrong. So what is going wrong - I have no clue. This code works on Android and in Java. I'm using the latest Chrome browser.
Here is the code:
export function createHcm3dObject(gl, diagram3D, deltaTheta, deltaPhi) {
let positions = [];
let colors = [];
let alpha = 1.0;
for (let theta = 0; theta < 360; theta += deltaTheta) {
for (let phi = 0; phi < 180; phi += deltaPhi) {
//r is scalefactor between 0 and 1 which shapes the sphere
let r = diagram3D[theta][phi];
//Color is generated according to the radius (alpha is currently set to 1.0)
let x1Color = generateColorArray(r, alpha);
let x1 = r * Math.sin(math3d.toRadians(phi)) * Math.cos(math3d.toRadians(theta));
let y1 = r * Math.sin(math3d.toRadians(phi)) * Math.sin(math3d.toRadians(theta));
let z1 = r * Math.cos(math3d.toRadians(phi));
r = diagram3D[theta + deltaTheta][phi];
let x2Color = generateColorArray(r, alpha);
let x2 = r * Math.sin(math3d.toRadians(phi)) * Math.cos(math3d.toRadians(theta + deltaTheta));
let y2 = r * Math.sin(math3d.toRadians(phi)) * Math.sin(math3d.toRadians(theta + deltaTheta));
let z2 = r * Math.cos(math3d.toRadians(phi));
r = diagram3D[theta][phi + deltaPhi];
let x3Color = generateColorArray(r, alpha);
let x3 = r * Math.sin(math3d.toRadians(phi + deltaPhi)) * Math.cos(math3d.toRadians(theta));
let y3 = r * Math.sin(math3d.toRadians(phi + deltaPhi)) * Math.sin(math3d.toRadians(theta));
let z3 = r * Math.cos(math3d.toRadians(phi + deltaPhi));
r = diagram3D[theta + deltaTheta][phi + deltaPhi];
let x4Color = generateColorArray(r, alpha);
let x4 = r * Math.sin(math3d.toRadians(phi + deltaPhi)) * Math.cos(math3d.toRadians(theta + deltaTheta));
let y4 = r * Math.sin(math3d.toRadians(phi + deltaPhi)) * Math.sin(math3d.toRadians(theta + deltaTheta));
let z4 = r * Math.cos(math3d.toRadians(phi + deltaPhi));
//1. Triangle
positions.push(x1, y1, z1);
positions.push(x3, y3, z3);
positions.push(x4, y4, z4);
//2. Triangle
positions.push(x2, y2, z2);
positions.push(x1, y1, z1);
positions.push(x4, y4, z4);
//Colors for 1. Triangle (red,green,blue,alpha=1.0)
colors.push(x1Color[0], x1Color[1], x1Color[2], x1Color[3]);
colors.push(x3Color[0], x3Color[1], x3Color[2], x3Color[3]);
colors.push(x4Color[0], x4Color[1], x4Color[2], x4Color[3]);
//Colors for 2. Triangle
colors.push(x2Color[0], x2Color[1], x2Color[2], x2Color[3]);
colors.push(x1Color[0], x1Color[1], x1Color[2], x1Color[3]);
colors.push(x4Color[0], x4Color[1], x4Color[2], x4Color[3]);
}
//console.log(positions);
//console.log(colors);
}
// Now pass the list of positions into WebGL to build the
// shape. We do this by creating a Float32Array from the
// JavaScript array, then use it to fill the current buffer.
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
return {
position: positionBuffer,
color: colorBuffer,
positionSize: positions.length,
deltaTheta,
deltaPhi
};
};
function generateColorArray(r, alpha) {
let colorQuad = [];
let green = Math.abs(Math.sin(2 * r * Math.PI));
let blue = Math.abs(Math.cos(2 * r * Math.PI));
colorQuad[0] = 0.0;
colorQuad[1] = green;
colorQuad[2] = blue;
colorQuad[3] = alpha;
if (r >= 0.5 / 2) {
let red = Math.abs(Math.cos(2 * r * Math.PI));
green = Math.abs(Math.sin(2 * r * Math.PI));
if (r < 0.5) {
green = 1.0;
}
colorQuad[0] = red;
colorQuad[1] = green;
colorQuad[2] = 0.0;
colorQuad[3] = alpha;
}
if (r >= 0.5) {
let red = Math.abs(Math.cos(2 * r * Math.PI));
green = Math.abs(Math.cos(2 * r * Math.PI));
if (r < 0.75) {
red = 1.0;
}
colorQuad[0] = red;
colorQuad[1] = green;
colorQuad[2] = 0.0;
colorQuad[3] = alpha;
}
if (r >= 0.75) {
let red = 1.0;
blue = Math.abs(Math.cos(2 * r * Math.PI));
colorQuad[0] = red;
colorQuad[1] = 0.0;
colorQuad[2] = blue;
colorQuad[3] = alpha;
}
return colorQuad;
}
React Class:
export class Viewer3d extends Component {
state = {
rotX: 0,
rotY: 0,
gl: null,
buffers: null,
programInfo: null,
};
componentDidMount() {
this.init();
}
init = () => {
console.log("Comp did mount");
const canvas = document.querySelector("#glCanvas");
/** #type {WebGLRenderingContext} */
const gl = canvas.getContext("webgl");
if (!gl) {
alert(
"Unable to initialize WebGL. Your browser or machine may not support it."
);
return;
}
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
let vs = document.getElementById("vshader").textContent;
let fs = document.getElementById("fshader").textContent;
//console.log(vs+" "+fs);
const shaderProgram = shader.initShaderProgram(gl, vs, fs);
let diagram3D = [];
let deltaTheta = 10;
let deltaPhi = 10;
for (let theta = 0; theta <= 360; theta += deltaTheta) {
let phiArray = [];
for (let phi = 0; phi <= 180; phi += deltaPhi) {
let eleCorr = 90 - phi;
let thetaCorr = 360 - theta;
let out = engine.antenna_correction(
thetaCorr,
0,
eleCorr,
0,
"012EA34",
"012EA34"
);
let att = out.a;
let logarithmic = false;
if (logarithmic) {
att = 1.0 - (-20.0 * Math.log10(att)) / 40.0;
}
phiArray[phi] = att;
}
diagram3D[theta] = phiArray;
}
//console.log(diagram3D);
const buffers = hcm3d.createHcm3dObject(
gl,
diagram3D,
deltaTheta,
deltaPhi
);
const programInfo = {
program: shaderProgram,
attribLocations: {
vertexPosition: gl.getAttribLocation(shaderProgram, "aVertexPosition"),
vertexColor: gl.getAttribLocation(shaderProgram,"aVertexColor"),
},
uniformLocations: {
projectionMatrix: gl.getUniformLocation(shaderProgram,"uProjectionMatrix"),
modelViewMatrix: gl.getUniformLocation(shaderProgram,"uModelViewMatrix"),
},
};
this.setState({ gl, buffers, programInfo });
this.drawScene(gl, programInfo, buffers);
};
drawScene = (gl, programInfo, buffers) => {
gl.clearColor(0.0, 0.0, 0.0, 1.0); // Clear to black, fully opaque
gl.clearDepth(1.0); // Clear everything
gl.enable(gl.DEPTH_TEST); // Enable depth testing
gl.depthFunc(gl.LEQUAL); // Near things obscure far things
// Clear the canvas before we start drawing on it.
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// Create a perspective matrix, a special matrix that is
// used to simulate the distortion of perspective in a camera.
// Our field of view is 45 degrees, with a width/height
// ratio that matches the display size of the canvas
// and we only want to see objects between 0.1 units
// and 100 units away from the camera.
const fieldOfView = (45 * Math.PI) / 180; // in radians
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const zNear = 0.1;
const zFar = 100.0;
const projectionMatrix = mat4.create();
// note: glmatrix.js always has the first argument
// as the destination to receive the result.
mat4.perspective(projectionMatrix, fieldOfView, aspect, zNear, zFar);
// Set the drawing position to the "identity" point, which is
// the center of the scene.
const modelViewMatrix = mat4.create();
// Now move the drawing position a bit to where we want to
// start drawing the square.
mat4.translate(
modelViewMatrix, // destination matrix
modelViewMatrix, // matrix to translate
[0, 0, -2.5]
);
mat4.rotate(modelViewMatrix, modelViewMatrix, this.state.rotY, [1, 0, 0]);
mat4.rotate(modelViewMatrix, modelViewMatrix, this.state.rotX, [0, 1, 0]);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.color);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position);
gl.vertexAttribPointer(
programInfo.attribLocations.vertexPosition,
3,
gl.FLOAT,
false,
0,
0
);
gl.vertexAttribPointer(
programInfo.attribLocations.vertexColor,
4,
gl.FLOAT,
false,
0,
0
);
gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition);
gl.enableVertexAttribArray(programInfo.attribLocations.vertexColor);
gl.useProgram(programInfo.program);
gl.uniformMatrix4fv(
programInfo.uniformLocations.projectionMatrix,
false,
projectionMatrix
);
gl.uniformMatrix4fv(
programInfo.uniformLocations.modelViewMatrix,
false,
modelViewMatrix
);
gl.drawArrays(gl.TRIANGLES, 0, buffers.positionSize);
};
onMouseMove = (evt) => {
if (!mouseDown) {
return;
}
evt.preventDefault();
let deltaX = evt.clientX - mouseX;
let deltaY = evt.clientY - mouseY;
mouseX = evt.clientX;
mouseY = evt.clientY;
this.rotateScene(deltaX, deltaY);
};
onMouseDown = (evt) => {
evt.preventDefault();
mouseDown = true;
mouseX = evt.clientX;
mouseY = evt.clientY;
};
onMouseUp = (evt) => {
evt.preventDefault();
mouseDown = false;
};
rotateScene = (deltaX, deltaY) => {
this.setState({
rotX: this.state.rotX + deltaX / 100,
rotY: this.state.rotY + deltaY / 100,
});
this.drawScene(this.state.gl, this.state.programInfo, this.state.buffers);
};
render() {
return (
<div className="w3-container w3-padding-16">
<canvas
id="glCanvas"
width={1280}
height={720}
onMouseMove={this.onMouseMove}
onMouseDown={this.onMouseDown}
onMouseUp={this.onMouseUp}
></canvas>
</div>
);
}
}
export default Viewer3d;
Fragment Shader:
<script id="fshader" type="x-shader/x-fragment">
precision mediump float;
varying vec4 vColor;
void main(void) {
gl_FragColor = vColor;
}
</script>
Vertex Shader:
<script id="vshader" type="x-shader/x-vertex">
attribute vec4 aVertexPosition;
attribute vec4 aVertexColor;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
varying vec4 vColor;
void main(void) {
gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
vColor = aVertexColor;
}
</script>
You need to bind one buffer (say the color one), then use vertexAttribPointer to bind the set buffer to the color attribute. Then again, bind the vertex position buffer, and call vertexAttribPointer to bind it the vertex position attribute. Pseudocode:
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.color);
gl.vertexAttribPointer(programInfo.attribLocations.vertexColor, ...);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position);
gl.vertexAttribPointer(programInfo.attribLocations.vertexPosition, ...);
I am trying to draw 2D metaballs using WebGL2. I render a bunch of quads with transparent radial gradient and gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) to a separate framebuffer. I then use the resulting texture in a fullscreen quad, where I decide if pixel should be rendered based on it's alpha value like so:
vec4 inputColor = texture(u_texture, v_uv);
float cutoffThreshold = 0.14;
float cutoff = step(cutoffThreshold, inputColor.a);
float threshold = 0.005;
outputColor = mix(
vec4(1, 0, 0, 1),
vec4(0, 0, 1, 1),
cutoff
);
While this kinda works, it produces really noticeable artefacts along the edges:
I think the problem lays in my blending operation. I tried enabling blending only when drawing to my framebuffer and disableing it when rendering my main quad without much success.
Here is my program:
const CONFIG = {
ballsCount: 10,
ballRadius: isMobileBrowser() ? 75 : 200,
gravity: 0.1,
lineWidth: innerWidth / 2,
startVelocityX: { min: 0, max: 0.1 },
startVelocityY: { min: 1, max: 3 },
}
const contentWrapper = document.querySelector('.content')
const canvas = document.createElement('canvas')
const gl = canvas.getContext('webgl2')
const dpr = devicePixelRatio > 2.5 ? 2.5 : devicePixelRatio
if (!gl) {
showWebGL2NotSupported()
}
const lineVertexArrayObject = gl.createVertexArray()
const quadVertexArrayObject = gl.createVertexArray()
const ballsVertexArrayObject = gl.createVertexArray()
const ballsOffsetsBuffer = gl.createBuffer()
let oldTime = 0
let lineAngle = 0
// WebGL Programs
let lineWebGLProgram
let quadWebGLProgram
let ballsWebGLProgram
let quadTextureUniformLoc
let lineAngleUniformLoc
let lineVertexArray
let ballsOffsetsArray
// Not for rendering, just storing the balls velocities
let ballsVelocitiesArray
/* ------- Create horizontal line WebGL program ------- */
{
const vertexShader = makeWebglShader(gl, {
shaderType: gl.VERTEX_SHADER,
shaderSource: `#version 300 es
uniform mat4 u_projectionMatrix;
uniform vec2 u_resolution;
uniform float u_angle;
in vec4 a_position;
mat4 rotationZ( in float angle ) {
return mat4(
cos(angle), -sin(angle), 0.0, 0.0,
sin(angle), cos(angle), 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0
);
}
void main () {
gl_Position = u_projectionMatrix * (rotationZ(u_angle) * a_position + vec4(u_resolution.xy / 2.0, 0.0, 1.0));
}
`,
})
const fragmentShader = makeWebglShader(gl, {
shaderType: gl.FRAGMENT_SHADER,
shaderSource: `#version 300 es
precision highp float;
out vec4 outputColor;
void main () {
outputColor = vec4(0, 0, 1, 1);
}
`,
})
lineWebGLProgram = makeWebglProram(gl, {
vertexShader,
fragmentShader,
})
}
/* ------- Create and assign horizontal line WebGL attributes ------- */
{
lineVertexArray = new Float32Array([-CONFIG.lineWidth / 2, 0, CONFIG.lineWidth / 2, 0])
const vertexBuffer = gl.createBuffer()
const a_position = gl.getAttribLocation(lineWebGLProgram, 'a_position')
gl.bindVertexArray(lineVertexArrayObject)
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
gl.bufferData(gl.ARRAY_BUFFER, lineVertexArray, gl.STATIC_DRAW)
gl.enableVertexAttribArray(a_position)
gl.vertexAttribPointer(a_position, 2, gl.FLOAT, false, 0, 0)
gl.bindVertexArray(null)
}
/* ------- Create metaballs WebGL program ------- */
{
const vertexShader = makeWebglShader(gl, {
shaderType: gl.VERTEX_SHADER,
shaderSource: `#version 300 es
uniform mat4 u_projectionMatrix;
in vec4 a_position;
in vec4 a_offsetPosition;
in vec2 a_uv;
out vec2 v_uv;
void main () {
vec4 correctOffsetedPosition = a_offsetPosition + a_position;
gl_Position = u_projectionMatrix * correctOffsetedPosition;
v_uv = a_uv;
}
`
})
const fragmentShader = makeWebglShader(gl, {
shaderType: gl.FRAGMENT_SHADER,
shaderSource: `#version 300 es
precision highp float;
in vec2 v_uv;
out vec4 outputColor;
void main () {
float dist = distance(v_uv, vec2(0.5));
float c = 0.5 - dist;
outputColor = vec4(vec3(1.0), c);
}
`
})
ballsWebGLProgram = makeWebglProram(gl, {
vertexShader,
fragmentShader,
})
}
/* ------- Create and assign metaballs WebGL attributes ------- */
{
const vertexArray = new Float32Array([
-CONFIG.ballRadius / 2, CONFIG.ballRadius / 2,
CONFIG.ballRadius / 2, CONFIG.ballRadius / 2,
CONFIG.ballRadius / 2, -CONFIG.ballRadius / 2,
-CONFIG.ballRadius / 2, CONFIG.ballRadius / 2,
CONFIG.ballRadius / 2, -CONFIG.ballRadius / 2,
-CONFIG.ballRadius / 2, -CONFIG.ballRadius / 2
])
const uvsArray = makeQuadUVs()
ballsOffsetsArray = new Float32Array(CONFIG.ballsCount * 2)
ballsVelocitiesArray = new Float32Array(CONFIG.ballsCount * 2)
for (let i = 0; i < CONFIG.ballsCount; i++) {
ballsOffsetsArray[i * 2 + 0] = Math.random() * innerWidth
ballsOffsetsArray[i * 2 + 1] = Math.random() * innerHeight
ballsVelocitiesArray[i * 2 + 0] = (Math.random() * 2 - 1) * CONFIG.startVelocityX.max + CONFIG.startVelocityX.min
ballsVelocitiesArray[i * 2 + 1] = Math.random() * CONFIG.startVelocityY.max + CONFIG.startVelocityY.min
}
const vertexBuffer = gl.createBuffer()
const uvsBuffer = gl.createBuffer()
const a_position = gl.getAttribLocation(ballsWebGLProgram, 'a_position')
const a_uv = gl.getAttribLocation(ballsWebGLProgram, 'a_uv')
const a_offsetPosition = gl.getAttribLocation(ballsWebGLProgram, 'a_offsetPosition')
gl.bindVertexArray(ballsVertexArrayObject)
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
gl.bufferData(gl.ARRAY_BUFFER, vertexArray, gl.STATIC_DRAW)
gl.enableVertexAttribArray(a_position)
gl.vertexAttribPointer(a_position, 2, gl.FLOAT, false, 0, 0)
gl.bindBuffer(gl.ARRAY_BUFFER, uvsBuffer)
gl.bufferData(gl.ARRAY_BUFFER, uvsArray, gl.STATIC_DRAW)
gl.enableVertexAttribArray(a_uv)
gl.vertexAttribPointer(a_uv, 2, gl.FLOAT, false, 0, 0)
gl.bindBuffer(gl.ARRAY_BUFFER, ballsOffsetsBuffer)
gl.bufferData(gl.ARRAY_BUFFER, ballsOffsetsArray, gl.DYNAMIC_DRAW)
gl.enableVertexAttribArray(a_offsetPosition)
gl.vertexAttribPointer(a_offsetPosition, 2, gl.FLOAT, false, 0, 0)
gl.vertexAttribDivisor(a_offsetPosition, 1)
gl.bindVertexArray(null)
}
/* ------- Create fullscreen quad WebGL program ------- */
{
const vertexShader = makeWebglShader(gl, {
shaderType: gl.VERTEX_SHADER,
shaderSource: `#version 300 es
uniform mat4 u_projectionMatrix;
in vec4 a_position;
in vec2 a_uv;
out vec2 v_uv;
void main () {
gl_Position = u_projectionMatrix * a_position;
v_uv = a_uv;
}
`
})
const fragmentShader = makeWebglShader(gl, {
shaderType: gl.FRAGMENT_SHADER,
shaderSource: `#version 300 es
precision highp float;
uniform sampler2D u_texture;
in vec2 v_uv;
out vec4 outputColor;
void main () {
vec4 inputColor = texture(u_texture, v_uv);
float cutoffThreshold = 0.14;
float cutoff = step(cutoffThreshold, inputColor.a);
float threshold = 0.005;
outputColor = mix(
vec4(1, 0, 0, 1),
vec4(0, 0, 1, 1),
cutoff
);
cutoffThreshold += 0.001;
cutoff = smoothstep(cutoffThreshold - threshold, cutoffThreshold + threshold, inputColor.a);
outputColor = mix(
outputColor,
vec4(1, 0, 0, 1),
cutoff
);
cutoffThreshold += 0.05;
cutoff = smoothstep(cutoffThreshold - threshold, cutoffThreshold + threshold, inputColor.a);
outputColor = mix(
outputColor,
vec4(0, 1, 0, 1),
cutoff
);
// outputColor = mix(inputColor, mix(baseColor, metaballsColor, cutoff), 0.3);
// outputColor = inputColor;
}
`
})
quadWebGLProgram = makeWebglProram(gl, {
vertexShader,
fragmentShader,
})
}
/* ------- Create and assign fullscreen quad WebGL attributes ------- */
{
const vertexArray = new Float32Array([
0, innerHeight / 2,
innerWidth / 2, innerHeight / 2,
innerWidth / 2, 0,
0, innerHeight / 2,
innerWidth / 2, 0,
0, 0
])
const uvsArray = makeQuadUVs()
const vertexBuffer = gl.createBuffer()
const uvsBuffer = gl.createBuffer()
const a_position = gl.getAttribLocation(quadWebGLProgram, 'a_position')
const a_uv = gl.getAttribLocation(quadWebGLProgram, 'a_uv')
gl.bindVertexArray(quadVertexArrayObject)
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
gl.bufferData(gl.ARRAY_BUFFER, vertexArray, gl.STATIC_DRAW)
gl.enableVertexAttribArray(a_position)
gl.vertexAttribPointer(a_position, 2, gl.FLOAT, false, 0, 0)
gl.bindBuffer(gl.ARRAY_BUFFER, uvsBuffer)
gl.bufferData(gl.ARRAY_BUFFER, uvsArray, gl.STATIC_DRAW)
gl.enableVertexAttribArray(a_uv)
gl.vertexAttribPointer(a_uv, 2, gl.FLOAT, false, 0, 0)
gl.bindVertexArray(null)
}
/* ------- Create WebGL texture to render to ------- */
gl.getExtension('EXT_color_buffer_float')
gl.getExtension('EXT_float_blend')
gl.getExtension('OES_texture_float_linear')
const targetTextureWidth = innerWidth * dpr
const targetTextureHeight = innerHeight * dpr
const targetTexture = gl.createTexture()
gl.bindTexture(gl.TEXTURE_2D, targetTexture)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, targetTextureWidth, targetTextureHeight, 0, gl.RGBA, gl.FLOAT, null)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
gl.bindTexture(gl.TEXTURE_2D, null)
/* ------- Create WebGL framebuffer to render to ------- */
const framebuffer = gl.createFramebuffer()
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer)
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, targetTexture, 0)
gl.bindFramebuffer(gl.FRAMEBUFFER, null)
init()
function init () {
document.body.appendChild(canvas)
resize()
window.addEventListener('resize', resize)
gl.enable(gl.BLEND)
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
// gl.blendEquation(gl.FUNC_SUBTRACT)
const projectionMatrix = makeProjectionMatrix(innerWidth / 2, innerHeight / 2)
let u_projectionMatrix
gl.useProgram(ballsWebGLProgram)
u_projectionMatrix = gl.getUniformLocation(ballsWebGLProgram, 'u_projectionMatrix')
gl.uniformMatrix4fv(u_projectionMatrix, false, projectionMatrix)
gl.useProgram(null)
gl.useProgram(quadWebGLProgram)
quadTextureUniformLoc = gl.getUniformLocation(quadWebGLProgram, 'u_texture')
gl.uniform1i(quadTextureUniformLoc, 0)
u_projectionMatrix = gl.getUniformLocation(quadWebGLProgram, 'u_projectionMatrix')
gl.uniformMatrix4fv(u_projectionMatrix, false, projectionMatrix)
gl.useProgram(null)
gl.useProgram(lineWebGLProgram)
u_projectionMatrix = gl.getUniformLocation(lineWebGLProgram, 'u_projectionMatrix')
gl.uniformMatrix4fv(u_projectionMatrix, false, projectionMatrix)
const u_resolution = gl.getUniformLocation(lineWebGLProgram, 'u_resolution')
gl.uniform2f(u_resolution, innerWidth, innerHeight)
lineAngleUniformLoc = gl.getUniformLocation(lineWebGLProgram, 'u_angle')
gl.uniform1f(lineAngleUniformLoc, lineAngle * Math.PI / 180)
gl.useProgram(null)
requestAnimationFrame(renderFrame)
}
let a = true
document.addEventListener('click', () => {
a = !a
})
function renderFrame (ts) {
const dt = ts - oldTime
oldTime = ts
for (let i = 0; i < CONFIG.ballsCount; i++) {
ballsVelocitiesArray[i * 2 + 1] += CONFIG.gravity
ballsOffsetsArray[i * 2 + 0] += ballsVelocitiesArray[i * 2 + 0]
ballsOffsetsArray[i * 2 + 1] += ballsVelocitiesArray[i * 2 + 1]
if (ballsOffsetsArray[i * 2 + 0] < CONFIG.ballRadius / 2) {
ballsOffsetsArray[i * 2 + 0] = CONFIG.ballRadius / 2
ballsVelocitiesArray[i * 2 + 0] *= -1
}
if (ballsOffsetsArray[i * 2 + 0] > innerWidth - CONFIG.ballRadius / 2) {
ballsOffsetsArray[i * 2 + 0] = innerWidth - CONFIG.ballRadius / 2
ballsVelocitiesArray[i * 2 + 0] *= -1
}
if (ballsOffsetsArray[i * 2 + 1] - CONFIG.ballRadius > innerHeight) {
ballsOffsetsArray[i * 2 + 1] = -CONFIG.ballRadius
ballsVelocitiesArray[i * 2 + 1] = 5 + Math.random() * 3
}
}
checkLine()
gl.bindBuffer(gl.ARRAY_BUFFER, ballsOffsetsBuffer)
gl.bufferData(gl.ARRAY_BUFFER, ballsOffsetsArray, gl.DYNAMIC_DRAW)
if (a) {
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer)
}
gl.viewport(0, 0, targetTextureWidth, targetTextureHeight)
gl.clearColor(0.1, 0.1, 0.1, 0)
gl.clear(gl.COLOR_BUFFER_BIT)
gl.bindVertexArray(ballsVertexArrayObject)
gl.useProgram(ballsWebGLProgram)
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, CONFIG.ballsCount)
gl.bindVertexArray(null)
gl.bindFramebuffer(gl.FRAMEBUFFER, null)
gl.viewport(0, 0, canvas.width, canvas.height)
if (a) {
gl.bindVertexArray(quadVertexArrayObject)
gl.useProgram(quadWebGLProgram)
gl.bindTexture(gl.TEXTURE_2D, targetTexture)
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true)
gl.drawArrays(gl.TRIANGLES, 0, 6)
gl.useProgram(null)
gl.bindVertexArray(null)
gl.bindTexture(gl.TEXTURE_2D, null)
}
lineAngle = Math.sin(ts * 0.001) * 30
gl.bindVertexArray(lineVertexArrayObject)
gl.useProgram(lineWebGLProgram)
gl.uniform1f(lineAngleUniformLoc, -lineAngle * Math.PI / 180)
gl.drawArrays(gl.LINES, 0, 2)
gl.useProgram(null)
gl.bindVertexArray(null)
requestAnimationFrame(renderFrame)
}
function getLineBounds () {
const x1 = lineVertexArray[0]
const y1 = lineVertexArray[1]
const x2 = lineVertexArray[2]
const y2 = lineVertexArray[3]
if (lineAngle === 0) {
const minX = Math.min(x1, x2)
const minY = Math.min(y1, y2)
const maxX = Math.max(x1, x2)
const maxY = Math.max(y1, y2)
return {
x: x1 + minX,
y: y1 + minY,
width: maxX - minX,
height: maxY - minY,
}
} else {
const rotation = lineAngle * Math.PI / 180
const sin = Math.sin(rotation)
const cos = Math.cos(rotation)
const x1r = cos * x1 + sin * y1
const x2r = cos * x2 + sin * y2
const y1r = cos * y1 + sin * x1
const y2r = cos * y2 + sin * x2
const x = innerWidth / 2 + x1 + Math.min(x1r, x2r) + CONFIG.lineWidth / 2
const y = innerHeight / 2 + y1 + Math.min(y1r, y2r) + CONFIG.lineWidth / 2
const width = Math.max(x1r, x2r) - Math.min(x1r, x2r)
const height = Math.max(y1r, y2r) - Math.min(y1r, y2r)
return {
x,
y,
width,
height,
}
}
}
function checkLine () {
const lineBounds = getLineBounds()
const ballRadius = CONFIG.ballRadius / 7
for (let i = 0; i < CONFIG.ballsCount; i++) {
const ballx = ballsOffsetsArray[i * 2 + 0]
const bally = ballsOffsetsArray[i * 2 + 1]
const ballvx = ballsVelocitiesArray[i * 2 + 0]
const ballvy = ballsVelocitiesArray[i * 2 + 1]
if (ballx + ballRadius / 2 > lineBounds.x && ballx - ballRadius / 2 < lineBounds.x + lineBounds.width) {
const lineRotation = lineAngle * Math.PI / 180
const cos = Math.cos(lineRotation)
const sin = Math.sin(lineRotation)
let x = ballx - innerWidth / 2
let y = bally - innerHeight / 2
let vx1 = cos * ballvx + sin * ballvy
let vy1 = cos * ballvy - sin * ballvx
let y1 = cos * y - sin * x
if (y1 > -ballRadius / 2 && y1 < vy1) {
// debugger
const x2 = cos * x + sin * y
y1 = -ballRadius / 2
vy1 *= -0.45
x = cos * x2 - sin * y1
y = cos * y1 + sin * x2
ballsVelocitiesArray[i * 2 + 0] = cos * vx1 - sin * vy1
ballsVelocitiesArray[i * 2 + 1] = cos * vy1 + sin * vx1
ballsOffsetsArray[i * 2 + 0] = innerWidth / 2 + x
ballsOffsetsArray[i * 2 + 1] = innerHeight / 2 + y
}
}
}
}
function resize () {
canvas.width = innerWidth * dpr
canvas.height = innerHeight * dpr
canvas.style.width = `${innerWidth}px`
canvas.style.height = `${innerHeight}px`
}
/* ------- WebGL helpers ------- */
function makeQuadUVs () {
return new Float32Array([0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1])
}
function makeWebglShader (gl, { shaderType, shaderSource }) {
const shader = gl.createShader(shaderType)
gl.shaderSource(shader, shaderSource)
gl.compileShader(shader)
const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS)
if (success) {
return shader
}
console.error(`
Error in ${shaderType === gl.VERTEX_SHADER ? 'vertex' : 'fragment'} shader:
${gl.getShaderInfoLog(shader)}
`)
gl.deleteShader(shader)
}
function makeWebglProram (gl, { vertexShader, fragmentShader }) {
const program = gl.createProgram()
gl.attachShader(program, vertexShader)
gl.attachShader(program, fragmentShader)
gl.linkProgram(program)
const success = gl.getProgramParameter(program, gl.LINK_STATUS)
if (success) {
return program
}
console.error(gl.getProgramInfoLog(program))
gl.deleteProgram(program)
}
function makeProjectionMatrix (width, height) {
return new Float32Array([
2 / width, 0, 0, 0,
0, -2 / height, 0, 0,
0, 0, 0, 0,
-1, 1, 0, 1,
])
}
function showWebGL2NotSupported () {
const errorMessageWrapper = document.createElement('div')
if (isIOS()) {
const iOSVersion = getIOSVersion().major
if (iOSVersion === 13) {
errorMessageWrapper.innerHTML = `
<p>Please update your device to iOS / iPadOS 14 so you can see this demo.</p>
`
} else if (iOSVersion === 14) {
errorMessageWrapper.innerHTML = `
<p>In order to see WebGL2 content, you need to enable it from your device settings.</p>
<p>Settings > Safari > Advanced > Experimental Features > WebGL2.0</p>
`
}
} else {
errorMessageWrapper.innerHTML = `
<h1>Your browser does not support WebGL2</h1>
<p>Please try one of these alternative browsers:</p>
<ul>
<li>Microsoft Edge (version 79+)</li>
<li>Mozilla Firefox (version 51+)</li>
<li>Google Chrome (version 56+)</li>
<li>Opera (version 43+)</li>
</ul>
`
}
errorMessageWrapper.classList.add('webgl2-error')
document.body.appendChild(errorMessageWrapper)
}
/* ------- Generic helpers ------- */
function isMobileBrowser () {
return (function (a) {
if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4))) {
return true
}
return false
})(navigator.userAgent || navigator.vendor || window.opera)
}
function isIOS () {
return (/AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent)) || isIPadOS()
}
function isIPadOS () {
return navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1 && !window.MSStream
}
function getIOSVersion () {
const found = navigator.userAgent.match(/(iPhone|iPad); (CPU iPhone|CPU) OS (\d+)_(\d+)(_(\d+))?\s+/)
if (!found || found.length < 4) {
return {
major: 0,
minor: 0
}
}
return {
major: parseInt(found[3], 10),
minor: parseInt(found[4], 10)
}
}
* { margin: 0; padding: 0; }
EDIT
By the first comment suggestion, I enabled the EXT_color_buffer_float and OES_texture_float_linear extensions and augmented my texImage2D call like this:
gl.getExtension('EXT_color_buffer_float')
gl.getExtension('OES_texture_float_linear')
// ...
const targetTexture = gl.createTexture()
gl.bindTexture(gl.TEXTURE_2D, targetTexture)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, targetTextureWidth, targetTextureHeight, 0, gl.RGBA, gl.FLOAT, null)
Unfortunately this results in corrupted rendering on all of my browsers running on MacOS. I updated my code snippet to reflect the new changes.
I read in MDN's Webgl2 best practices that EXT_float_blend is also required in order to allow blending in framebuffers, but is implicitly allowed when requesting EXT_color_buffer_float?
On a sidenote, I guess the OES_texture_float_linear would not work on mobile devices? So if the extension is not available, I would need to fallback to 8bit RGBA textures?
Thanks!
I'm pretty sure the issue the texture your rendering to is 8bits. Switch it to a floating point texture (RGBA32F) You'll need to check for and enable EXT_color_buffer_float and OES_texture_float_linear
Update
You say it won't work on mobile but you're using WebGL2 which hasn't shipped on iPhone yet (2021/1/3). As for RGBA32F not being renderable on mobile you could try RGBA16F. You'll have to check for and enable the corresponding extensions, EXT_color_buffer_half_float and OES_texture_half_float_linear. Your current code is not checking that the extensions actually exist (I'm assuming that was just to keep the code minimal)
The corruption is that your circle calculation draws alpha < 0 outside the circle but inside the quad. Before that was getting clipped to 0 because of the texture format but now with floating point textures it's not so it affects other circles.
Either discard if c <= 0 or clamp so it doesn't go below 0.
Note: you might find coloring faster and more flexible using a ramp texture. example, example2
Also note: It would have been nice if you'd created a more minimal repo. There's no need for the animation to show either issue
Update 2
Something else to point out, maybe you already knew this, but, the circle calculation
float dist = distance(v_uv, vec2(0.5));
float c = 0.5 - dist;
means the max value is 0.5 so half the range is being thrown away.
Switching to this
float dist = distance(v_uv, vec2(0.5)) * 2.0;
float c = 1.0 - dist;
Changes the range from 0 to 1 which is arguably better for shading later and to not throw away a bunch of precision in the texture. Example
Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 2 years ago.
Improve this question
I have the Image as texture. example
And I want to repeat only the part of this texture.
For example the third rectangle in first row from [0.5,0] to [0.75,0.25]. (the brown one)
Is it any way to do it in Webgl 2 ?
ps. maybe it could be done using textureOffset and something else...
Thank you!
To repeat part of a texture you can do that in the shader by setting some uniforms that define the section of the texture you wish to repeat.
// uniform that defines the x, y (top left) and width and height of repeat
uniform vec4 repeat; // x, y, w, h
You can then repeat the texture as follows
gl_FragColor = vec4(texture2D(tex, mod(uv, vec2(1)) * repeat.zw + repeat.xy));
There is one issue when you use a texture that is not set with NEAREST as the interpolation will case pixels at the edge to bleed in. This will cause unwanted visible seams where the texture repeats.
The easiest way to fix is the reduce the repeating pattern size by a pixel and the pattern start position in by half a pixel.
// example for 256 texture size
const pixel = 1 / 256;
const repeat = [0.5 + pixel / 2, 0.0 + pixel / 2 ,0.25 - pixel, 0.25 - pixel];
Example
Example creates a texture (image on right) and then renders a random part of that text in the canvas on the left. The repeat set to a random amount each new render
const shaders = {
vs: `
attribute vec2 vert;
varying vec2 uv;
void main() {
uv = vert;
gl_Position = vec4(vert, 0.0, 1.0);
}`,
fs: `precision mediump float;
uniform sampler2D tex;
varying vec2 uv;
uniform vec4 repeat;
uniform vec2 tiles;
void main(){
gl_FragColor = vec4(texture2D(tex, mod(uv * tiles, vec2(1)) * repeat.zw + repeat.xy));
}`
};
const colors = "#ff0000,#ff8800,#ffff00,#88ff00,#00ff00,#00ff88,#00f0f0,#0088ff,#0000ff,#8800ff,#ff00ff,#ff0088".split(",");
const randCol = (cols = colors) => cols[Math.random() * cols.length | 0];
const F32A = a => new Float32Array(a), UI16A = a => new Uint16Array(a);
const GLBuffer = (data, type = gl.ARRAY_BUFFER, use = gl.STATIC_DRAW, buf) => (gl.bindBuffer(type, buf = gl.createBuffer()), gl.bufferData(type, data, use), buf);
const GLLocs = (shr, type, ...names) => names.reduce((o,name) => (o[name] = (gl[`get${type}Location`])(shr, name), o), {});
const GLShader = (prg, source, type = gl.FRAGMENT_SHADER, shr) => {
gl.shaderSource(shr = gl.createShader(type), source);
gl.compileShader(shr);
gl.attachShader(prg, shr);
}
function texture(gl, image, {min = "LINEAR", mag = "LINEAR"} = {}) {
const texture = gl.createTexture();
target = gl.TEXTURE_2D;
gl.bindTexture(target, texture);
gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, gl[min]);
gl.texParameteri(target, gl.TEXTURE_MAG_FILTER, gl[mag]);
gl.texImage2D(target, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
return texture;
}
const bindTexture = (texture, unit = 0) => { gl.activeTexture(gl.TEXTURE0 + unit); gl.bindTexture(gl.TEXTURE_2D, texture) }
const createTag = (tag, props = {}) => Object.assign(document.createElement(tag), props);
const appendEl = (par, ...sibs) => sibs.reduce((p,sib) => (p.appendChild(sib), p),par);
function createTexture(width = 256, height = 256) {
const tex = createTag("canvas", {width, height, className: "texture"});
appendEl(document.body, tex);
const ctx = tex.getContext("2d");
var x = 4, y = 4, count = 0;
const xStep = width / x, yStep = height / y;
ctx.font = (yStep * 0.95 | 0) + "px arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
while (y--) {
x = 4;
while (x--) {
ctx.fillStyle = randCol();
ctx.fillRect(x * xStep, y * yStep, xStep, yStep);
ctx.fillStyle = "#000";
ctx.fillText((count++).toString(16).toUpperCase(), (x + 0.5) * xStep, (y + 0.5) * yStep);
}
}
ctx.setTransform(1,0,0,-1,0,height);
ctx.globalCompositeOperation = "copy";
ctx.drawImage(tex,0,0);
bindTexture(texture(gl, tex));
ctx.drawImage(tex,0,0);
}
var W;
const gl = canvas.getContext("webgl");
requestAnimationFrame(renderRandom);
addEventListener("resize", renderRandom);
const prog = gl.createProgram();
GLShader(prog, shaders.vs, gl.VERTEX_SHADER);
GLShader(prog, shaders.fs);
gl.linkProgram(prog);
gl.useProgram(prog);
const locs = GLLocs(prog, "Uniform", "repeat", "tiles");
const attIdxs = GLLocs(prog, "Attrib", "vert");
GLBuffer(F32A([-1,-1, 1,-1, 1,1, -1,1]));
GLBuffer(UI16A([1,2,3, 0,1,3]), gl.ELEMENT_ARRAY_BUFFER);
gl.enableVertexAttribArray(attIdxs.vert);
gl.vertexAttribPointer(attIdxs.vert, 2, gl.FLOAT, false, 0, 0);
createTexture();
function renderRandom() {
gl.viewport(0, 0, W = canvas.width = Math.min(innerWidth,innerHeight), canvas.height = W);
const textPxSize = 1/256;
const x = (Math.random() * 4 | 0) / 4 + textPxSize / 2;
const y = (Math.random() * 4 | 0) / 4 + textPxSize / 2;
const tiles = Math.random() * 8 + 1 | 0;
gl.uniform4fv(locs.repeat, F32A([x,y,0.25 - textPxSize, 0.25 - textPxSize ]));
gl.uniform2fv(locs.tiles, F32A([tiles, tiles]));
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
setTimeout(renderRandom, 4000);
}
canvas {
border: 2px solid black;
}
.texture { width: 128px; height: 128px;}
<canvas id="canvas"></canvas>
I am trying to implement conway's game of life in 3D. Basically, I am experimenting it with an extra dimension.
I am instantiating a list of cubes at the start of the game and give each one of them an index that's going to be associated with a logic object where I call twgl.drawObjectList if it's alive, else I will skip it within a function that I am using requestAnimationFrame on.
The problem is that the FPS drops below 1 when I make a 50*50*50 (125000 cubes) game. Is this normal? Am I doing the correct approach?
Edit:
function newGame (xDimV, yDimV, zDimV, gameSelected = false) {
// No game to load
if (!gameSelected) {
xDim = xDimV;
yDim = yDimV;
zDim = zDimV;
} else {
xDim = gameSelected[0][0].length;
yDim = gameSelected[0].length;
zDim = gameSelected.length;
}
myGame = Object.create(game);
myGame.consutructor(xDim , yDim , zDim, gameSelected);
objects = [];
for (var z = 0; z < zDim; z++) {
for (var y = 0; y < yDim; y++){
for (var x = 0; x < xDim; x++){
var uniforms = {
u_colorMult: chroma.hsv(emod(baseHue + rand(0, 120), 360), rand(0.5,
1), rand(0.5, 1)).gl(),
u_world: m4.identity(),
u_worldInverseTranspose: m4.identity(),
u_worldViewProjection: m4.identity(),
};
var drawObjects = [];
drawObjects.push({
programInfo: programInfo,
bufferInfo: cubeBufferInfo,
uniforms: uniforms,
});
objects.push({
translation: [(x*scale)-xDim*scale/2, (z*scale), (y*scale)-yDim*scale/2],
scale: scale,
uniforms: uniforms,
bufferInfo: cubeBufferInfo,
programInfo: programInfo,
drawObject: drawObjects,
index: [z, y, x],
});
}
}
}
requestAnimationFrame(render);
}
var then = 0;
function render(time) {
time *= 0.001;
var elapsed = time - then;
then = time;
twgl.resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.clearColor(255, 255, 0, 0.1);
var fovy = 30 * Math.PI / 180;
var projection = m4.perspective(fovy, gl.canvas.clientWidth / gl.canvas.clientHeight, 0.5, 10000);
var eye = [cameraX, cameraY, cameraZ];
var target = [cameraX, cameraY, 10];
var up = [0, 1, 0];
var camera = m4.lookAt(eye, target, up);
var view = m4.inverse(camera);
var viewProjection = m4.multiply(projection, view);
viewProjection = m4.rotateX(viewProjection, phi);
viewProjection = m4.rotateY(viewProjection, theta);
targetTimer -= elapsed;
objects.forEach(function(obj) {
var uni = obj.uniforms;
var world = uni.u_world;
m4.identity(world);
m4.translate(world, obj.translation, world);
m4.scale(world, [obj.scale, obj.scale, obj.scale], world);
m4.transpose(m4.inverse(world, uni.u_worldInverseTranspose), uni.u_worldInverseTranspose);
m4.multiply(viewProjection, uni.u_world, uni.u_worldViewProjection);
if (myGame.life[obj.index[0]][obj.index[1]][obj.index[2]] === 1) {
twgl.drawObjectList(gl, obj.drawObject);
}
});
if (targetTimer <= 0 && !paused) {
targetTimer = targetChangeInterval / speed;
myGame.nextGen();
setGameStatus();
myGame.resetStatus();
}
requestAnimationFrame(render);
}
Thanks in advance.
125k cubes is quite a lot. Typical AAA games generally make 1000 to 5000 draw calls total. There are breakdowns on the web of various game engines and how many draw calls they take the generate a frame.
Here's a talk with several methods. It includes putting all the cubes in one giant mesh and moving them around in JavaScript so they're is effectively one draw call.
If it was me I'd do that and I'd make a texture with one pixel per cube. So for 125k cubes that texture would be like 356x356 though I'd probably choose something more fitting the cube size like 500x300 (since each face slice is 50x50). For each vertex of each cube I'd have an attribute with a UV pointing to a specific pixel in that texture. In other words for the first vertices of the first cube there would be an attribute that UVs that repeats 36 times, in a new UVs for the 2nd cube that repeats 36 times,
attribute vec2 cubeUV;
Then I can use the cubeUV to lookup a pixel in the texture whether or not the cube should be on or off
attribute vec2 cubeUV;
uniform sampler2D lifeTexture;
void main() {
float cubeOn = texture2D(lifeTexture, cubeUV).r;
}
I could clip out the cube pretty easily with
if (cubeOn < 0.5) {
gl_Position = vec4(2, 2, 2, 1); // outside clip space
return;
}
// otherwise do the calcs for a cube
In this case the cubes don't need to move so all JavaScript has to do each frame is compute life in some Uint8Array and then call
gl.bindTexture(gl.TEXTURE_2D, lifeTexture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, width, height, 0,
gl.LUMINANCE, gl.UNSIGNED_BYTE, lifeStatusUint8Array);
every frame and make one draw call.
Note: You can effectively see examples of this type of shader here except that that shdaer is not looking at a texture with life running in it, instead it's looking at a texture with 4 seconds of audio data in it. It's also generating the cubeId from vertexId and generating the cube vertices and normals from vertexId. That would make it slower than putting that data in attributes but it is an example of positioning or drawing cubes based on data coming from a texture.
const vs = `
attribute vec4 position;
attribute vec3 normal;
attribute vec2 cubeUV;
uniform mat4 u_matrix;
uniform sampler2D u_lifeTex;
varying vec3 v_normal;
void main() {
float on = texture2D(u_lifeTex, cubeUV).r;
if (on < .5) {
gl_Position = vec4(20, 20, 20, 1);
return;
}
gl_Position = u_matrix * position;
v_normal = normal;
}
`;
const fs = `
precision mediump float;
varying vec3 v_normal;
void main() {
gl_FragColor = vec4(v_normal * .5 + .5, 1);
}
`;
const oneFace = [
[ -1, -1, ],
[ 1, -1, ],
[ -1, 1, ],
[ -1, 1, ],
[ 1, -1, ],
[ 1, 1, ],
];
const m4 = twgl.m4;
const gl = document.querySelector("canvas").getContext("webgl");
// compiles shaders, links program, looks up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
const cubeSize = 50;
const texBuf = makeCubeTexBuffer(gl, cubeSize);
const tex = twgl.createTexture(gl, {
src: texBuf.buffer,
width: texBuf.width,
format: gl.LUMINANCE,
wrap: gl.CLAMP_TO_EDGE,
minMag: gl.NEAREST,
});
const arrays = makeCubes(cubeSize, texBuf);
// calls gl.createBuffer, gl.bindBuffer, gl.bufferData for each array
const bufferInfo = twgl.createBufferInfoFromArrays(gl, arrays);
function render(time) {
time *= 0.001; // seconds
twgl.resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.enable(gl.DEPTH_TEST);
//gl.enable(gl.CULL_FACE);
const fov = Math.PI * .25;
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const zNear = .01;
const zFar = 1000;
const projection = m4.perspective(fov, aspect, zNear, zFar);
const radius = cubeSize * 2.5;
const speed = time * .1;
const position = [
Math.sin(speed) * radius,
Math.sin(speed * .7) * radius * .7,
Math.cos(speed) * radius,
];
const target = [0, 0, 0];
const up = [0, 1, 0];
const camera = m4.lookAt(position, target, up);
const view = m4.inverse(camera);
const mat = m4.multiply(projection, view);
// do life
// (well, randomly turn on/off cubes)
for (let i = 0; i < 100; ++i) {
texBuf.buffer[Math.random() * texBuf.buffer.length | 0] = Math.random() > .5 ? 255 : 0;
}
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, texBuf.width, texBuf.height,
0, gl.LUMINANCE, gl.UNSIGNED_BYTE, texBuf.buffer);
gl.useProgram(programInfo.program)
// calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
twgl.setUniforms(programInfo, {
u_matrix: mat,
u_lifeTex: tex,
});
// calls gl.drawArrays or gl.drawElements
twgl.drawBufferInfo(gl, bufferInfo);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
// generate cubes
function makeCube(vertOffset, off, uv, arrays) {
const positions = arrays.position;
const normals = arrays.normal;
const cubeUV = arrays.cubeUV;
for (let f = 0; f < 6; ++f) {
const axis = f / 2 | 0;
const sign = f % 2 ? -1 : 1;
const major = (axis + 1) % 3;
const minor = (axis + 2) % 3;
for (let i = 0; i < 6; ++i) {
const offset2 = vertOffset * 2;
const offset3 = vertOffset * 3;
positions[offset3 + axis ] = off[axis] + sign;
positions[offset3 + major] = off[major] + oneFace[i][0];
positions[offset3 + minor] = off[minor] + oneFace[i][1];
normals[offset3 + axis ] = sign;
normals[offset3 + major] = 0;
normals[offset3 + minor] = 0;
cubeUV[offset2 + 0] = uv[0];
cubeUV[offset2 + 1] = uv[1];
++vertOffset;
}
}
return vertOffset;
}
function makeCubes(size, texBuf) {
const numCubes = size * size * size;
const numVertsPerCube = 36;
const numVerts = numCubes * numVertsPerCube;
const slicesAcross = texBuf.width / size | 0;
const arrays = {
position: new Float32Array(numVerts * 3),
normal: new Float32Array(numVerts * 3),
cubeUV: new Float32Array(numVerts * 2),
};
let spacing = size * 1.2;
let vertOffset = 0;
for (let z = 0; z < size; ++z) {
const zoff = (z / (size - 1) * 2 - 1) * spacing;
for (let y = 0; y < size; ++y) {
const yoff = (y / (size - 1) * 2 - 1) * spacing;
for (let x = 0; x < size; ++x) {
const xoff = (x / (size - 1) * 2 - 1) * spacing;
const sx = z % slicesAcross;
const sy = z / slicesAcross | 0;
const uv = [
(sx * size + x + 0.5) / texBuf.width,
(sy * size + y + 0.5) / texBuf.height,
];
vertOffset = makeCube(vertOffset, [xoff, yoff, zoff], uv, arrays);
}
}
}
arrays.cubeUV = {
numComponents: 2,
data: arrays.cubeUV,
};
return arrays;
}
function makeCubeTexBuffer(gl, cubeSize) {
const numCubes = cubeSize * cubeSize * cubeSize;
const maxTextureSize = Math.min(gl.getParameter(gl.MAX_TEXTURE_SIZE), 2048);
const maxSlicesAcross = maxTextureSize / cubeSize | 0;
const slicesAcross = Math.min(cubeSize, maxSlicesAcross);
const slicesDown = Math.ceil(cubeSize / slicesAcross);
const width = slicesAcross * cubeSize;
const height = slicesDown * cubeSize;
const buffer = new Uint8Array(width * height);
return {
buffer: buffer,
slicesAcross: slicesAcross,
slicesDown: slicesDown,
width: width,
height: height,
};
}
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://twgljs.org/dist/3.x/twgl-full.min.js"></script>
<canvas></canvas>
hoiested from the comments below, using a big merged mesh appears to be 1.3x faster than using instanced drawing. Here's 3 samples
big mesh using texture uvs (same as above)
instanced using texture uvs (less data, same shader)
instanced no texture (no texture, life data is in buffer/attribute)
For me, on my machine #1 can do 60x60x60 cubes (216000) at 60fps whereas both #2 and #3 only get 56x56x56 cubes (175616) at 60fps. Of course other GPUs/system/browsers might be different.
The fps drop is coming from two things most likely:
The overhead of doing 125k matrix operations each tick.
The overhead of doing 125k drawcalls.
You can look into instancing
http://blog.tojicode.com/2013/07/webgl-instancing-with.html?m=1
And possibly move the matrix stuff into a shader
We've created a WebGl application which displays a scene containing multiple objects. The entire scene can be rotated in multiple directions. The application requires the user to be able to zoom up to but NOT thru the object. I know this functionality can be implemented using webgl frameworks such as Three.js and SceneJs. Unfortunately, our application is not leveraging a framework. Is there a way to implement the zoom functionality described here using webgl only? Note: I don't believe object picking will work for us since the user is not required to select any object in the scene. Thanks for your help.
Off the top of my head.
First off you need to know the size of each object in world space. For example if one object is 10 units big and another is 100 units big you probably want to be a different distance from the 100 unit object as the 10 unit object. By world space I also mean if you're scaling the 10 unit object by 9 then in world space it would be 90 units big and again you'd want to get a different distance away then if it was 10 units
You generally compute the size of an object in local space by computing the extents of its vertices. Just go through all the vertices and keep track of the min and max values in x, y, and z. Whether you want to take the biggest value from the object's origin or compute an actual center point is up to you.
So, given the size we can compute how far away you need to be to see the entire object. For the standard perspective matrix you can just work backward. If you know your object is 10 units big then you need to fit 10 units in your frustum. You'd probably actually pick something like 14 units (say size * 1.4) so there's some space around the object.
We know halfFovy, halfSizeToFitOnScreen, we need to compute distance
sohcahtoa
tangent = opposite / adjacent
opposite = halfsizeToFitOnScreen
adjacent = distance
tangent = Math.tan(halfFovY)
Therefore
tangent = sizeToFitOnScreen / distance
tangent * distance = sizeToFitOnScreen
distance = sizeToFitOnScreen / tangent
distance = sizeToFitOnScreen / Math.tan(halfFovY)
So now we know the camera needs to be distance away from the object. There's an entire sphere that's distance away from the object. Where you pick on that sphere is up to you. Assuming you go from where the camera currently is you can compute the direction from the object to the camera
direction = normalize(cameraPos - objectPos)
Now you can compute a point distance away in that direction.
desiredCameraPosition = direction * distance
Now either put the camera there using some lookAt function
matrix = lookAt(desiredCameraPosition, objectPosition, up)
Or lerp between where the camera currently is to it's new desired position
var m4 = twgl.m4;
var v3 = twgl.v3;
twgl.setAttributePrefix("a_");
var gl = twgl.getWebGLContext(document.getElementById("c"));
var programInfo = twgl.createProgramInfo(gl, ["vs", "fs"]);
var shapes = [
twgl.primitives.createCubeBufferInfo(gl, 2),
twgl.primitives.createSphereBufferInfo(gl, 1, 24, 12),
twgl.primitives.createTruncatedConeBufferInfo(gl, 1, 0, 2, 24, 1),
];
function rand(min, max) {
return min + Math.random() * (max - min);
}
function easeInOut(t, start, end) {
var c = end - start;
if ((t /= 0.5) < 1) {
return c / 2 * t * t + start;
} else {
return -c / 2 * ((--t) * (t - 2) - 1) + start;
}
}
// Shared values
var lightWorldPosition = [1, 8, -10];
var lightColor = [1, 1, 1, 1];
var camera = m4.identity();
var view = m4.identity();
var viewProjection = m4.identity();
var targetNdx = 0;
var targetTimer = 0;
var zoomTimer = 0;
var eye = v3.copy([1, 4, -60]);
var target = v3.copy([0, 0, 0]);
var up = [0, 1, 0];
var zoomScale = 1.4;
var zoomDuration = 2;
var targetChangeInterval = 3;
var oldEye;
var oldTarget;
var newEye;
var newTarget;
var tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 2, 2, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([
255,255,255,255,
192,192,192,255,
192,192,192,255,
255,255,255,255]));
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
var objects = [];
var drawObjects = [];
var numObjects = 100;
var baseHue = rand(0, 360);
for (var ii = 0; ii < numObjects; ++ii) {
var uniforms = {
u_lightWorldPos: lightWorldPosition,
u_lightColor: lightColor,
u_diffuseMult: chroma.hsv((baseHue + rand(0, 60)) % 360, 0.4, 0.8).gl(),
u_specular: [1, 1, 1, 1],
u_shininess: 50,
u_specularFactor: 1,
u_diffuse: tex,
u_viewInverse: camera,
u_world: m4.identity(),
u_worldInverseTranspose: m4.identity(),
u_worldViewProjection: m4.identity(),
};
drawObjects.push({
programInfo: programInfo,
bufferInfo: shapes[ii % shapes.length],
uniforms: uniforms,
});
objects.push({
translation: [rand(-50, 50), rand(-50, 50), rand(-50, 50)],
scale: rand(1, 5),
size: 2,
xSpeed: rand(0.2, 0.7),
zSpeed: rand(0.2, 0.7),
uniforms: uniforms,
});
}
var then = Date.now() * 0.001;
function render() {
twgl.resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
var time = Date.now() * 0.001;
var elapsed = time - then;
then = time;
var radius = 6;
var fovy = 30 * Math.PI / 180;
var projection = m4.perspective(fovy, gl.canvas.clientWidth / gl.canvas.clientHeight, 0.5, 200);
targetTimer -= elapsed;
if (targetTimer <= 0) {
targetTimer = targetChangeInterval;
zoomTimer = 0;
targetNdx = (targetNdx + 1) % objects.length;
oldEye = v3.copy(eye);
oldTarget = v3.copy(target);
var targetObj = objects[targetNdx];
newTarget = targetObj.translation;
var halfSize = targetObj.size * targetObj.scale * zoomScale * 0.5;
var distance = halfSize / Math.tan(fovy * 0.5);
var direction = v3.normalize(v3.subtract(eye, newTarget));
newEye = v3.add(newTarget, v3.mulScalar(direction, distance));
}
zoomTimer += elapsed;
var lerp = easeInOut(Math.min(1, zoomTimer / zoomDuration), 0, 1);
eye = v3.lerp(oldEye, newEye, lerp);
target = v3.lerp(oldTarget, newTarget, lerp);
m4.lookAt(eye, target, up, camera);
m4.inverse(camera, view);
m4.multiply(projection, view, viewProjection);
objects.forEach(function(obj, ndx) {
var uni = obj.uniforms;
var world = uni.u_world;
m4.identity(world);
m4.translate(world, obj.translation, world);
m4.rotateX(world, time * obj.xSpeed, world);
m4.rotateZ(world, time * obj.zSpeed, world);
m4.scale(world, [obj.scale, obj.scale, obj.scale], world);
m4.transpose(m4.inverse(world, uni.u_worldInverseTranspose), uni.u_worldInverseTranspose);
m4.multiply(viewProjection, uni.u_world, uni.u_worldViewProjection);
});
twgl.drawObjectList(gl, drawObjects);
requestAnimationFrame(render);
}
render();
body {
margin: 0;
}
canvas {
width: 100vw;
height: 100vh;
display: block;
}
<script src="//twgljs.org/dist/4.x/twgl-full.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/chroma-js/0.6.3/chroma.min.js"></script>
<canvas id="c"></canvas>
<script id="vs" type="notjs">
uniform mat4 u_worldViewProjection;
uniform vec3 u_lightWorldPos;
uniform mat4 u_world;
uniform mat4 u_viewInverse;
uniform mat4 u_worldInverseTranspose;
attribute vec4 a_position;
attribute vec3 a_normal;
attribute vec2 a_texcoord;
varying vec4 v_position;
varying vec2 v_texCoord;
varying vec3 v_normal;
varying vec3 v_surfaceToLight;
varying vec3 v_surfaceToView;
void main() {
v_texCoord = a_texcoord;
v_position = (u_worldViewProjection * a_position);
v_normal = (u_worldInverseTranspose * vec4(a_normal, 0)).xyz;
v_surfaceToLight = u_lightWorldPos - (u_world * a_position).xyz;
v_surfaceToView = (u_viewInverse[3] - (u_world * a_position)).xyz;
gl_Position = v_position;
}
</script>
<script id="fs" type="notjs">
precision mediump float;
varying vec4 v_position;
varying vec2 v_texCoord;
varying vec3 v_normal;
varying vec3 v_surfaceToLight;
varying vec3 v_surfaceToView;
uniform vec4 u_lightColor;
uniform vec4 u_diffuseMult;
uniform sampler2D u_diffuse;
uniform vec4 u_specular;
uniform float u_shininess;
uniform float u_specularFactor;
vec4 lit(float l ,float h, float m) {
return vec4(1.0,
abs(l),//max(l, 0.0),
(l > 0.0) ? pow(max(0.0, h), m) : 0.0,
1.0);
}
void main() {
vec4 diffuseColor = texture2D(u_diffuse, v_texCoord) * u_diffuseMult;
vec3 a_normal = normalize(v_normal);
vec3 surfaceToLight = normalize(v_surfaceToLight);
vec3 surfaceToView = normalize(v_surfaceToView);
vec3 halfVector = normalize(surfaceToLight + surfaceToView);
vec4 litR = lit(dot(a_normal, surfaceToLight),
dot(a_normal, halfVector), u_shininess);
vec4 outColor = vec4((
u_lightColor * (diffuseColor * litR.y +
u_specular * litR.z * u_specularFactor)).rgb,
diffuseColor.a);
gl_FragColor = outColor;
}
</script>