webgl tilemap rendering incorrect UV calculation - javascript

I'm trying to render a tile map onto a single quad.
My approach uses a "tile map" texture in which each pixel stores the X and Y indices of a tile inside a tile set.
When rendering a fragment, the idea is to:
Sample the "tile map" texture using vertex texture coordinates
Retrieve the X and Y indices from the R and G channels of the texture
Calculate the UVs of the selected tile
Use the UVs to sample the texture atlas
I'm having issues with getting #3 to work.
Here is the shader code I'm trying to use to render this:
vertex
#version 300 es
precision mediump float;
uniform mat4 uVIEW;
uniform mat4 uPROJECTION;
uniform mat3 uMODEL;
layout(location = 0) in vec2 aPOSITION;
layout(location = 1) in vec2 aTEXCOORD;
out vec2 vTEXCOORD;
void main()
{
// flip uv and pass it to fragment shader
vTEXCOORD = vec2(aTEXCOORD.x, 1.0f - aTEXCOORD.y);
// transform vertex position
vec3 transformed = uMODEL * vec3(aPOSITION, 1.0);
gl_Position = uPROJECTION * uVIEW * vec4(transformed.xy, 0.0, 1.0);
}
fragment
#version 300 es
precision mediump float;
precision mediump usampler2D;
uniform usampler2D uMAP;
uniform sampler2D uATLAS;
uniform vec2 uATLAS_SIZE;
in vec2 vTEXCOORD;
out vec4 oFRAG;
void main()
{
// sample "tile map" texture
vec4 data = vec4(texture(uMAP, vTEXCOORD));
// calculate UV
vec2 uv = (data.xy * 32.0 / uATLAS_SIZE) + (vTEXCOORD * 32.0 / uATLAS_SIZE);
// sample the tileset
oFRAG = texture(uATLAS, uv);
}
I believe this is the culprit:
vec2 uv = (data.xy * 32.0 / uATLAS_SIZE) + (vTEXCOORD * 32.0 / uATLAS_SIZE);
The formula here is uv = (tile_xy_indices * tile_size) + (texcoord * tile_size), where:
texcoord is the vertex uv (the standard [0, 1], [0, 0], [1, 0], [1, 1])
tile_xy_indices are the X,Y coordinates of the tile in the tileset
tile_size is the normalized size of one tile in the tileset
So if I have the values texcoord = (0, 0), tile_xy_indices = (7, 7), tile_size = (32 / 1024, 32 / 1024), then the UV for this fragment should be (0.21875, 0.21875), and if texcoord = (1, 1), then it should be (0.25, 0.25). These values seem correct to me, why do they produce the wrong result, and how do I fix it?
Here is some extra context:
Tile map texture (exaggerated colors):
Expected result (minus the grid lines):
Actual result:

