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
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'm rendering a variable number of circles in the plane with variable size, color, and position using instancing. I'm hoping to reach on the order of 10k-100k circles/labels.
in float instanceSize;
in vec3 instanceColor;
in vec2 instanceCenter;
The buffer backing the instanceCenter attribute changes every frame, animating the circles, but the rest is mostly static.
I have a quad per circle and I'm creating the circle in the fragment shader.
Now I'm looking into labeling the shapes with labels with font size proportional to circle size, centered on the circle, moving with the circles. From what I've read the most performant way to do so is to use a glyph texture with a quad for every letter using either a bitmap texture atlas or a signed distance field texture atlas. The examples I've seen seem to do a lot of work on the Javascript side and then use a draw call for every string like: https://webgl2fundamentals.org/webgl/lessons/webgl-text-glyphs.html
Is there a way to render the text with one draw call (with instancing, or otherwise?), while reusing the Float32Array backing instanceCenter every frame? It seems like more work would need to be done in the shaders but I'm not exactly sure how. Because each label has a variable number of glyphs I'm not sure how to associate a single instanceCenter with a single label.
All that aside, more basically I'm wondering how one centers text at a point?
Any help appreciated
Off the top of my head you could store your messages in a texture and add a message texcoord and length per instance. You can then compute the size of the rectangle needed to draw the message in the vertex shader and use that to center as well.
attribute float msgLength;
attribute vec2 msgTexCoord;
...
widthOfQuad = max(minSizeForCircle, msgLength * glphyWidth)
In the fragment shader read the message from the texture and use it look up glyphs (image based or SDF based).
varying vec2 v_msgTexCoord; // passed in from vertex shader
varying float v_msgLength; // passed in from vertex shader
varying vec2 uv; // uv that goes 0 to 1 across quad
float glyphIndex = texture2D(
messageTexture,
v_msgTexCoord + vec2(uv.x * v_msgLength / widthOfMessageTexture)).r;
// now convert glyphIndex to tex coords to look up glyph in glyph texture
glyphUV = (up to you)
textColor = texture2D(glyphTexture,
glyphUV + glyphSize * vec2(fract(uv.x * v_msgLength), uv.v) / glyphTextureSize);
Or something like that. I have no idea how slow it would be
async function main() {
const gl = document.querySelector('canvas').getContext('webgl');
twgl.addExtensionsToContext(gl);
function convertToGlyphIndex(c) {
c = c.toUpperCase();
if (c >= 'A' && c <= 'Z') {
return c.charCodeAt(0) - 0x41;
} else if (c >= '0' && c <= '9') {
return c.charCodeAt(0) - 0x30 + 26;
} else {
return 255;
}
}
const messages = [
'pinapple',
'grape',
'banana',
'strawberry',
];
const glyphImg = await loadImage("https://webglfundamentals.org/webgl/resources/8x8-font.png");
const glyphTex = twgl.createTexture(gl, {
src: glyphImg,
minMag: gl.NEAREST,
});
// being lazy about size, making them all the same.
const glyphsAcross = 8;
// too lazy to pack these in a texture in a more compact way
// so just put one message per row
const longestMsg = Math.max(...messages.map(m => m.length));
const messageData = new Uint8Array(longestMsg * messages.length * 4);
messages.forEach((message, row) => {
for (let i = 0; i < message.length; ++i) {
const c = convertToGlyphIndex(message[i]);
const offset = (row * longestMsg + i) * 4;
const u = c % glyphsAcross;
const v = c / glyphsAcross | 0;
messageData[offset + 0] = u;
messageData[offset + 1] = v;
}
});
const messageTex = twgl.createTexture(gl, {
src: messageData,
width: longestMsg,
height: messages.length,
minMag: gl.NEAREST,
});
const vs = `
attribute vec4 position; // a centered quad (-1 + 1)
attribute vec2 texcoord;
attribute float messageLength; // instanced
attribute vec4 center; // instanced
attribute vec2 messageUV; // instanced
uniform vec2 glyphDrawSize;
varying vec2 v_texcoord;
varying vec2 v_messageUV;
varying float v_messageLength;
void main() {
vec2 size = vec2(messageLength * glyphDrawSize.x, glyphDrawSize.y);
gl_Position = position * vec4(size, 1, 0) + center;
v_texcoord = texcoord;
v_messageUV = messageUV;
v_messageLength = messageLength;
}
`;
const fs = `
precision highp float;
varying vec2 v_texcoord;
varying vec2 v_messageUV;
varying float v_messageLength;
uniform sampler2D messageTex;
uniform vec2 messageTexSize;
uniform sampler2D glyphTex;
uniform vec2 glyphTexSize;
uniform vec2 glyphSize;
void main() {
vec2 msgUV = v_messageUV + vec2(v_texcoord.x * v_messageLength / messageTexSize.x, 0);
vec2 glyphOffset = texture2D(messageTex, msgUV).xy * 255.0;
vec2 glyphsAcrossDown = glyphTexSize / glyphSize;
vec2 glyphUVOffset = glyphOffset / glyphsAcrossDown;
vec2 glyphUV = fract(v_texcoord * vec2(v_messageLength, 1)) * glyphSize / glyphTexSize;
vec4 glyphColor = texture2D(glyphTex, glyphUVOffset + glyphUV);
// do some math here for a circle
// TBD
if (glyphColor.a < 0.1) discard;
gl_FragColor = glyphColor;
}
`;
const prgInfo = twgl.createProgramInfo(gl, [vs, fs]);
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: {
numComponents: 2,
data: [
-1, -1,
1, -1,
-1, 1,
-1, 1,
1, -1,
1, 1,
],
},
texcoord: [
0, 1,
1, 1,
0, 0,
0, 0,
1, 1,
1, 0,
],
center: {
numComponents: 2,
divisor: 1,
data: [
-0.4, 0.1,
-0.3, -0.5,
0.6, 0,
0.1, 0.5,
],
},
messageLength: {
numComponents: 1,
divisor: 1,
data: messages.map(m => m.length),
},
messageUV: {
numComponents: 2,
divisor: 1,
data: messages.map((m, i) => [0, i / messages.length]).flat(),
},
});
gl.clearColor(0, 0, 1, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(prgInfo.program);
twgl.setBuffersAndAttributes(gl, prgInfo, bufferInfo);
twgl.setUniformsAndBindTextures(prgInfo, {
glyphDrawSize: [16 / gl.canvas.width, 16 / gl.canvas.height],
messageTex,
messageTexSize: [longestMsg, messages.length],
glyphTex,
glyphTexSize: [glyphImg.width, glyphImg.height],
glyphSize: [8, 8],
});
// ext.drawArraysInstancedANGLE(gl.TRIANGLES, 0, 6, messages.length);
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, messages.length);
}
function loadImage(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = "anonymous";
img.onerror = reject;
img.onload = () => resolve(img);
img.src = url;
});
}
main();
<canvas></canvas>
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
note that if the glyphs were different sizes it seems like it would get extremely slow, at least off the top of my head, the only way to find each glyph as you draw a quad would be to loop over all the glyphs in the message for every pixel.
On the other hand, you could build a mesh of glyphs similar to the article, for each message, for every glyph in that message, add a per vertex message id or message uv that you use to look up offsets or matrices from a texture. In this way you can move every message independently but make it all happen in a single draw call. This would
allow non-monospaced glyphs. As an example of storing positions or matrices in a texture see this article on skinning. It stores bone matrices in a texture.
async function main() {
const gl = document.querySelector('canvas').getContext('webgl');
const ext = gl.getExtension('OES_texture_float');
if (!ext) {
alert('need OES_texture_float');
return;
}
twgl.addExtensionsToContext(gl);
function convertToGlyphIndex(c) {
c = c.toUpperCase();
if (c >= 'A' && c <= 'Z') {
return c.charCodeAt(0) - 0x41;
} else if (c >= '0' && c <= '9') {
return c.charCodeAt(0) - 0x30 + 26;
} else {
return 255;
}
}
const messages = [
'pinapple',
'grape',
'banana',
'strawberry',
];
const glyphImg = await loadImage("https://webglfundamentals.org/webgl/resources/8x8-font.png");
const glyphTex = twgl.createTexture(gl, {
src: glyphImg,
minMag: gl.NEAREST,
});
// being lazy about size, making them all the same.
const glyphsAcross = 8;
const glyphsDown = 5;
const glyphWidth = glyphImg.width / glyphsAcross;
const glyphHeight = glyphImg.height / glyphsDown;
const glyphUWidth = glyphWidth / glyphImg.width;
const glyphVHeight = glyphHeight / glyphImg.height;
// too lazy to pack these in a texture in a more compact way
// so just put one message per row
const positions = [];
const texcoords = [];
const messageIds = [];
const matrixData = new Float32Array(messages.length * 16);
const msgMatrices = [];
const quadPositions = [
-1, -1,
1, -1,
-1, 1,
-1, 1,
1, -1,
1, 1,
];
const quadTexcoords = [
0, 1,
1, 1,
0, 0,
0, 0,
1, 1,
1, 0,
];
messages.forEach((message, id) => {
msgMatrices.push(matrixData.subarray(id * 16, (id + 1) * 16));
for (let i = 0; i < message.length; ++i) {
const c = convertToGlyphIndex(message[i]);
const u = (c % glyphsAcross) * glyphUWidth;
const v = (c / glyphsAcross | 0) * glyphVHeight;
for (let j = 0; j < 6; ++j) {
const offset = j * 2;
positions.push(
quadPositions[offset ] * 0.5 + i - message.length / 2,
quadPositions[offset + 1] * 0.5,
);
texcoords.push(
u + quadTexcoords[offset ] * glyphUWidth,
v + quadTexcoords[offset + 1] * glyphVHeight,
);
messageIds.push(id);
}
}
});
const matrixTex = twgl.createTexture(gl, {
src: matrixData,
type: gl.FLOAT,
width: 4,
height: messages.length,
minMag: gl.NEAREST,
wrap: gl.CLAMP_TO_EDGE,
});
const vs = `
attribute vec4 position;
attribute vec2 texcoord;
attribute float messageId;
uniform sampler2D matrixTex;
uniform vec2 matrixTexSize;
uniform mat4 viewProjection;
varying vec2 v_texcoord;
void main() {
vec2 uv = (vec2(0, messageId) + 0.5) / matrixTexSize;
mat4 model = mat4(
texture2D(matrixTex, uv),
texture2D(matrixTex, uv + vec2(1.0 / matrixTexSize.x, 0)),
texture2D(matrixTex, uv + vec2(2.0 / matrixTexSize.x, 0)),
texture2D(matrixTex, uv + vec2(3.0 / matrixTexSize.x, 0)));
gl_Position = viewProjection * model * position;
v_texcoord = texcoord;
}
`;
const fs = `
precision highp float;
varying vec2 v_texcoord;
uniform sampler2D glyphTex;
void main() {
vec4 glyphColor = texture2D(glyphTex, v_texcoord);
// do some math here for a circle
// TBD
if (glyphColor.a < 0.1) discard;
gl_FragColor = glyphColor;
}
`;
const prgInfo = twgl.createProgramInfo(gl, [vs, fs]);
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: {
numComponents: 2,
data: positions,
},
texcoord: texcoords,
messageId: {
numComponents: 1,
data: messageIds
},
});
gl.clearColor(0, 0, 1, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(prgInfo.program);
const m4 = twgl.m4;
const viewProjection = m4.ortho(0, gl.canvas.width, 0, gl.canvas.height, -1, 1);
msgMatrices.forEach((mat, i) => {
m4.translation([80 + i * 30, 30 + i * 25, 0], mat);
m4.scale(mat, [16, 16, 1], mat)
});
// update the matrices
gl.bindTexture(gl.TEXTURE_2D, matrixTex);
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, 4, messages.length, gl.RGBA, gl.FLOAT, matrixData);
twgl.setBuffersAndAttributes(gl, prgInfo, bufferInfo);
twgl.setUniformsAndBindTextures(prgInfo, {
viewProjection,
matrixTex,
matrixTexSize: [4, messages.length],
glyphTex,
});
gl.drawArrays(gl.TRIANGLES, 0, positions.length / 2);
}
function loadImage(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = "anonymous";
img.onerror = reject;
img.onload = () => resolve(img);
img.src = url;
});
}
main();
<canvas></canvas>
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
Also see https://stackoverflow.com/a/54720138/128511
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 have been working on a voxel engine using webgl. It uses gl.points to draw voxels using a square based on your distance to the point.
Here is the basics of how it work
Vertex:
//Get position using the projection matrix and block XYZ
gl_Position = uMatrix * uModelMatrix * vec4(aPixelPosition[0],-aPixelPosition[2],aPixelPosition[1],1.0);
//Set size of point based on screen height and divide it by depth to size based on distance
gl_PointSize = (uScreenSize[1]) / gl_Position[2];
And here is how that looks when it is from a non-problematic angle.
You can see, it looks just how I want to (of course its not as good as real cubes, but preforms amazing on mobile) now lets go inside of this hollow cube and see what it looks like. This picture is me looking into the corner
I changed the background color to highlight the issue. Basically if you are looking directly at the blocks, they work fine, but if they are at an angle to you, they are too small and leave large gaps. This picture is me looking at a wall directly
You can see facing the back wall works perfect, but all the other walls look bad.
So clearly I am doing something wrong, or not thinking about something properly. I have tried a lot of different things to try and repair it but none of my fixes work proper.
I have tried making it so blocks towards the edge of the screen are bigger, this fixes the problem but it also makes blocks bigger that don't need to be. Like for example looking at a flat wall, the edges would become much bigger even though looking at a flat wall doesn't have the issue.
I have also tried making the squares much bigger and this fixes it but then they overlap everywhere and it doesn't look nearly as clean.
You can see the example of the problem here (just takes a second to generate the structure)
https://sebastian97.itch.io/voxel-glitchy
WASD- movement
Arrow keys / mouse - Look
Assuming you have your projection matrix separated out I think you want gl_PointSize to be
vec4 modelViewPosition = view * model * position;
gl_PointSize = someSize / -modelViewPosition.z;
gl_Position = projection * modelViewPosition;
'use strict';
/* global window, twgl, requestAnimationFrame, document */
const height = 120;
const width = 30
const position = [];
const color = [];
const normal = [];
for (let z = 0; z < width; ++z) {
for (let x = 0; x < width; ++x) {
position.push(x, 0, z);
color.push(r(0.5), 1, r(0.5));
normal.push(0, 1, 0);
}
}
for (let y = 1; y < height ; ++y) {
for (let x = 0; x < width; ++x) {
position.push(x, -y, 0);
color.push(0.6, 0.6, r(0.5));
normal.push(0, 0, -1);
position.push(x, -y, width - 1);
color.push(0.6, 0.6, r(0.5));
normal.push(0, 0, 1);
position.push(0, -y, x);
color.push(0.6, 0.6, r(0.5));
normal.push(-1, 0, 0);
position.push(width - 1, -y, x);
color.push(0.6, 0.6, r(0.5));
normal.push(1, 0, 0);
}
}
function r(min, max) {
if (max === undefined) {
max = min;
min = 0;
}
return Math.random() * (max - min) + min;
}
const m4 = twgl.m4;
const v3 = twgl.v3;
const gl = document.querySelector('canvas').getContext('webgl');
const vs = `
attribute vec4 position;
attribute vec3 normal;
attribute vec3 color;
uniform mat4 projection;
uniform mat4 modelView;
varying vec3 v_normal;
varying vec3 v_color;
void main() {
vec4 modelViewPosition = modelView * position;
gl_Position = projection * modelViewPosition;
gl_PointSize = 850.0 / -modelViewPosition.z;
v_normal = mat3(modelView) * normal;
v_color = color;
}
`;
const fs = `
precision highp float;
varying vec3 v_normal;
varying vec3 v_color;
void main() {
vec3 lightDirection = normalize(vec3(1, 2, 3)); // arbitrary light direction
float l = dot(lightDirection, normalize(v_normal)) * .5 + .5;
gl_FragColor = vec4(v_color * l, 1);
}
`;
// compile shader, link, look up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
// make some vertex data
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
position,
normal,
color: { numComponents: 3, data: color },
});
const keys = [];
const eye = [10, 10, 55];
const target = [0, 0, 0];
const up = [0, 1, 0];
const speed = 50;
const kUp = 38;
const kDown = 40;
const kLeft = 37;
const kRight = 39;
const kForward = 87;
const kBackward = 83;
const kSlideLeft = 65;
const kSlideRight = 68;
const keyMove = new Map();
keyMove.set(kForward, { ndx: 8, eye: 1, target: -1 });
keyMove.set(kBackward, { ndx: 8, eye: 1, target: 1 });
keyMove.set(kSlideLeft, { ndx: 0, eye: 1, target: -1 });
keyMove.set(kSlideRight, { ndx: 0, eye: 1, target: 1 });
keyMove.set(kLeft, { ndx: 0, eye: 0, target: -1 });
keyMove.set(kRight, { ndx: 0, eye: 0, target: 1 });
keyMove.set(kUp, { ndx: 4, eye: 0, target: -1 });
keyMove.set(kDown, { ndx: 4, eye: 0, target: 1 });
let then = 0;
function render(time) {
time *= 0.001; // seconds
const deltaTime = 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);
const fov = Math.PI * 0.25;
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const near = 0.1;
const far = 1000;
const projection = m4.perspective(fov, aspect, near, far);
const camera = m4.lookAt(eye, target, up);
const view = m4.inverse(camera);
const modelView = m4.translate(view, [width / -2, 0, width / -2]);
keyMove.forEach((move, key) => {
if (keys[key]) {
const dir = camera.slice(move.ndx, move.ndx + 3);
const delta = v3.mulScalar(dir, deltaTime * speed * move.target);
v3.add(target, delta, target);
if (move.eye) {
v3.add(eye, delta, eye);
}
}
});
gl.useProgram(programInfo.program);
// calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
// calls gl.activeTexture, gl.bindTexture, gl.uniformXXX
twgl.setUniforms(programInfo, {
projection,
modelView,
});
// calls gl.drawArrays or gl.drawElements
twgl.drawBufferInfo(gl, bufferInfo, gl.POINTS);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
window.addEventListener('keydown', (e) => {
e.preventDefault();
keys[e.keyCode] = true;
});
window.addEventListener('keyup', (e) => {
keys[e.keyCode] = false;
});
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
#i { position: absolute; top: 0; left: 5px; font-family: monospace; }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas></canvas>
<div id="i">ASWD ⬆️⬇️⬅️➡️</div>
I'm using WebGL 1.0. I drew a circle to the stencil buffer, and now I want to use this stencil buffer multiple times without clearing it. The first time I use it, I enable stencil testing with:
gl.enable(GL.STENCIL_TEST);
Then, I perform my drawing to the color buffer. After this, at some later date, I want to draw again, but this time I want to clip to the inverse of what is in the stencil buffer. I know that I can draw to the stencil buffer again, but since I didn't use gl.stencilOp(GL.ZERO, GL.ZERO, GL.ZERO), the stencil buffer should still be around, but with the original values in it.
My question is - is there a quick WebGL way to invert this stencil buffer, or do I have to perform the drawing operations again with the stencil operation of GL.INVERT?
Assuming you clear the stencil to one value and draw to the stencil with a different value then you can use gl.stencilFunc(gl.EQUAL, value, 0xFFFF) to
draw only where the stencil matches value.
Example:
The code below the stencil with a circle of 1s and a square of 2s. It then draws 3 scenes. A scene with cubes only where the stencil is 0, a scene with spheres only where the stencil is 1, and a scene with flying through a ring of toruses only where the stencil is 2
const m4 = twgl.m4;
const v3 = twgl.v3;
const gl = document.querySelector("canvas").getContext("webgl", {
stencil: true,
});
const programInfo = makeProgramInfo(gl);
const renderStencil1 = setupSceneStencil1();
const renderStencil2 = setupSceneStencil2();
const renderScene1 = setupScene1();
const renderScene2 = setupScene2();
const renderScene3 = setupScene3();
function render(time) {
time *= 0.001;
twgl.resizeCanvasToDisplaySize(gl.canvas);
gl.disable(gl.STENCIL_TEST);
gl.clearStencil(0);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clearColor(1, 1, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
// draw 1s into stencil
gl.enable(gl.STENCIL_TEST);
gl.stencilFunc(gl.ALWAYS, 1, 0xFF);
if (time / 5 % 2 | 0) {
// this will end up with a 2s where the square overlaps the circle
gl.stencilOp(gl.INCR, gl.INCR, gl.INCR);
} else {
// this will end up with a 2s where the square is drawn
gl.stencilOp(gl.REPLACE, gl.REPLACE, gl.REPLACE);
}
gl.disable(gl.DEPTH_TEST);
renderStencil1(time);
// draw 2s into stencil
gl.stencilFunc(gl.ALWAYS, 2, 0xFF);
gl.disable(gl.DEPTH_TEST);
renderStencil2(time);
// draw where there are 0s
gl.enable(gl.DEPTH_TEST);
gl.stencilFunc(gl.EQUAL, 0, 0xFF);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
renderScene1(time);
// draw where there are 1s
gl.stencilFunc(gl.EQUAL, 1, 0xFF);
renderScene2(time);
// draw where there are 2s
gl.stencilFunc(gl.EQUAL, 2, 0xFF);
renderScene3(time);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
function setupSceneStencil1() {
const bufferInfo = twgl.primitives.createDiscBufferInfo(gl, 1, 48);
const color = [1, 0, 0, 1];
const tex = twgl.createTexture(gl, {
src: [255, 255, 255, 255],
});
function render(time, viewProjection) {
gl.useProgram(programInfo.program);
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
const s = 1 + (Math.sin(time) * .5 + .5) * 10;
let mat = m4.copy(viewProjection);
mat = m4.translate(mat, [
Math.sin(time * 1.7) * 3,
0,
0,
]);
mat = m4.scale(mat, [s, s, s]);
mat = m4.rotateX(mat, Math.PI * .5);
twgl.setUniforms(programInfo, {
u_diffuse: tex,
u_diffuseMult: color,
u_worldViewProjection: mat,
});
twgl.drawBufferInfo(gl, bufferInfo);
}
return setupScene(render);
}
function setupSceneStencil2() {
const bufferInfo = twgl.primitives.createPlaneBufferInfo(gl, 2, 2);
const color = [0, 0, 1, 1];
const tex = twgl.createTexture(gl, {
src: [255, 255, 255, 255],
});
function render(time, viewProjection) {
gl.useProgram(programInfo.program);
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
const s = 1 + (Math.cos(time * 2.3) * .5 + .5) * 5;
let mat = m4.copy(viewProjection);
mat = m4.translate(mat, [
Math.cos(time * 1.3) * 3,
0,
0,
]);
mat = m4.scale(mat, [s, s, s]);
mat = m4.rotateZ(mat, -time);
mat = m4.rotateX(mat, Math.PI * .5);
twgl.setUniforms(programInfo, {
u_diffuse: tex,
u_diffuseMult: color,
u_worldViewProjection: mat,
});
twgl.drawBufferInfo(gl, bufferInfo);
}
return setupScene(render);
}
function setupScene1() {
const bufferInfo = twgl.primitives.createCubeBufferInfo(gl, 4);
const color = makeColor();
function render(time, viewProjection, tex) {
gl.useProgram(programInfo.program);
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
const numCubes = 20;
for (let i = 0; i < numCubes; ++i) {
const u = i / (numCubes - 1);
const uu = u * 2 - 1;
let mat = m4.copy(viewProjection);
mat = m4.translate(mat, [
uu * 15 + Math.sin(time),
((u * 8 + time) % 2 - 1) * 15,
0,
]);
mat = m4.rotateY(mat, u + time);
mat = m4.rotateX(mat, u + time);
twgl.setUniforms(programInfo, {
u_diffuse: tex,
u_diffuseMult: color,
u_worldViewProjection: mat,
});
twgl.drawBufferInfo(gl, bufferInfo);
}
}
return setupScene(render);
}
function setupScene2() {
const bufferInfo = twgl.primitives.createSphereBufferInfo(gl, 1, 24, 12);
const color = makeColor();
// adapted from http://stackoverflow.com/a/26127012/128511
// used to space the cubes around the sphere
function fibonacciSphere(samples, i) {
const rnd = 1.;
const offset = 2. / samples;
const increment = Math.PI * (3. - Math.sqrt(5.));
// for i in range(samples):
const y = ((i * offset) - 1.) + (offset / 2.);
const r = Math.sqrt(1. - Math.pow(y ,2.));
const phi = ((i + rnd) % samples) * increment;
const x = Math.cos(phi) * r;
const z = Math.sin(phi) * r;
return [x, y, z];
}
function render(time, viewProjection, tex) {
gl.useProgram(programInfo.program);
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
const numSpheres = 100;
for (let i = 0; i < numSpheres; ++i) {
const u = i / (numSpheres - 1);
const uu = u * 2 - 1;
let mat = m4.copy(viewProjection);
mat = m4.rotateY(mat, time);
mat = m4.rotateZ(mat, time);
mat = m4.translate(mat, v3.mulScalar(fibonacciSphere(numSpheres, i), 8));
mat = m4.rotateX(mat, u + time);
twgl.setUniforms(programInfo, {
u_diffuse: tex,
u_diffuseMult: color,
u_worldViewProjection: mat,
});
twgl.drawBufferInfo(gl, bufferInfo);
}
}
return setupScene(render);
}
function setupScene3() {
const bufferInfo = twgl.primitives.createTorusBufferInfo(gl, 2, 0.4, 24, 12);
const color = makeColor();
function render(time, viewProjection, tex) {
gl.useProgram(programInfo.program);
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
const numSpheres = 100;
for (let i = 0; i < numSpheres; ++i) {
const u = i / (numSpheres - 1);
const uu = u * 2 - 1;
let mat = m4.copy(viewProjection);
mat = m4.rotateZ(mat, time);
mat = m4.translate(mat, [0, 40, -20]);
mat = m4.rotateX(mat, time + u * Math.PI * 2);
mat = m4.translate(mat, [0, 40, 0]);
mat = m4.rotateX(mat, Math.PI * .5);
mat = m4.rotateY(mat, u * Math.PI * 20);
twgl.setUniforms(programInfo, {
u_diffuse: tex,
u_diffuseMult: color,
u_worldViewProjection: mat,
});
twgl.drawBufferInfo(gl, bufferInfo);
}
}
return setupScene(render);
}
function setupScene(renderFn) {
const camera = m4.identity();
const view = m4.identity();
const viewProjection = m4.identity();
const tex = twgl.createTexture(gl, {
min: gl.NEAREST,
mag: gl.NEAREST,
format: gl.LUMINANCE,
src: [
255, 192, 255, 192,
192, 255, 192, 255,
255, 192, 255, 192,
192, 255, 192, 255,
],
});
return function render(time) {
const projection = m4.perspective(
30 * Math.PI / 180,
gl.canvas.clientWidth / gl.canvas.clientHeight,
0.5,
100);
const eye = [0, 0, -20];
const target = [0, 0, 0];
const up = [0, 1, 0];
m4.lookAt(eye, target, up, camera);
m4.inverse(camera, view);
m4.multiply(projection, view, viewProjection);
renderFn(time, viewProjection, tex);
}
}
function rand(min, max) {
if (max === undefined) {
max = min;
min = 0;
}
return min + Math.random() * (max - min);
}
function makeProgramInfo(gl) {
const vs = `
uniform mat4 u_worldViewProjection;
attribute vec4 position;
attribute vec2 texcoord;
varying vec2 v_texcoord;
void main() {
v_texcoord = texcoord;
gl_Position = u_worldViewProjection * position;
}
`;
const fs = `
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D u_diffuse;
uniform vec4 u_diffuseMult;
void main() {
gl_FragColor = texture2D(u_diffuse, v_texcoord) * u_diffuseMult;
}
`;
return twgl.createProgramInfo(gl, [vs, fs]);
}
function makeColor() {
const color = [rand(1), rand(1), rand(1), 1];
color[rand(3) | 0] = .8;
return color;
}
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas></canvas>
Of course there's also LESS, LEQUAL, GREATER, GEQUAL, NOTEQUAL, and of course NEVER, and ALWAYS as other possibilities for stenciling the opposite.