Related
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
Apologies in advanced if I don't explain anything clearly, please feel free to ask for clarification. This hobby game project means a lot to me
I am making a voxel rendering engine using webgl. It uses gl.points to draw squares for each voxel. I simply use a projection matrix translated by the cameras position, and then rotated by the cameras rotations.
gl_Position =
uMatrix * uModelMatrix * vec4(aPixelPosition[0],-aPixelPosition[2],aPixelPosition[1],1.0);
The modelviewmatrix is simply just the default mat4.create(), for some reason it would not display anything without one. aPixelPosition is simply the X,Z,Y (in webgl space) of a voxel.
Using something like this:
gl_PointSize = (uScreenSize[1]*0.7) / (gl_Position[2]);
You can set the size of the voxels based on their distance from the camera. Which works pretty well minus one visual error.
(Picture from inside a large hollow cube)
You can see the back wall displays fine (because they all are pointed directly at you) but the walls that are displayed at an angle to you, need to be increased in size.
So I used the dot product between your facing position, and the position of the voxel minus your camera position to get the angle of each block and colored them accordingly.
vPosition=acos(dot( normalize(vec2(sin(uYRotate),-cos(uYRotate))) ,
normalize(vec2(aPixelPosition[0],aPixelPosition[1])-
vec2(uCam[0],uCam[1]))));
then color the blocks with what this returns.
(walls go from black to white depending on their angle to you)
This visual demonstration shows the problem, the walls on the back face all point at an angle to you except for the ones you are directly looking at, the walls on the side of the same face get more and more angled to you.
If I adjust the pointSize to increase with the angle using this, it will fix the visual glitch, but it introduces a new one.
Everything looks good from here, but if you get really close to a wall of blocks and move left and right
There is a fairly noticeable bubbling effect as you scan left and right, because the ones on the side of your view are slightly more at an angle (even though they should face the same way anyways)
So clearly, my math isn't the best. How could I have it so only the walls on the side return an angle? And the ones on the back wall all don't return any angle. Thanks a ton.
I have tried making it so the dot product always checks the voxels X as if it is the same as the cameras, but this just made it so each voxel was colored the same.
I'm not sure you can actually do what you're trying to do which is represent voxel (cubes) and 2D squares (gl.POINTS).
I'm not sure I can demo the issue. Maybe I should write a program to draw this so you can move the camera around but ...
Consider these 6 cubes
Just putting a square at their projected centers won't work
It seems to me there are no squares that will represent those cubes in a generic way that have no gaps and no other issues.
To make sure there are no gaps, every pixel the cube would cover needs to be covered by the square. So, first we can draw the rectangle that covers each cube
Then because gl.POINTS are square we need to expand each area to a square
given the amount of overlap there are going to be all kinds of issues. At extreme angles the size a particular square needs to be to cover the screen space of the cube it represents will get really large. Then, when Z is the same for a bunch of cubes you'll get z-fighting issues. For example the blue square will appear in front of the green square where they overlap making a little notch in the green.
We can see that here
Each green pixel is partially overlapped by the brown pixel that is one column to the right and one voxel down because that POINT is in front and large enough to cover the screen space the brown voxel takes it ends up covering the green pixel to the left and up one.
Here's a shader that follows the algorithm above. For each point in 3D space it assumes a unit cube. It computes the normalized device coordinates (NDC) of each of the 8 points of the cube and uses those to get the min and max NDC coordinates. From that it can compute the gl_PointSize need to cover that large of an area. It then places the point in the center of that area.
'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;
uniform vec2 resolution;
varying vec3 v_normal;
varying vec3 v_color;
vec2 computeNDC(vec4 p, vec4 off) {
vec4 clipspace = projection * modelView * (p + off);
return clipspace.xy / clipspace.w;
}
void main() {
vec2 p0 = computeNDC(position, vec4(-.5, -.5, -.5, 0));
vec2 p1 = computeNDC(position, vec4( .5, -.5, -.5, 0));
vec2 p2 = computeNDC(position, vec4(-.5, .5, -.5, 0));
vec2 p3 = computeNDC(position, vec4( .5, .5, -.5, 0));
vec2 p4 = computeNDC(position, vec4(-.5, -.5, .5, 0));
vec2 p5 = computeNDC(position, vec4( .5, -.5, .5, 0));
vec2 p6 = computeNDC(position, vec4(-.5, .5, .5, 0));
vec2 p7 = computeNDC(position, vec4( .5, .5, .5, 0));
vec2 minNDC =
min(p0, min(p1, min(p2, min(p3, min(p4, min(p5, min(p6, p7)))))));
vec2 maxNDC =
max(p0, max(p1, max(p2, max(p3, max(p4, max(p5, max(p6, p7)))))));
vec2 minScreen = (minNDC * 0.5 + 0.5) * resolution;
vec2 maxScreen = (maxNDC * 0.5 + 0.5) * resolution;
vec2 rangeScreen = ceil(maxScreen) - floor(minScreen);
float sizeScreen = max(rangeScreen.x, rangeScreen.y);
// sizeSize is now how large the point has to be to touch the
// corners
gl_PointSize = sizeScreen;
vec4 pos = projection * modelView * position;
// clip ourselves
if (pos.x < -pos.w || pos.x > pos.w) {
gl_Position = vec4(0,0,-10,1);
return;
}
// pos is the wrong place to put the point. The correct
// place to put the point is the center of the extents
// of the screen space points
gl_Position = vec4(
(minNDC + (maxNDC - minNDC) * 0.5) * pos.w,
pos.z,
pos.w);
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);
gl_FragColor.rgb *= gl_FragColor.a;
}
`;
// 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 },
});
let camera;
const eye = [10, 10, 55];
const target = [0, 0, 0];
const up = [0, 1, 0];
const speed = 0.5;
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 });
function render() {
twgl.resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE);
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);
camera = m4.lookAt(eye, target, up);
const view = m4.inverse(camera);
const modelView = m4.translate(view, [width / -2, 0, width / -2]);
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,
resolution: [gl.canvas.width, gl.canvas.height],
});
// calls gl.drawArrays or gl.drawElements
twgl.drawBufferInfo(gl, bufferInfo, gl.POINTS);
}
render();
window.addEventListener('keydown', (e) => {
e.preventDefault();
const move = keyMove.get(e.keyCode);
if (move) {
const dir = camera.slice(move.ndx, move.ndx + 3);
const delta = v3.mulScalar(dir, speed * move.target);
v3.add(target, delta, target);
if (move.eye) {
v3.add(eye, delta, eye);
}
render();
}
});
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>
Even on top of that you're going to have other issues using POINTS
the max point size only has to be 1.
The spec says implementation can choose a max size point they support and that at has to be at least 1. In other words, some implementations might only support point sizes of 1. Checking WebGLStats it appears it appears in reality you might be ok but still...
some implementations clip POINTS in correctly and it's unlikely to be fixed
See https://stackoverflow.com/a/56066386/128511
I am working on software which is visualising engineering data on a surface of 3D model as color maps. For this I am using WebGL. At the moment I was able to display colors on surface of 3D model.
But now I need to improve visualisation to make sharp transitions between colors (without color interpolation on a surface of triangles).
I am not sure how to do it efficiently.
smooth contours plot
sharp contours plot
It's not clear what you're trying to do. You have not provided enough information to understand how your colors are chosen/computed in the first place.
I can only guess of a couple of solutions that might fit your description
Post process with a posterization type of technique
You could do a simple
gl_FragColor.rgb = floor(gl_FragColor.rgb * numLevels) / numLevels;
Or you could do it in some color space like
// convert to HSV
vec3 hsv = rgb2hsv(gl_FragColor.rgb);
// quantize hue only
hsv.x = floor(hsv.x * numLevels) / numLevels;
// concert back to RGB
gl_FragColor.rgb = hsv2rgb(hsv);
Or you could also do this in your 3D shader, it doesn't have to be post process.
You can find rgb2hsv and hsv2rgb here but of course you could use some other color space.
Example:
const gl = document.querySelector('canvas').getContext('webgl');
const m4 = twgl.m4;
const v3 = twgl.v3;
// used to generate colors
const ctx = document.createElement('canvas').getContext('2d');
ctx.canvas.width = 1;
ctx.canvas.height = 1;
const vs = `
attribute vec4 position;
attribute vec3 normal;
// note: there is no reason this has to come from an attrbute (per vertex)
// it could just as easily come from a texture used in the fragment shader
// for more resolution
attribute vec4 color;
uniform mat4 projection;
uniform mat4 modelView;
varying vec3 v_normal;
varying vec4 v_color;
void main () {
gl_Position = projection * modelView * position;
v_normal = mat3(modelView) * normal;
v_color = color;
}
`;
const fs = `
precision mediump float;
varying vec3 v_normal;
varying vec4 v_color;
uniform float numLevels;
uniform vec3 lightDirection;
vec3 rgb2hsv(vec3 c) {
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}
vec3 hsv2rgb(vec3 c) {
c = vec3(c.x, clamp(c.yz, 0.0, 1.0));
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
void main() {
vec3 hsv = rgb2hsv(v_color.rgb);
hsv.x = floor(hsv.x * numLevels) / numLevels;
vec3 rgb = hsv2rgb(hsv);
// fake light
float light = dot(normalize(v_normal), lightDirection) * .5 + .5;
gl_FragColor = vec4(rgb * light, v_color.a);
// uncomment next line to see without hue quantization
// gl_FragColor = v_color;
}
`;
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
const radius = 5;
const thickness = 2;
const radialDivisions = 32;
const bodyDivisions = 12;
// creates positions, normals, etc...
const arrays = twgl.primitives.createTorusVertices(
radius, thickness, radialDivisions, bodyDivisions);
// add colors for each vertex
const numVerts = arrays.position.length / 3;
const colors = new Uint8Array(numVerts * 4);
for (let i = 0; i < numVerts; ++i) {
const pos = arrays.position.subarray(i * 3, i * 3 + 3);
const dist = v3.distance([3, 1, 3 + Math.sin(pos[0])], pos);
colors.set(hsla(clamp(dist / 10, 0, 1), 1, .5, 1), i * 4);
}
arrays.color = {
numComponents: 4,
data: colors,
};
// calls gl.createBuffer, gl.bindBuffer, gl.bufferData for each
// array in arrays
const bufferInfo = twgl.createBufferInfoFromArrays(gl, arrays);
twgl.resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.enable(gl.DEPTH_TEST);
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const halfHeight = 8;
const halfWidth = halfHeight * aspect;
const projection = m4.ortho(
-halfWidth, halfWidth,
-halfHeight, halfHeight,
-2, 2);
const modelView = m4.identity();
m4.rotateX(modelView, Math.PI * .5, modelView);
gl.useProgram(programInfo.program);
// calls gl.bindbuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
// for each attribute
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
// calls gl.activeTexture, gl.bindTexture, gl.uniformXXX
twgl.setUniforms(programInfo, {
projection,
modelView,
numLevels: 8,
lightDirection: v3.normalize([1, 2, 3]),
});
// calls gl.drawArrays or gl.drawElements
twgl.drawBufferInfo(gl, bufferInfo);
function hsla(h, s, l, a) {
ctx.fillStyle = `hsla(${h * 360 | 0},${s * 100 | 0}%,${l * 100 | 0}%,${a})`;
ctx.fillRect(0, 0, 1, 1);
return ctx.getImageData(0, 0, 1, 1).data;
}
function clamp(v, min, max) {
return Math.min(max, Math.max(min, v));
}
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>
Render in 1 channel, use a lookup table
In this case you'd make an Nx1 texture with your N colors. Then in your shader you'd just compute a gray scale (it's not clear how you're coloring things now) and use that to look up a color from your texture
uniform sampler2D lookupTable; // Nx1 texture set to nearest filtering
float gray = whateverYourDoingNow();
vec4 color = texture2D(lookupTable, vec2((gray, 0.5);
// apply lighting to color
...
Example:
const gl = document.querySelector('canvas').getContext('webgl');
const m4 = twgl.m4;
const v3 = twgl.v3;
const vs = `
attribute vec4 position;
attribute vec3 normal;
// note: there is no reason this has to come from an attrbute (per vertex)
// it could just as easily come from a texture used in the fragment shader
// for more resolution
attribute float hotness; // the data value 0 to 1
uniform mat4 projection;
uniform mat4 modelView;
varying vec3 v_normal;
varying float v_hotness;
void main () {
gl_Position = projection * modelView * position;
v_normal = mat3(modelView) * normal;
v_hotness = hotness;
}
`;
const fs = `
precision mediump float;
varying vec3 v_normal;
varying float v_hotness;
uniform float numColors;
uniform sampler2D lookupTable;
uniform vec3 lightDirection;
void main() {
vec4 color = texture2D(lookupTable, vec2(v_hotness, 0.5));
// fake light
float light = dot(normalize(v_normal), lightDirection) * .5 + .5;
gl_FragColor = vec4(color.rgb * light, color.a);
}
`;
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
const radius = 5;
const thickness = 2;
const radialDivisions = 32;
const bodyDivisions = 12;
// creates positions, normals, etc...
const arrays = twgl.primitives.createTorusVertices(
radius, thickness, radialDivisions, bodyDivisions);
// add a hotness value, 0 <-> 1, for each vertex
const numVerts = arrays.position.length / 3;
const hotness = [];
for (let i = 0; i < numVerts; ++i) {
const pos = arrays.position.subarray(i * 3, i * 3 + 3);
const dist = v3.distance([3, 1, 3 + Math.sin(pos[0])], pos);
hotness[i] = clamp(dist / 10, 0, 1);
}
arrays.hotness = {
numComponents: 1,
data: hotness,
};
// calls gl.createBuffer, gl.bindBuffer, gl.bufferData for each
// array in arrays
const bufferInfo = twgl.createBufferInfoFromArrays(gl, arrays);
const colors = [
255, 0, 0, 255, // red
255, 150, 30, 255, // orange
255, 255, 0, 255, // yellow
0, 210, 0, 255, // green
0, 255, 255, 255, // cyan
0, 0, 255, 255, // blue
160, 30, 255, 255, // purple
255, 0, 255, 255, // magenta
];
// calls gl.createTexture, gl.texImage2D, gl.texParameteri
const lookupTableTexture = twgl.createTexture(gl, {
src: colors,
width: colors.length / 4,
wrap: gl.CLAMP_TO_EDGE,
minMag: gl.NEAREST, // comment this line out to see non hard edges
});
twgl.resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.enable(gl.DEPTH_TEST);
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const halfHeight = 8;
const halfWidth = halfHeight * aspect;
const projection = m4.ortho(
-halfWidth, halfWidth,
-halfHeight, halfHeight,
-2, 2);
const modelView = m4.identity();
m4.rotateX(modelView, Math.PI * .5, modelView);
gl.useProgram(programInfo.program);
// calls gl.bindbuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
// for each attribute
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
// calls gl.activeTexture, gl.bindTexture, gl.uniformXXX
twgl.setUniforms(programInfo, {
projection,
modelView,
lookupTable: lookupTableTexture,
lightDirection: v3.normalize([1, 2, 3]),
});
// calls gl.drawArrays or gl.drawElements
twgl.drawBufferInfo(gl, bufferInfo);
function clamp(v, min, max) {
return Math.min(max, Math.max(min, v));
}
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>
One way of doing this would be add the flat interpolation modifier to your color attribute, as described in this tutorial. This will prevent color values from beeing interpolated, so each triangle will end up with only one color (the one specified in the first of the three vertices).
Sadly I couldn't find anything about its webgl support, but you might as well try it out to see if it works.
If it doesn't work or you don't want the inividual triangles to be visible, you could also load the color data to a texture and retrieve the color of each pixel in the fragment shader. There would still be some interpolation depending on the texture size though (similar to how an image becomes blurry when scaled up).
We've created a WebGl application which displays a scene containing multiple objects. The entire scene can be rotated in multiple directions. The application requires the user to be able to zoom up to but NOT thru the object. I know this functionality can be implemented using webgl frameworks such as Three.js and SceneJs. Unfortunately, our application is not leveraging a framework. Is there a way to implement the zoom functionality described here using webgl only? Note: I don't believe object picking will work for us since the user is not required to select any object in the scene. Thanks for your help.
Off the top of my head.
First off you need to know the size of each object in world space. For example if one object is 10 units big and another is 100 units big you probably want to be a different distance from the 100 unit object as the 10 unit object. By world space I also mean if you're scaling the 10 unit object by 9 then in world space it would be 90 units big and again you'd want to get a different distance away then if it was 10 units
You generally compute the size of an object in local space by computing the extents of its vertices. Just go through all the vertices and keep track of the min and max values in x, y, and z. Whether you want to take the biggest value from the object's origin or compute an actual center point is up to you.
So, given the size we can compute how far away you need to be to see the entire object. For the standard perspective matrix you can just work backward. If you know your object is 10 units big then you need to fit 10 units in your frustum. You'd probably actually pick something like 14 units (say size * 1.4) so there's some space around the object.
We know halfFovy, halfSizeToFitOnScreen, we need to compute distance
sohcahtoa
tangent = opposite / adjacent
opposite = halfsizeToFitOnScreen
adjacent = distance
tangent = Math.tan(halfFovY)
Therefore
tangent = sizeToFitOnScreen / distance
tangent * distance = sizeToFitOnScreen
distance = sizeToFitOnScreen / tangent
distance = sizeToFitOnScreen / Math.tan(halfFovY)
So now we know the camera needs to be distance away from the object. There's an entire sphere that's distance away from the object. Where you pick on that sphere is up to you. Assuming you go from where the camera currently is you can compute the direction from the object to the camera
direction = normalize(cameraPos - objectPos)
Now you can compute a point distance away in that direction.
desiredCameraPosition = direction * distance
Now either put the camera there using some lookAt function
matrix = lookAt(desiredCameraPosition, objectPosition, up)
Or lerp between where the camera currently is to it's new desired position
var m4 = twgl.m4;
var v3 = twgl.v3;
twgl.setAttributePrefix("a_");
var gl = twgl.getWebGLContext(document.getElementById("c"));
var programInfo = twgl.createProgramInfo(gl, ["vs", "fs"]);
var shapes = [
twgl.primitives.createCubeBufferInfo(gl, 2),
twgl.primitives.createSphereBufferInfo(gl, 1, 24, 12),
twgl.primitives.createTruncatedConeBufferInfo(gl, 1, 0, 2, 24, 1),
];
function rand(min, max) {
return min + Math.random() * (max - min);
}
function easeInOut(t, start, end) {
var c = end - start;
if ((t /= 0.5) < 1) {
return c / 2 * t * t + start;
} else {
return -c / 2 * ((--t) * (t - 2) - 1) + start;
}
}
// Shared values
var lightWorldPosition = [1, 8, -10];
var lightColor = [1, 1, 1, 1];
var camera = m4.identity();
var view = m4.identity();
var viewProjection = m4.identity();
var targetNdx = 0;
var targetTimer = 0;
var zoomTimer = 0;
var eye = v3.copy([1, 4, -60]);
var target = v3.copy([0, 0, 0]);
var up = [0, 1, 0];
var zoomScale = 1.4;
var zoomDuration = 2;
var targetChangeInterval = 3;
var oldEye;
var oldTarget;
var newEye;
var newTarget;
var tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 2, 2, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([
255,255,255,255,
192,192,192,255,
192,192,192,255,
255,255,255,255]));
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
var objects = [];
var drawObjects = [];
var numObjects = 100;
var baseHue = rand(0, 360);
for (var ii = 0; ii < numObjects; ++ii) {
var uniforms = {
u_lightWorldPos: lightWorldPosition,
u_lightColor: lightColor,
u_diffuseMult: chroma.hsv((baseHue + rand(0, 60)) % 360, 0.4, 0.8).gl(),
u_specular: [1, 1, 1, 1],
u_shininess: 50,
u_specularFactor: 1,
u_diffuse: tex,
u_viewInverse: camera,
u_world: m4.identity(),
u_worldInverseTranspose: m4.identity(),
u_worldViewProjection: m4.identity(),
};
drawObjects.push({
programInfo: programInfo,
bufferInfo: shapes[ii % shapes.length],
uniforms: uniforms,
});
objects.push({
translation: [rand(-50, 50), rand(-50, 50), rand(-50, 50)],
scale: rand(1, 5),
size: 2,
xSpeed: rand(0.2, 0.7),
zSpeed: rand(0.2, 0.7),
uniforms: uniforms,
});
}
var then = Date.now() * 0.001;
function render() {
twgl.resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
var time = Date.now() * 0.001;
var elapsed = time - then;
then = time;
var radius = 6;
var fovy = 30 * Math.PI / 180;
var projection = m4.perspective(fovy, gl.canvas.clientWidth / gl.canvas.clientHeight, 0.5, 200);
targetTimer -= elapsed;
if (targetTimer <= 0) {
targetTimer = targetChangeInterval;
zoomTimer = 0;
targetNdx = (targetNdx + 1) % objects.length;
oldEye = v3.copy(eye);
oldTarget = v3.copy(target);
var targetObj = objects[targetNdx];
newTarget = targetObj.translation;
var halfSize = targetObj.size * targetObj.scale * zoomScale * 0.5;
var distance = halfSize / Math.tan(fovy * 0.5);
var direction = v3.normalize(v3.subtract(eye, newTarget));
newEye = v3.add(newTarget, v3.mulScalar(direction, distance));
}
zoomTimer += elapsed;
var lerp = easeInOut(Math.min(1, zoomTimer / zoomDuration), 0, 1);
eye = v3.lerp(oldEye, newEye, lerp);
target = v3.lerp(oldTarget, newTarget, lerp);
m4.lookAt(eye, target, up, camera);
m4.inverse(camera, view);
m4.multiply(projection, view, viewProjection);
objects.forEach(function(obj, ndx) {
var uni = obj.uniforms;
var world = uni.u_world;
m4.identity(world);
m4.translate(world, obj.translation, world);
m4.rotateX(world, time * obj.xSpeed, world);
m4.rotateZ(world, time * obj.zSpeed, world);
m4.scale(world, [obj.scale, obj.scale, obj.scale], world);
m4.transpose(m4.inverse(world, uni.u_worldInverseTranspose), uni.u_worldInverseTranspose);
m4.multiply(viewProjection, uni.u_world, uni.u_worldViewProjection);
});
twgl.drawObjectList(gl, drawObjects);
requestAnimationFrame(render);
}
render();
body {
margin: 0;
}
canvas {
width: 100vw;
height: 100vh;
display: block;
}
<script src="//twgljs.org/dist/4.x/twgl-full.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/chroma-js/0.6.3/chroma.min.js"></script>
<canvas id="c"></canvas>
<script id="vs" type="notjs">
uniform mat4 u_worldViewProjection;
uniform vec3 u_lightWorldPos;
uniform mat4 u_world;
uniform mat4 u_viewInverse;
uniform mat4 u_worldInverseTranspose;
attribute vec4 a_position;
attribute vec3 a_normal;
attribute vec2 a_texcoord;
varying vec4 v_position;
varying vec2 v_texCoord;
varying vec3 v_normal;
varying vec3 v_surfaceToLight;
varying vec3 v_surfaceToView;
void main() {
v_texCoord = a_texcoord;
v_position = (u_worldViewProjection * a_position);
v_normal = (u_worldInverseTranspose * vec4(a_normal, 0)).xyz;
v_surfaceToLight = u_lightWorldPos - (u_world * a_position).xyz;
v_surfaceToView = (u_viewInverse[3] - (u_world * a_position)).xyz;
gl_Position = v_position;
}
</script>
<script id="fs" type="notjs">
precision mediump float;
varying vec4 v_position;
varying vec2 v_texCoord;
varying vec3 v_normal;
varying vec3 v_surfaceToLight;
varying vec3 v_surfaceToView;
uniform vec4 u_lightColor;
uniform vec4 u_diffuseMult;
uniform sampler2D u_diffuse;
uniform vec4 u_specular;
uniform float u_shininess;
uniform float u_specularFactor;
vec4 lit(float l ,float h, float m) {
return vec4(1.0,
abs(l),//max(l, 0.0),
(l > 0.0) ? pow(max(0.0, h), m) : 0.0,
1.0);
}
void main() {
vec4 diffuseColor = texture2D(u_diffuse, v_texCoord) * u_diffuseMult;
vec3 a_normal = normalize(v_normal);
vec3 surfaceToLight = normalize(v_surfaceToLight);
vec3 surfaceToView = normalize(v_surfaceToView);
vec3 halfVector = normalize(surfaceToLight + surfaceToView);
vec4 litR = lit(dot(a_normal, surfaceToLight),
dot(a_normal, halfVector), u_shininess);
vec4 outColor = vec4((
u_lightColor * (diffuseColor * litR.y +
u_specular * litR.z * u_specularFactor)).rgb,
diffuseColor.a);
gl_FragColor = outColor;
}
</script>
How do I draw a filled circle with openGl on iPhone ?
I've found many solutions but none of them work. Probably because there are many ways to do it. But what's the method with shortest code ?
For a truly smooth circle, you're going to want a custom fragment shader. For example, the following vertex shader:
attribute vec4 position;
attribute vec4 inputTextureCoordinate;
varying vec2 textureCoordinate;
void main()
{
gl_Position = position;
textureCoordinate = inputTextureCoordinate.xy;
}
and fragment shader:
varying highp vec2 textureCoordinate;
const highp vec2 center = vec2(0.5, 0.5);
const highp float radius = 0.5;
void main()
{
highp float distanceFromCenter = distance(center, textureCoordinate);
lowp float checkForPresenceWithinCircle = step(distanceFromCenter, radius);
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0) * checkForPresenceWithinCircle;
}
will draw a smooth red circle within a square that you draw to the screen. You'll need to supply vertices for your square to the position attribute and coordinates that range from 0.0 to 1.0 in X and Y to the inputTextureCoordinate attribute, but this will draw a circle that's as sharp as your viewport's resolution allows and do so very quickly.
One way would be to use GL_POINTS:
glPointSize(radius);
glBegin(GL_POINTS);
glVertex2f(x,y);
glEnd();
Another alternative would be to use GL_TRIANGLE_FAN:
radius = 1.0;
glBegin(GL_TRIANGLE_FAN);
glVertex2f(x, y);
for(int angle = 1; angle <= 360; angle = angle + 1)
glVertex2f(x + sind(angle) * radius, y + cosd(angle) * radius);
glEnd();
To get the verices of a circle:
float[] verts=MakeCircle2d(1,100,0,0)
public static float[] MakeCircle2d(float rad,int points,float x,float y)//x,y ofsets
{
float[] verts=new float[points*2+2];
boolean first=true;
float fx=0;
float fy=0;
int c=0;
for (int i = 0; i < points; i++)
{
float fi = 2*Trig.PI*i/points;
float xa = rad*Trig.sin(fi + Trig.PI)+x ;
float ya = rad*Trig.cos(fi + Trig.PI)+y ;
if(first)
{
first=false;
fx=xa;
fy=ya;
}
verts[c]=xa;
verts[c+1]=ya;
c+=2;
}
verts[c]=fx;
verts[c+1]=fy;
return verts;
}
Draw it as GL10.GL_LINES if you want a empty circle
gl.glDrawArrays(GL10.GL_LINES, 0, verts.length / 2);
Or draw it as GL10.GL_TRIANGLE_FAN if you want a filled one
gl.glDrawArrays(GL10.GL_TRIANGLE_FAN, 0, verts.length / 2);
Its java but it is really easy to convert to c++/objc
Here is a super fast way using shaders... Just make a Quad with a vertex buffer and set the UV's from -1 to 1 for each corner of the quad.
The vertex buffer in floats should look like:
NOTE: this needs a index buffer too.
var verts = new float[20]
{
-1, -1, 0, -1, -1,
-1, 1, 0, -1, 1,
1, 1, 0, 1, 1,
1, -1, 0, 1, -1,
};
#VS
attribute vec3 Position0;
attribute vec2 Texcoord0;
varying vec4 Position_VSPS;
varying vec2 Texcoord_VSPS;
uniform vec2 Location;
void main()
{
vec3 loc = vec3(Position0.xy + Location, Position0.z);
gl_Position = Position_VSPS = vec4(loc, 1);
Texcoord_VSPS = loc.xy;
}
#END
#PS
varying vec4 Position_VSPS;
varying vec2 Texcoord_VSPS;
uniform vec2 Location;
void main()
{
float dis = distance(Location, Texcoord_VSPS);
if (.1 - dis < 0.0) discard;
gl_FragData[0] = vec4(0, 0, 1, 1);
}
#END