The code is conflating 3 things in these 2 lines
// sample "tile map" texture
vec4 data = vec4(texture(uMAP, vTEXCOORD));
// calculate UV
vec2 uv = (data.xy * 32.0 / uATLAS_SIZE) + (vTEXCOORD * 32.0 / uATLAS_SIZE);
// sample the tileset
The first one, for the entire quad you're drawing, goes over your entire tilemap. That's probably not what you want. Usually apps that use a tilemap want to show a portion of it, not the entire thing.
The second problem is the second line needs to know how many pixels a tile will cover, not how many pixels a tile is. In other words if you have a 32x32 tile and you draw it in 4x4 pixels then your texture coordinate need to go from 0.0 to 1.0 across that tile in 4 pixels, not in 32 pixels.
A 3rd problem is dividing by 32 will not go across a 32pixel tile unless there are 32 tiles across the texture. Imagine you have a single tile is 32x32 pixels but there 8x4 tiles in your tile set. You need go from 0 to 1 across 1/8th and 1/4th not across 1/32
this answer implements a tilemap
Effectively it uses 2 matrices. One to draw the quad which being a quad could be rotated, scaled, projected in 3D etc, but lets just say the normal thing for a tilemap would be to just draw a quad that covers the canvas.
The second is a texture matrix (or tile matrix) where each unit is 1 tile. So given, a 0 to 1 quad you compute a matrix to expand and rotate that quad on to the quad above.
Let's say you don't rotate, you still need to decide how many tiles to draw across and down the quad. If you wanted 4 tiles across the quad and 3 tiles down then you you'd set the scale to x=4 and y=3.
This way, automatically, every tile goes from 0 to 1 in its own space. Or maybe to put it another way, tile 2x7 goes from 2.0<->3.0 in U and 7.0<->8.0 in V. We can then look up from the map tile 2,7 and use fract to cover that tile in the space that tile occupies in the quad.
const vs = `#version 300 es
precision mediump float;
uniform mat4 uVIEW;
uniform mat4 uPROJECTION;
uniform mat4 uMODEL;
uniform mat4 uTEXMATRIX;
layout(location = 0) in vec4 aPOSITION;
layout(location = 1) in vec4 aTEXCOORD;
out vec2 vTEXCOORD;
void main()
{
vTEXCOORD = (uTEXMATRIX * aTEXCOORD).xy;
gl_Position = uPROJECTION * uVIEW * uMODEL * aPOSITION;
}
`;
const fs = `#version 300 es
precision mediump float;
precision mediump usampler2D;
uniform usampler2D uMAP;
uniform sampler2D uATLAS;
uniform vec2 uTILESET_SIZE; // how many tiles across and down the tileset
in vec2 vTEXCOORD;
out vec4 oFRAG;
void main()
{
// the integer portion of vTEXCOORD is the tilemap coord
ivec2 mapCoord = ivec2(vTEXCOORD);
uvec4 data = texelFetch(uMAP, mapCoord, 0);
// the fractional portion of vTEXCOORD is the UV across the tile
vec2 texcoord = fract(vTEXCOORD);
vec2 uv = (vec2(data.xy) + texcoord) / uTILESET_SIZE;
// sample the tileset
oFRAG = texture(uATLAS, uv);
}
`;
const tileWidth = 32;
const tileHeight = 32;
const tilesAcross = 8;
const tilesDown = 4;
const m4 = twgl.m4;
const gl = document.querySelector('canvas').getContext('webgl2');
if (!gl) alert('need WebGL2');
// compile shaders, link, look up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
// gl.createBuffer, bindBuffer, bufferData
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
aPOSITION: {
numComponents: 2,
data: [
0, 0,
1, 0,
0, 1,
0, 1,
1, 0,
1, 1,
],
},
aTEXCOORD: {
numComponents: 2,
data: [
0, 0,
1, 0,
0, 1,
0, 1,
1, 0,
1, 1,
],
},
});
function r(min, max) {
if (max === undefined) {
max = min;
min = 0;
}
return min + (max - min) * Math.random();
}
// make some tiles
const ctx = document.createElement('canvas').getContext('2d');
ctx.canvas.width = tileWidth * tilesAcross;
ctx.canvas.height = tileHeight * tilesDown;
ctx.font = "bold 24px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const f = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ~';
for (let y = 0; y < tilesDown; ++y) {
for (let x = 0; x < tilesAcross; ++x) {
const color = `hsl(${r(360) | 0},${r(50,100)}%,50%)`;
ctx.fillStyle = color;
const tx = x * tileWidth;
const ty = y * tileHeight;
ctx.fillRect(tx, ty, tileWidth, tileHeight);
ctx.fillStyle = "#FFF";
ctx.fillText(f.substr(y * 8 + x, 1), tx + tileWidth * .5, ty + tileHeight * .5);
}
}
document.body.appendChild(ctx.canvas);
const tileTexture = twgl.createTexture(gl, {
src: ctx.canvas,
minMag: gl.NEAREST,
});
// make a tilemap
const mapWidth = 400;
const mapHeight = 300;
const tilemap = new Uint32Array(mapWidth * mapHeight);
const tilemapU8 = new Uint8Array(tilemap.buffer);
const totalTiles = tilesAcross * tilesDown;
for (let i = 0; i < tilemap.length; ++i) {
const off = i * 4;
// mostly tile 9
const tileId = r(4) < 1
? (r(totalTiles) | 0)
: 9;
tilemapU8[off + 0] = tileId % tilesAcross;
tilemapU8[off + 1] = tileId / tilesAcross | 0;
}
const mapTexture = twgl.createTexture(gl, {
internalFormat: gl.RGBA8UI,
src: tilemapU8,
width: mapWidth,
minMag: gl.NEAREST,
});
function ease(t) {
return Math.cos(t) * .5 + .5;
}
function lerp(a, b, t) {
return a + (b - a) * t;
}
function easeLerp(a, b, t) {
return lerp(a, b, ease(t));
}
function render(time) {
time *= 0.001; // convert to seconds;
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clearColor(0, 1, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(programInfo.program);
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
// these mats affects where the quad is drawn
const projection = m4.ortho(0, gl.canvas.width, gl.canvas.height, 0, -1, 1);
const view = m4.identity();
const model =
m4.scaling([gl.canvas.width, gl.canvas.height, 1]);
const tilesAcrossQuad = 10;//easeLerp(.5, 2, time * 1.1);
const tilesDownQuad = 5;//easeLerp(.5, 2, time * 1.1);
// scroll position in tiles
// set this to 0,0 and the top left corner of the quad
// will be the start of the map.
const scrollX = time % mapWidth;
const scrollY = 0;//time % (mapHeight * tileHeight);
const tmat = m4.identity();
// sets where in the map to look at in tile coordinates
// so 3,4 means start drawing 3 tiles over, 4 tiles down
m4.translate(tmat, [scrollX, scrollY, 0], tmat);
// sets how many tiles to display
m4.scale(tmat, [tilesAcrossQuad, tilesDownQuad, 1], tmat);
twgl.setUniforms(programInfo, {
uPROJECTION: projection,
uVIEW: view,
uMODEL: model,
uTEXMATRIX: tmat,
uMAP: mapTexture,
uATLAS: tileTexture,
uTILESET_SIZE: [tilesAcross, tilesDown],
});
gl.drawArrays(gl.TRIANGLES, 0, 6);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
canvas { border: 1px solid black; }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas></canvas>
next time post a working snippet is so much friendlier to answerers.

Related

WebGL Stencils, How to use a 2D sprite's transparency as a mask?

if (statuseffect) {
// Clearing the stencil buffer
gl.clearStencil(0);
gl.clear(gl.STENCIL_BUFFER_BIT);
gl.stencilFunc(gl.ALWAYS, 1, 1);
gl.stencilOp(gl.REPLACE, gl.REPLACE, gl.REPLACE);
gl.colorMask(false, false, false, false);
gl.enable(gl.STENCIL_TEST);
// Renders the mask through gl.drawArrays L111
drawImage(statuseffectmask.texture, lerp(-725, 675, this.Transtion_Value), 280, 128 * 4, 32 * 4)
// Telling the stencil now to draw/keep only pixels that equals 1 - which we set earlier
gl.stencilFunc(gl.EQUAL, 1, 1);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
// enabling back the color buffer
gl.colorMask(true, true, true, true);
drawImage(statuseffect.texture, lerp(-725, 675, this.Transtion_Value), 280, 128 * 4, 32 * 4)
gl.disable(gl.STENCIL_TEST);
}
Im trying to get something to work like this
Where it gets the transparency of the sprite, and then draws a sprite in areas where there is no transparency, thank you.
It's not clear why you want to use the stencil for this. Normally you'd just setup blending and use the transparency to blend.
If you really wanted to use the stencil you'd need to make a shader that calls discard if the transparency (alpha) is less then some value in order to make the stencil get set only where the sprite is not transparent
precision highp float;
varying vec2 v_texcoord;
uniform sampler2D u_texture;
uniform float u_alphaTest;
void main() {
vec4 color = texture2D(u_texture, v_texcoord);
if (color.a < u_alphaTest) {
discard; // don't draw this pixel
}
gl_FragColor = color;
}
But the thing is that would already draw the texture transparently without using the stencil.
const m4 = twgl.m4;
const gl = document.querySelector('canvas').getContext('webgl');
const vs = `
attribute vec4 position;
attribute vec2 texcoord;
uniform mat4 u_matrix;
varying vec2 v_texcoord;
void main() {
gl_Position = u_matrix * position;
v_texcoord = texcoord;
}
`;
const fs = `
precision highp float;
varying vec2 v_texcoord;
uniform sampler2D u_texture;
uniform float u_alphaTest;
void main() {
vec4 color = texture2D(u_texture, v_texcoord);
if (color.a < u_alphaTest) {
discard; // don't draw this pixel
}
gl_FragColor = color;
}
`;
// compile shaders, link program, look up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
// make buffers for positions and texcoords for a plane
// calls gl.createBuffer, gl.bindBuffer, gl.bufferData
const bufferInfo = twgl.primitives.createXYQuadBufferInfo(gl);
const texture = makeSpriteTexture(gl, '🌲', 128);
function render(time) {
time *= 0.001; // convert to seconds
gl.useProgram(programInfo.program);
// calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
const mat = m4.ortho(-150, 150, 75, -75, -1, 1);
m4.translate(mat, [Math.cos(time) * 20, Math.sin(time) * 20, 0], mat);
m4.scale(mat, [64, 64, 1], mat);
// calls gl.activeTexture, gl.bindTexture, gl.uniformXXX
twgl.setUniformsAndBindTextures(programInfo, {
u_texture: texture,
u_alphaTest: 0.5,
u_matrix: mat,
});
// calls gl.drawArrays or gl.drawElements
twgl.drawBufferInfo(gl, bufferInfo);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
// just so we don't have to download an image
// we'll make our own using a canvas and the 2D API
function makeSpriteTexture(gl, str, size) {
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
ctx.font = `${size * 3 / 4}px sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(str, size / 2, size / 2);
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(
gl.TEXTURE_2D,
0, // mip level
gl.RGBA, // internal format
gl.RGBA, // format
gl.UNSIGNED_BYTE, // type,
canvas);
// let's assume we used power of 2 dimensions
gl.generateMipmap(gl.TEXTURE_2D);
return tex;
}
canvas {
background: url(https://i.imgur.com/v38pV.jpg) no-repeat center center;
background-size: cover;
}
<canvas></canvas>
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
Otherwise if you really want to use the stencil now that the code is discarding some pixels it should work and your code was correct. note the code below doesn't clear the stencil because it defaults to being cleared every frame
const m4 = twgl.m4;
const gl = document.querySelector('canvas').getContext('webgl', {stencil: true});
const vs = `
attribute vec4 position;
attribute vec2 texcoord;
uniform mat4 u_matrix;
varying vec2 v_texcoord;
void main() {
gl_Position = u_matrix * position;
v_texcoord = texcoord;
}
`;
const fs = `
precision highp float;
varying vec2 v_texcoord;
uniform sampler2D u_texture;
uniform float u_alphaTest;
void main() {
vec4 color = texture2D(u_texture, v_texcoord);
if (color.a < u_alphaTest) {
discard; // don't draw this pixel
}
gl_FragColor = color;
}
`;
// compile shaders, link program, look up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
// make buffers for positions and texcoords for a plane
// calls gl.createBuffer, gl.bindBuffer, gl.bufferData
const bufferInfo = twgl.primitives.createXYQuadBufferInfo(gl);
const starTexture = makeSpriteTexture(gl, '✱', 128);
const bugTexture = makeSpriteTexture(gl, '🐞', 128);
function render(time) {
time *= 0.001; // convert to seconds
gl.useProgram(programInfo.program);
// calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
const mat = m4.ortho(-150, 150, 75, -75, -1, 1);
m4.translate(mat, [Math.cos(time) * 20, 0, 0], mat);
m4.scale(mat, [64, 64, 1], mat);
gl.enable(gl.STENCIL_TEST);
// set the stencil to 1 everwhere we draw a pixel
gl.stencilFunc(gl.ALWAYS, 1, 0xFF);
gl.stencilOp(gl.REPLACE, gl.REPLACE, gl.REPLACE);
// don't render pixels
gl.colorMask(false, false, false, false);
// calls gl.activeTexture, gl.bindTexture, gl.uniformXXX
twgl.setUniformsAndBindTextures(programInfo, {
u_texture: starTexture,
u_alphaTest: 0.5,
u_matrix: mat,
});
// calls gl.drawArrays or gl.drawElements
twgl.drawBufferInfo(gl, bufferInfo);
// only draw if the stencil = 1
gl.stencilFunc(gl.EQUAL, 1, 0xFF);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
// render pixels
gl.colorMask(true, true, true, true);
m4.ortho(-150, 150, 75, -75, -1, 1, mat);
m4.translate(mat, [0, Math.cos(time * 1.1) * 20, 0], mat);
m4.scale(mat, [64, 64, 1], mat);
// calls gl.activeTexture, gl.bindTexture, gl.uniformXXX
twgl.setUniformsAndBindTextures(programInfo, {
u_texture: bugTexture,
u_alphaTest: 0, // draw all pixels (but stencil will prevent some)
u_matrix: mat,
});
// calls gl.drawArrays or gl.drawElements
twgl.drawBufferInfo(gl, bufferInfo);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
// just so we don't have to download an image
// we'll make our own using a canvas and the 2D API
function makeSpriteTexture(gl, str, size) {
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
ctx.font = `${size * 3 / 4}px sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(str, size / 2, size / 2);
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(
gl.TEXTURE_2D,
0, // mip level
gl.RGBA, // internal format
gl.RGBA, // format
gl.UNSIGNED_BYTE, // type,
canvas);
// let's assume we used power of 2 dimensions
gl.generateMipmap(gl.TEXTURE_2D);
return tex;
}
canvas {
background: url(https://i.imgur.com/v38pV.jpg) no-repeat center center;
background-size: cover;
}
<canvas></canvas>
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
Let me also point out that that this is also probably better done using alpha blending, passing both textures into a single shader and passing in another matrix or other uniforms to apply one texture's alpha ot the other. This would be more flexible as you could can blend across all values of 0 to 1 where as with the stencil you can only mask 0 or 1 period.
My point isn't to say "don't use the stencil" but rather that there are times where it's best and times where it's not. Only you can know for your situation which solution to choose.

Make light independent from the view in a Phong model

I'm trying to implement the Phong shading model, but I come across something quite strange. When I change the viewing position, it looks like the light behaves differently, as if it was dependent from the view. Like, if I'm close to the object I only see the effects of the ambient light, while if I go far away from it I start seeing the diffuse's contribution.
These are my shaders:
//Vertex Shader
attribute vec4 vPosition;
attribute vec4 vNormal;
varying vec3 N, L, E;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform vec4 lightPosition;
void main()
{
vec3 pos = -(modelViewMatrix * vPosition).xyz;
vec3 light = lightPosition.xyz;
L = normalize(light - pos);
E = -pos;
N = normalize((modelViewMatrix * vNormal).xyz);
gl_Position = projectionMatrix * modelViewMatrix * vPosition;
}
//Fragment Shader
uniform vec4 ambientProduct;
uniform vec4 diffuseProduct;
uniform vec4 specularProduct;
uniform float shininess;
varying vec3 N, L, E;
void main()
{
vec4 fColor;
vec3 H = normalize(L + E);
vec4 ambient = ambientProduct;
float Kd = max(dot(L, N), 0.0);
vec4 diffuse = Kd * diffuseProduct;
float Ks = pow(max(dot(N, H), 0.0), shininess);
vec4 specular = Ks * specularProduct;
if (dot(L, N) < 0.0) {
specular = vec4(0.0, 0.0, 0.0, 1.0);
}
fColor = ambient + diffuse + specular;
fColor.a = 1.0;
gl_FragColor = fColor;
}
What am I doing wrong? How can I make the light behave independently from the viewer position?
Update 1:
After #Rabbid76's answer I edited the vertex shader by adding these lines (as well as passing the separate model and view matrices but I'll omit that for brevity's sake):
vec3 pos = (modelViewMatrix * vPosition).xyz;
vec3 light = (viewMatrix * lightPosition).xyz;
And I also updated the calculation of the N vector as the previous way of doing it seemed to not actually allow a per-fragment shading:
N = normalize(mat3(modelViewMatrix) * vNormal.xyz);
Still, the shade seems to move along with the rotation of the camera. This could be related to the fact that the light is multiplied by the viewMatrix I guess?
The calculation of the light vector is wrong.
L = normalize(light - pos);
While pos is a position in view space, light is a position in world space. light is the position of the light in the world. So light - pos doesn't make any sense at all. Both vectors have to be related to the same reference systems.
Transform the position of the light source by the view matrix, before you set it to the uniform lightPosition, to solve the issue.
Of course the transformation can also be done in shader code:
uniform mat4 viewMatrix;
uniform mat4 modelViewMatrix;
uniform vec4 lightPosition;
void main()
{
vec3 pos = (modelViewMatrix * vPosition).xyz;
vec3 light = (viewMatrix * lightPosition).xyz;
L = normalize(light - pos);
// ...
}
Further note that the position in view space has not to be inverted. It has to be
vec3 pos = (modelViewMatrix * vPosition).xyz;
rather than
vec3 pos = -(modelViewMatrix * vPosition).xyz;
A working snippet in your question is always helpful!
Issues
The light and the position need to be in the same space.
Those could be world space or view space but they need to be the same space.
The code had the position E in view space but the lightPosition in world space
You can't multiply a normal by a modelViewMatrix
You need to remove the translation. You also potentially need to deal
with scaling issues. See this article
The code is computing values in the vertex shader so they will be interpolated as they get passed to the fragment shader. That means they will no longer be unit vectors so you need to re-normalize them.
In computing the half vector you need to add their directions
The code was adding L (the direction from the surface to the light) to the view position of the surface instead of the direction from the surface to the view.
In computing a surface to light direction that would be light - pos but the code was negating pos. Of course you also need pos to be negative for the surface to view direction E
const gl = document.querySelector('canvas').getContext('webgl');
const m4 = twgl.m4;
const vs = `
attribute vec4 vPosition;
attribute vec4 vNormal;
varying vec3 N, L, E;
uniform mat4 viewMatrix;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform vec4 lightPosition;
void main()
{
vec3 pos = (modelViewMatrix * vPosition).xyz;
vec3 light = (viewMatrix * lightPosition).xyz;
L = light - pos;
E = -pos;
N = mat3(modelViewMatrix) * vNormal.xyz;
gl_Position = projectionMatrix * modelViewMatrix * vPosition;
}
`;
const fs = `
precision highp float;
uniform vec4 ambientProduct;
uniform vec4 diffuseProduct;
uniform vec4 specularProduct;
uniform float shininess;
varying vec3 N, L, E;
void main()
{
vec4 fColor;
vec3 normal = normalize(N);
vec3 surfaceToLightDir = normalize(L);
vec3 surfaceToViewDir = normalize(E);
vec3 H = normalize(surfaceToLightDir + surfaceToViewDir);
vec4 ambient = ambientProduct;
float Kd = max(dot(surfaceToLightDir, normal), 0.0);
vec4 diffuse = Kd * diffuseProduct;
float Ks = pow(max(dot(normal, H), 0.0), shininess);
vec4 specular = Ks * specularProduct;
if (dot(surfaceToLightDir, normal) < 0.0) {
specular = vec4(0.0, 0.0, 0.0, 1.0);
}
fColor = ambient + diffuse + specular;
fColor.a = 1.0;
gl_FragColor = fColor;
}
`;
// compiles shaders, links program, looks up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
const vertices = twgl.primitives.createSphereVertices(
2, // radius
8, // subdivision around
6, // subdivisions down
);
// calls gl.createBuffer, gl.bindBuffer, gl.bufferData
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
vPosition: vertices.position,
vNormal: vertices.normal,
indices: vertices.indices,
});
function render(time) {
time *= 0.001; // convert to seconds
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE);
gl.useProgram(programInfo.program);
// calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
const projectionMatrix = m4.perspective(
60 * Math.PI / 180, // field of view
gl.canvas.clientWidth / gl.canvas.clientHeight, // aspect
0.1, // znear
100, // zfar
);
const eye = [
Math.sin(time) * 5,
3,
3 + Math.cos(time) * 5,
];
const target = [0, 2, 3];
const up = [0, 1, 0];
const cameraMatrix = m4.lookAt(eye, target, up);
const viewMatrix = m4.inverse(cameraMatrix);
const worldMatrix = m4.translation([0, 2, 3]);
const modelViewMatrix = m4.multiply(viewMatrix, worldMatrix);
const uniforms = {
viewMatrix,
modelViewMatrix,
projectionMatrix,
lightPosition: [4, 3, 1, 1],
ambientProduct: [0, 0, 0, 1],
diffuseProduct: [1, 1, 1, 1],
specularProduct: [1, 1, 1, 1],
shininess: 50,
};
// calls gl.uniformXXX
twgl.setUniforms(programInfo, uniforms);
// calls gl.drawArrays or gl.drawElements
twgl.drawBufferInfo(gl, bufferInfo);
// -- not important to answer --
drawLightAndGrid(uniforms)
requestAnimationFrame(render);
}
requestAnimationFrame(render);
// -- ignore below this line. The only point is to give a frame
// of reference.
let gridBufferInfo;
function drawLightAndGrid(sphereUniforms) {
if (!gridBufferInfo) {
const vPosition = [];
const s = 100;
for (let x = -s; x <= s; x += 2) {
vPosition.push(x, 0, -s);
vPosition.push(x, 0, s);
vPosition.push(-s, 0, x);
vPosition.push( s, 0, x);
}
gridBufferInfo = twgl.createBufferInfoFromArrays(gl, {
vPosition,
vNormal: { value: [0, 0, 0], }
});
}
const worldMatrix = m4.translation(sphereUniforms.lightPosition);
m4.scale(worldMatrix, [0.1, 0.1, 0.1], worldMatrix);
const uniforms = Object.assign({}, sphereUniforms, {
modelViewMatrix: m4.multiply(sphereUniforms.viewMatrix, worldMatrix),
ambientProduct: [1, 0, 0, 1],
diffuseProduct: [0, 0, 0, 0],
specularProduct: [0, 0, 0, 0],
});
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
twgl.setUniforms(programInfo, uniforms);
twgl.drawBufferInfo(gl, bufferInfo);
twgl.setBuffersAndAttributes(gl, programInfo, gridBufferInfo);
twgl.setUniforms(programInfo, {
modelViewMatrix: sphereUniforms.viewMatrix,
ambientProduct: [0, 0, 1, 1],
});
twgl.drawBufferInfo(gl, gridBufferInfo, gl.LINES);
}
canvas { border: 1px solid black }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas></canvas>
For me personally I find short cryptic variable names hard to follow but that's a personal preference.

How to compare 2 textures in JavaScript / WebGL2?

I am writing a fragment shader for an image processing algorithm. The shader will run multiple times between two framebuffers in a loop (ping-pong). At some point I need to stop the loop when input and output textures are identical.
What I intend to do is the last step of Canny edge detector algorithm, "hysterezis edge tracking". I want to make a real time GPU/WebGL2 version of Canny algorithm and upload it to a web site.
This last step is as follows:
Given a double thresholded image containing "strong" edge pixels (1.0) and "weak" edge pixels (0.5)
find all chains of weak pixels connected with a strong pixel and mark them "strong"
keep all "strong" pixels and discard all remaining "weak" ones.
This can be implemented in a fragment shader running multiple times in a loop. The current "weak" pixel is marked "strong" if there is at least one strong pixel in its 8-pixel neighbourhood. At every iteration, we should have more strong pixels and less weak pixels. At the end, only isolated chains of weak pixel should remain. This is the point where the fragment shader becomes a pass-through shader and should be detected to stop the loop.
Update Sept 2019: I uploaded the GPU Canny Edge Detector here http://www.oldrinb.info/dip/canny/ . It works in browsers with WebGL2 support, as well in browsers that support WebGL1 and 'WEBGL_draw_buffers' extension. I'll put also the source code to github shortly.
I'm not 100% sure what you're asking. You're asking to compare on the CPU. You can read the contents of a texture by attaching it to a framebuffer and then calling gl.readPixels. you can then compare all the pixels. Note: not all texture formats can be attached to a framebuffer but assuming you're using a format that can. You've already attached textures to framebuffers for your ping-ponging so what more did you want?
Like I wrote in the comment on the GPU you can write a shader to compare 2 textures
#version 300 es
precision highp float;
uniform sampler2D tex1;
uniform sampler2D tex2;
out vec4 outColor;
void main() {
ivec2 size = textureSize(tex1, 0); // size of mip 0
float len = 0.0;
for (int y = 0; y < size.y; ++y) {
for (int x = 0; x < size.x; ++x) {
vec4 color1 = texelFetch(tex1, ivec2(x, y), 0);
vec4 color2 = texelFetch(tex2, ivec2(x, y), 0);
vec4 diff = color1 - color2;
len = length(diff);
if (len > 0.0) break;
}
if (len > 0.0) break;
}
outColor = mix(vec4(0), vec4(1), step(len, 0.0));
}
now just draw 1 pixel and read it with readPixels. if it's 0 the textures are the same. If it's not they are different.
The code assumes the textures are the same size but of course if they aren't the same size then we already know they can't be the same.
// make 3 canvaes as sources for textures
const canvases = ['A', 'B', 'B'].map((msg) => {
const canvas = document.createElement('canvas');
canvas.width = 128;
canvas.height = 128;
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'blue';
ctx.fillRect(0, 0, 128, 128);
ctx.font = '80px monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = 'yellow';
ctx.fillText(msg, 64, 64);
document.body.appendChild(canvas);
return canvas;
});
const gl = document.createElement('canvas').getContext('webgl2');
if (!gl) { alert('need webgl2'); }
const vs = `#version 300 es
void main() {
gl_PointSize = 1.0;
gl_Position = vec4(0, 0, 0, 1);
}
`;
const fs = `#version 300 es
precision highp float;
uniform sampler2D tex1;
uniform sampler2D tex2;
out vec4 outColor;
void main() {
ivec2 size = textureSize(tex1, 0); // size of mip 0
float len = 0.0;
for (int y = 0; y < size.y; ++y) {
for (int x = 0; x < size.x; ++x) {
vec4 color1 = texelFetch(tex1, ivec2(x, y), 0);
vec4 color2 = texelFetch(tex2, ivec2(x, y), 0);
vec4 diff = color1 - color2;
len = length(diff);
if (len > 0.0) break;
}
if (len > 0.0) break;
}
outColor = mix(vec4(0), vec4(1), step(len, 0.0));
}
`;
// compile shaders, link program, look up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
const textures = canvases.map((canvas) => {
// gl.createTexture, gl.bindTexture, gl.texImage, etc.
return twgl.createTexture(gl, {src: canvas});
});
compareTextures(0, 1);
compareTextures(1, 2);
function compareTextures(ndx1, ndx2) {
gl.useProgram(programInfo.program);
// gl.activeTexture, gl.bindTexture, gl.uniform
twgl.setUniforms(programInfo, {
tex1: textures[ndx1],
tex2: textures[ndx2],
});
// draw the bottom right pixel
gl.viewport(0, 0, 1, 1);
gl.drawArrays(gl.POINTS, 0, 1); // draw 1 point
// read the pixel
const result = new Uint8Array(4);
gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, result);
console.log('textures', ndx1, 'and', ndx2, 'are', result[0] ? 'the same' : 'not the same');
}
canvas { padding: 5px; }
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
You could also use occlusion queries. The plus is they might not block the GPU where as readPixels does. The minus is you can't check them in the same JavaScript event so they might not fit your needs
// make 3 canvaes as sources for textures
const canvases = ['A', 'B', 'B'].map((msg) => {
const canvas = document.createElement('canvas');
canvas.width = 128;
canvas.height = 128;
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'blue';
ctx.fillRect(0, 0, 128, 128);
ctx.font = '80px monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = 'yellow';
ctx.fillText(msg, 64, 64);
document.body.appendChild(canvas);
return canvas;
});
const gl = document.createElement('canvas').getContext('webgl2');
if (!gl) { alert('need webgl2'); }
const vs = `#version 300 es
void main() {
gl_PointSize = 1.0;
gl_Position = vec4(0, 0, 0, 1);
}
`;
const fs = `#version 300 es
precision highp float;
uniform sampler2D tex1;
uniform sampler2D tex2;
out vec4 outColor;
void main() {
ivec2 size = textureSize(tex1, 0); // size of mip 0
float len = 0.0;
for (int y = 0; y < size.y; ++y) {
for (int x = 0; x < size.x; ++x) {
vec4 color1 = texelFetch(tex1, ivec2(x, y), 0);
vec4 color2 = texelFetch(tex2, ivec2(x, y), 0);
vec4 diff = color1 - color2;
len = length(diff);
if (len > 0.0) break;
}
if (len > 0.0) break;
}
if (len > 0.0) {
discard;
}
outColor = vec4(1);
}
`;
// compile shaders, link program, look up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
const textures = canvases.map((canvas) => {
// gl.createTexture, gl.bindTexture, gl.texImage, etc.
return twgl.createTexture(gl, {src: canvas});
});
function wait(ms = 0) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function test() {
await compareTextures(0, 1);
await compareTextures(1, 2);
}
test();
async function compareTextures(ndx1, ndx2) {
gl.clear(gl.DEPTH_BUFFER_BIT);
gl.enable(gl.DEPTH_TEST);
gl.useProgram(programInfo.program);
// gl.activeTexture, gl.bindTexture, gl.uniform
twgl.setUniforms(programInfo, {
tex1: textures[ndx1],
tex2: textures[ndx2],
});
// draw the bottom right pixel
gl.viewport(0, 0, 1, 1);
const query = gl.createQuery();
gl.beginQuery(gl.ANY_SAMPLES_PASSED, query);
gl.drawArrays(gl.POINTS, 0, 1); // draw 1 point
gl.endQuery(gl.ANY_SAMPLES_PASSED);
gl.flush();
let ready = false;
while(!ready) {
await wait();
ready = gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE);
}
const same = gl.getQueryParameter(query, gl.QUERY_RESULT);
console.log('textures', ndx1, 'and', ndx2, 'are', same ? 'the same' : 'not the same');
}
canvas { padding: 5px; }
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>

How I can do something with the extension ANGLE_instanced_arrays?

I want to do any ANGLE_instanced_arrays and I was read the documentation on MDN and I did not understand anything. Well, I understood that I can make graphic 2D and 3D like : examples but for me these examples are too advanced and it's difficult to understand all the code. Can anyone help me with an example?
This part I understand of the documentation official:
The ANGLE_instanced_arrays extension is part of the WebGL API and allows to draw the same object, or groups of similar objects multiple times, if they share the same vertex data, primitive count and type.
here is what I read
Can you draw without ANGLE_instanced_arrays? The difference between drawing with and without are
you call a different draw function and pass the an extra parameter of how many instances to draw. ext.drawArraysInstancedANGLE or ext.drawElementsInstancedANGLE instead of the normal ext.drawArrays or ext.drawElements
you add one or more attributes to your vertex shader who's values will only change once per instance drawn. In other words if you're drawing a cube the attribute's value will be the same value for every vertex while drawing the first cube, a different value while drawing the 2nd cube, a 3rd value while drawn the 3rd cube. Where as normal attributes change for each vertex these attributes only change once per cube/item.
The most obvious attribute to add is an extra per cube position so that each cube can have a different position added to the vertex positions but you could add another attribute for a pure cube color or add matrix attributes so you can orient each cube completely independently or whatever you want.
For those attributes that only change once per cube you set their vertex divisor to 1 by calling ext.vertexAttribDivisorANGLE. The 1 means "advance the attribute every 1 instance". 0 (the default) means advance the attribute every vertex (every iteration of the vertex shader).
Here's an example drawing a single quad (2 triangles, 6 vertices)
const vs = `
attribute vec4 position;
uniform mat4 u_matrix;
void main() {
gl_Position = u_matrix * position;
}
`;
const fs = `
precision mediump float;
uniform vec4 u_color;
void main() {
gl_FragColor = u_color;
}
`;
const m4 = twgl.m4;
const gl = document.querySelector("canvas").getContext("webgl");
const program = twgl.createProgram(gl, [vs, fs]);
const positionLocation = gl.getAttribLocation(program, "position");
const matrixLocation = gl.getUniformLocation(program, "u_matrix");
const colorLocation = gl.getUniformLocation(program, "u_color");
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
// one face
-1, -1,
1, -1,
-1, 1,
-1, 1,
1, -1,
1, 1,
],
), gl.STATIC_DRAW);
gl.enable(gl.DEPTH_TEST);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.enableVertexAttribArray(positionLocation);
{
const size = 2; // 2 values per vertex
const type = gl.FLOAT;
const normalize = false;
const stride = 0;
const offset = 0;
gl.vertexAttribPointer(positionLocation, size, type, normalize, stride, offset);
}
gl.useProgram(program);
gl.uniform4fv(colorLocation, [1, .5, .2, 1]);
gl.uniformMatrix4fv(matrixLocation, false, m4.scaling([.25, .25, .25]));
const offset = 0;
const vertexCount = 6;
gl.drawArrays(gl.TRIANGLES, offset, vertexCount);
canvas { border: 1px solid black; }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas></canvas>
and here's an example drawing 100 quads using ANGLE_instanced_arrays. We've added a planeOffset for an offset for each quad and a planeColor for a color for each quad.
const vs = `
attribute vec4 position;
attribute vec2 planeOffset; // per plane offset
attribute vec4 planeColor; // per plane color
uniform mat4 u_matrix;
varying vec4 v_color;
void main() {
mat4 translation = mat4(
vec4(1, 0, 0, 0),
vec4(0, 1, 0, 0),
vec4(0, 0, 1, 0),
vec4(planeOffset, 0, 1));
gl_Position = u_matrix * translation * position;
v_color = planeColor;
}
`;
const fs = `
precision mediump float;
varying vec4 v_color;
void main() {
gl_FragColor = v_color;
}
`;
function main() {
const m4 = twgl.m4;
const gl = document.querySelector("canvas").getContext("webgl");
const ext = gl.getExtension("ANGLE_instanced_arrays");
if (!ext) {
alert("need ANGLE_instanced_arrays");
return;
}
const program = twgl.createProgram(gl, [vs, fs]);
const positionLocation = gl.getAttribLocation(program, "position");
const offsetLocation = gl.getAttribLocation(program, "planeOffset");
const colorLocation = gl.getAttribLocation(program, "planeColor");
const matrixLocation = gl.getUniformLocation(program, "u_matrix");
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
// one face
-1, -1,
1, -1,
-1, 1,
-1, 1,
1, -1,
1, 1,
],
), gl.STATIC_DRAW);
// make 100 offsets and 100 colors
const colors = [];
const offsets = [];
const numInstances = 100;
for (let i = 0; i < 100; ++i) {
colors.push(Math.random(), Math.random(), Math.random(), 1);
offsets.push(Math.random() * 20 - 10, Math.random() * 20 - 10);
}
// put those in buffers
const offsetBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, offsetBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(offsets), gl.STATIC_DRAW);
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
gl.enable(gl.DEPTH_TEST);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.enableVertexAttribArray(positionLocation);
{
const size = 2; // 2 values per vertex
const type = gl.FLOAT;
const normalize = false;
const stride = 0;
const offset = 0;
gl.vertexAttribPointer(positionLocation, size, type, normalize, stride, offset);
}
gl.bindBuffer(gl.ARRAY_BUFFER, offsetBuffer);
gl.enableVertexAttribArray(offsetLocation);
{
const size = 2; // 2 values per vertex
const type = gl.FLOAT;
const normalize = false;
const stride = 0;
const offset = 0;
gl.vertexAttribPointer(offsetLocation, size, type, normalize, stride, offset);
ext.vertexAttribDivisorANGLE(offsetLocation, 1);
}
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.enableVertexAttribArray(colorLocation);
{
const size = 4; // 4 values per vertex
const type = gl.FLOAT;
const normalize = false;
const stride = 0;
const offset = 0;
gl.vertexAttribPointer(colorLocation, size, type, normalize, stride, offset);
ext.vertexAttribDivisorANGLE(colorLocation, 1);
}
gl.useProgram(program);
gl.uniformMatrix4fv(matrixLocation, false, m4.scaling([.1, .1, .1]));
const offset = 0;
const vertexCount = 6;
ext.drawArraysInstancedANGLE(gl.TRIANGLES, offset, vertexCount, numInstances);
}
main();
canvas { border: 1px solid black; }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas></canvas>

alpha blending in webgl works not correctly

code:
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LESS);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
Problem in that on a figure "superfluous" is drawn:
how to correct it?
P.S. alpha=0.9
What is your perspective zNear and zFar set to? Is it possible you're setting it too close and the back of your cube is being clipped? See sample below where it's set too close. That doesn't look like your issue but it's hard to tell.
Also are you sorting your polygons? When rendering transparent things you generally have to draw front to back. For a convex object like a sphere, pyramid, or cube you can draw twice with culling on, first with gl.cullFace(gl.FRONT) to draw only the backfacing triangles, the ones further from the camera, and then again with gl.cullFace(gl.BACK) to draw only the front facing triangles, the ones closer to the camera.
Yet another issue is are you correctly providing premultiplied alpha to the canvas? Most shaders do this
gl_FragColor = someUnpremultipliedAlphaColor;
But by default you need to provide pre-multiplied alpha colors
gl_FragColor = vec4(color.rgb * color.a, color.a);
Or you can set the canvas to use un-premultiplied colors
gl = someCanvas.getContext("webgl", { premultipliedAlpha: false });
window.onload = function() {
// Get A WebGL context
var canvas = document.getElementById("c");
var gl = canvas.getContext("webgl");
if (!gl) {
return;
}
var programInfo = webglUtils.createProgramInfo(gl, ["vs", "fs"]);
var createFlattenedVertices = function(gl, vertices) {
return webglUtils.createBufferInfoFromArrays(
gl,
primitives.makeRandomVertexColors(
primitives.deindexVertices(vertices),
{
vertsPerColor: 6,
rand: function(ndx, channel) {
return channel < 3 ? ((128 + Math.random() * 128) | 0) : 255;
}
})
);
};
var bufferInfo = createFlattenedVertices(gl, primitives.createCubeVertices(1));
function degToRad(d) {
return d * Math.PI / 180;
}
var cameraAngleRadians = degToRad(0);
var fieldOfViewRadians = degToRad(60);
var uniforms = {
u_color: [1, 1, 1, 0.8],
u_matrix: null,
};
var zClose = false;
var zNear = 1;
var zFar = 3;
var zElem = document.getElementById("z");
var bElem = document.getElementById("b");
bElem.addEventListener('click', toggleZDepth, false);
toggleZDepth();
function toggleZDepth() {
zClose = !zClose;
zFar = zClose ? 3.5 : 4;
zElem.innerHTML = zFar;
}
function drawScene() {
gl.enable(gl.CULL_FACE);
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
var aspect = canvas.clientWidth / canvas.clientHeight;
var projectionMatrix =
makePerspective(fieldOfViewRadians, aspect, zNear, zFar);
var time = Date.now() * 0.0005;
var radius = 3;
var cameraPosition = [Math.cos(time) * radius, 1, Math.sin(time) * radius];
var target = [0, 0, 0];
var up = [0, 1, 0];
var cameraMatrix = makeLookAt(cameraPosition, target, up);
var viewMatrix = makeInverse(cameraMatrix);
uniforms.u_matrix = matrixMultiply(viewMatrix, projectionMatrix);
gl.useProgram(programInfo.program);
webglUtils.setBuffersAndAttributes(gl, programInfo.attribSetters, bufferInfo);
webglUtils.setUniforms(programInfo.uniformSetters, uniforms);
// draw back facing polygons first
gl.cullFace(gl.FRONT);
gl.drawArrays(gl.TRIANGLES, 0, bufferInfo.numElements);
// now draw front facing polygons
gl.cullFace(gl.BACK);
gl.drawArrays(gl.TRIANGLES, 0, bufferInfo.numElements);
requestAnimationFrame(drawScene);
}
drawScene();
}
canvas {
border: 1px solid black;
}
#overlay {
position: absolute;
top: 20px;
left: 20px;
z-index: 2;
}
<script src="//webglfundamentals.org/webgl/resources/webgl-utils.js"></script>
<script src="//webglfundamentals.org/webgl/resources/webgl-3d-math.js"></script>
<script src="//webglfundamentals.org/webgl/resources/primitives.js"></script>
<canvas id="c" width="400" height="200"></canvas>
<div id="overlay">
<button id="b">toggle z-far</button>
<div>z-far = <span id="z"></span></div>
</div>
<!-- vertex shader -->
<script id="vs" type="x-shader/x-vertex">
attribute vec4 a_position;
attribute vec4 a_color;
varying vec4 v_color;
uniform mat4 u_matrix;
void main() {
gl_Position = u_matrix * a_position;
v_color = a_color;
}
</script>
<!-- fragment shader -->
<script id="fs" type="x-shader/x-fragment">
precision mediump float;
uniform vec4 u_color;
varying vec4 v_color;
void main() {
vec4 color = v_color * u_color;
gl_FragColor = vec4(color.rgb * color.a, color.a); // premultiply color
}
</script>

Categories

Resources