Face animation in webgl - javascript

I need some help with webgl.
I have to open the mouth of a face model (Lee Perry Smith) from code, but I don't know how to identify the correct vertexes to do it.
For my task I'm not allowed to use three.js.
I've tried to get the indexes from blender but I had no luck for some reason (it's like the identified vertexes in blender do not correspond to the son that I generated for webgl.
Does someone have any idea..?
More infos:
I've used this snippet in blender to get the indices: http://blenderscripting.blogspot.it/2011/07/getting-index-of-currently-selected.html
then went into my javascript and used this function to edit the vertexes coordinates (just to see if they were right, even though this is not the real transformation wanted):
function move_vertex(indices,x,y,z){
vertex = headObject.vertices[0];
indices.forEach(function(index){
vertex[3*index] += x;
vertex[3*index+1]+=y;
vertex[3*index+2]+=z;
});
gl.bindBuffer(gl.ARRAY_BUFFER,headObject.modelVertexBuffer[0]);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, new Float32Array(vertex));
gl.bindBuffer(gl.ARRAY_BUFFER,null);
}

There are basically unlimited ways to do this . Which one fits your situation I have no idea.
One would be to use a skinning system. Attach the mouth vertices to bones and move the bones.
Another would be to use morph targets. Basically save the mesh once with mouth open and once with mouth closed. Load both meshes in webgl, pass both to your shader and lerp between them
attribute vec4 position1; // data from mouth closed model
attribute vec4 position2; // data from mouth open model
uniform float mixAmount;
uniform mat4 worldViewProjection;
...
// compute the position to use based on the mixAmount
// 0 = close mouth
// 1 = open mouth
// 0.5 = 50% between open and closed mouth etc..
vec4 position = mix(position1, position2, mixAmount);
// use the result in the standard way
gl_Position = worldViewProjection * position;
You'd do a similar mix for normals though you'd want to normalize the result.
Most modeling packages support using morph targets inside the package. It up to the file format and the exporter whether or not that data gets exported. The easy way to just hack something together would just be to export the face twice and load 2 files with the code you have.
Another might be to use vertex colors. In your modeling program color the lip vertices a distinct color then find those vertices by color in your code.
Another would be to assign the lips a different material then use the material to find the vertices.
Some 3d modeling programs let you add meta data to vertices. That's basically a variation of the vertex colors method. You'd probably need to write your own exporter as few 3rd party formats support extra data. Even if the format could theoretically support extra data most exporters don't export it.
Similarly some 3d modeling programs let you add vertices to selections/clusters/groups which you can then reference to find the lips. Again this method probably requires your own exporter as most format don't support this data
One other really hacky way but will get the job done in a pinch. Select the lip vertices and move them 1000 units to the right. Then in your program you can find all the vertices too far to the right and subtract 1000 units from each one to put them back where they originally would have been. This might mess up your normals but you can recompute normals after.
Yet another would be to use the data you have and program an interface to highlight each vertex one at a time, write down which vertices are the mouth.
For example put a <input type="number"> on the screen. Based on the number do something with that vertex. Set a vertex color or tweak it's position, something you can do to see it. Then write down which vertices are the mouth. If you're lucky they're in some range so you only have to write down the first and last ones.
const m4 = twgl.m4;
const v3 = twgl.v3;
const gl = document.querySelector("canvas").getContext("webgl");
const vs = `
attribute vec4 a_position;
attribute vec4 a_normal;
uniform mat4 u_matrix;
varying vec4 v_color;
void main() {
// Multiply the position by the matrix.
gl_Position = u_matrix * a_position;
// Pass the normal as a color to the fragment shader.
v_color = a_normal * .5 + .5;
}
`;
const fs = `
precision mediump float;
// Passed in from the vertex shader.
varying vec4 v_color;
void main() {
gl_FragColor = v_color;
}
`;
// Yes, this sample is using TWGL (https://twgljs.org).
// You should be able to tell what it's doing from the names
// of the functions and be able to easily translate that to raw WebGL
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
a_position: HeadData.positions,
a_normal: HeadData.normals,
});
const numVertices = bufferInfo.numElements;
let vertexId = 0; // id of vertex we're inspecting
let newVertexId = 251; // id of vertex we want to inspect
// these are normals and get converted to colors in the shader
const black = new Float32Array([-1, -1, -1]);
const red = new Float32Array([ 1, -1, -1]);
const white = new Float32Array([ 1, 1, 1]);
const colors = [
black,
red,
white,
];
const numElem = document.querySelector("#number");
numElem.textContent = newVertexId;
document.querySelector("#prev").addEventListener('click', e => {
newVertexId = (newVertexId + numVertices - 1) % numVertices;
numElem.textContent = newVertexId;
});
document.querySelector("#next").addEventListener('click', e => {
newVertexId = (newVertexId + 1) % numVertices;
numElem.textContent = newVertexId;
});
let frameCount = 0;
function render(time) {
++frameCount;
twgl.resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE);
// restore old data
// for what's in bufferInfo see
// http://twgljs.org/docs/module-twgl.html#.BufferInfo
const origData = new Float32Array(
HeadData.normals.slice(vertexId * 3, (vertexId + 3) * 3));
const oldOffset = vertexId * 3 * 4; // 4 bytes per float
gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.a_normal.buffer);
gl.bufferSubData(gl.ARRAY_BUFFER, oldOffset, origData);
// set new vertex to a color
const newOffset = newVertexId * 3 * 4; // 4 bytes per float
gl.bufferSubData(
gl.ARRAY_BUFFER,
newOffset,
colors[(frameCount / 3 | 0) % colors.length]);
vertexId = newVertexId;
const fov = 45 * Math.PI / 180;
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const zNear = 0.1;
const zFar = 50;
const projection = m4.perspective(fov, aspect, zNear, zFar);
const eye = [0, 0, 25];
const target = [0, 0, 0];
const up = [0, 1, 0];
const camera = m4.lookAt(eye, target, up);
const view = m4.inverse(camera);
const viewProjection = m4.multiply(projection, view);
const world = m4.identity();
const worldViewProjection = m4.multiply(viewProjection, world);
gl.useProgram(programInfo.program);
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
twgl.setUniforms(programInfo, {
u_matrix: worldViewProjection,
});
gl.drawArrays(gl.TRIANGLES, 0, numVertices);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
.ui {
position: absolute;
left: 1em;
top: 1em;
background: rgba(0,0,0,0.9);
padding: 1em;
font-size: large;
color: white;
font-family: monospace;
}
#number {
display: inline-block;
text-align: center;
}
<script src="https://twgljs.org/dist/2.x/twgl-full.min.js"></script>
<script src="https://webglfundamentals.org/webgl/resources/headdata.js"></script>
<canvas></canvas>
<div class="ui">
<button id="prev">⬅</button>
<span>vert ndx:</span><span id="number"></span>
<button id="next">➡</button>
</div>

Related

generating a texture to pull values from during fragment shading yields blank screen for correct width and height

I would like to create a texture in code consisting of an array of RGBA color values and use those values to determine the colors of tiles that I'm generating in a fragment shader. I got the idea, and much of the code to do this from the top solution provided to this SO question: Index expression must be constant - WebGL/GLSL error
However, if I create the texture using the height and width that correspond to my color array, I don't see anything render to the canvas. If I hardcode different values, I sometimes get an image, but that image doesn't place the tile colors in the desired positions, of course, and they move around as I change my viewPos variables.
From trial and error testing with a handful of handpicked values, it seems that I MIGHT only be getting an image when gl.texImage2D() receives a height and a width equal to a power of 2, though I don't see anything about this in documentation. 32 was the largest width I could produce an image with, and 16 was the largest height I could produce an image with. 1, 2, 4, and 8 also work. (the texture size should be 27 by 20 for the window size I'm testing with)
Note that the fragment shader still receives the uTileColorSampSize vector that relates to the size of the color array. I only need the gl.texImage2D() width and height values to be hardcoded to produce an image. In fact, every value i've tried for the uniform has produced an image, though each with different tile color patterns.
I've included a slightly simplified version of my Gfx class (the original is kinda messy, and includes a lot of stuff not relevant to this issue) below. I'd imagine the problem is above like 186 or so, but I've included a few additional functions below that in case those happen to be relevant.
class Gfx {
constructor() {
this.canvas = document.getElementById("canvas");
this.gl = canvas.getContext("webgl");
//viewPos changes as you drag your cursor across the canvas
this.x_viewPos = 0;
this.y_viewPos = 0;
}
init() {
this.resizeCanvas(window.innerWidth, window.innerHeight);
const vsSource = `
attribute vec4 aVertPos;
uniform mat4 uMVMat;
uniform mat4 uProjMat;
void main() {
gl_Position = uProjMat * uMVMat * aVertPos;
}
`;
//my tiles get drawn in the frag shader below
const fsSource = `
precision mediump float;
uniform vec2 uViewPos;
uniform vec2 uTileColorSampSize;
uniform sampler2D uTileColorSamp;
void main() {
//tile width and height are both 33px including a 1px border
const float lineThickness = (1.0/33.0);
//gridMult components will either be 0.0 or 1.0. This is used to place the grid lines
vec2 gridMult = vec2(
ceil(max(0.0, fract((gl_FragCoord.x-uViewPos.x)/33.0) - lineThickness)),
ceil(max(0.0, fract((gl_FragCoord.y-uViewPos.y)/33.0) - lineThickness))
);
//tileIndex is used to pull color data from the sampler texture
//add 0.5 due to pixel coords being off in gl
vec2 tileIndex = vec2(
floor((gl_FragCoord.x-uViewPos.x)/33.0) + 0.5,
floor((gl_FragCoord.y-uViewPos.y)/33.0) + 0.5
);
//divide by samp size as tex coords are 0.0 to 1.0
vec4 tileColor = texture2D(uTileColorSamp, vec2(
tileIndex.x/uTileColorSampSize.x,
tileIndex.y/uTileColorSampSize.y
));
gl_FragColor = vec4(
tileColor.x * gridMult.x * gridMult.y,
tileColor.y * gridMult.x * gridMult.y,
tileColor.z * gridMult.x * gridMult.y,
1.0 //the 4th rgba in our sampler is always 1.0 anyway
);
}
`;
const shader = this.buildShader(vsSource, fsSource);
this.programInfo = {
program: shader,
attribLocs: {
vertexPosition: this.gl.getAttribLocation(shader, 'aVertPos')
},
uniformLocs: {
projMat: this.gl.getUniformLocation(shader, 'uProjMat'),
MVMat: this.gl.getUniformLocation(shader, 'uMVMat'),
viewPos: this.gl.getUniformLocation(shader, 'uViewPos'),
tileColorSamp: this.gl.getUniformLocation(shader, 'uTileColorSamp'),
tileColorSampSize: this.gl.getUniformLocation(shader, 'uTileColorSampSize')
}
};
const buffers = this.initBuffers();
//check and enable OES_texture_float to allow us to create our sampler tex
if (!this.gl.getExtension("OES_texture_float")) {
alert("Sorry, your browser/GPU/driver doesn't support floating point textures");
}
this.gl.clearColor(0.0, 0.0, 0.15, 1.0);
this.gl.clearDepth(1.0);
this.gl.enable(this.gl.DEPTH_TEST);
this.gl.depthFunc(this.gl.LEQUAL);
const FOV = 45 * Math.PI / 180; // in radians
const aspect = this.gl.canvas.width / this.gl.canvas.height;
this.projMat = glMatrix.mat4.create();
glMatrix.mat4.perspective(this.projMat, FOV, aspect, 0.0, 100.0);
this.MVMat = glMatrix.mat4.create();
glMatrix.mat4.translate(this.MVMat, this.MVMat, [-0.0, -0.0, -1.0]);
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffers.position);
this.gl.vertexAttribPointer(this.programInfo.attribLocs.vertPos, 2, this.gl.FLOAT, false, 0, 0);
this.gl.enableVertexAttribArray(this.programInfo.attribLocs.vertPos);
this.glDraw();
}
//glDraw() gets called once above, as well as in every frame of my render loop
//(not included here as I have it in a seperate Timing class)
glDraw() {
this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
this.gl.useProgram(this.programInfo.program);
//X and Y TILE_COUNTs varrified to correspond to colorArray size in testing
//(colorArray.length = rgbaLength * X_TILE_COUNT * Y_TILE_COUNT)
//(colorArray.length = rgbaLength * widthInTiles * heightInTiles)
//(colorArray.length = 4 * 27 * 20)
let x_tileColorSampSize = X_TILE_COUNT;
let y_tileColorSampSize = Y_TILE_COUNT;
//getTileColorArray() produces a flat array of floats between 0.0and 1.0
//equal in length to rgbaLength * X_TILE_COUNT * Y_TILE_COUNT
//every 4th value is 1.0, representing tile alpha
let colorArray = this.getTileColorArray();
let colorTex = this.colorMapTexFromArray(
x_tileColorSampSize,
y_tileColorSampSize,
colorArray
);
//SO solution said to use anyting between 0 and 15 for texUnit, they used 3
//I imagine this is just an arbitrary location in memory to hold a texture
let texUnit = 3;
this.gl.activeTexture(this.gl.TEXTURE0 + texUnit);
this.gl.bindTexture(this.gl.TEXTURE_2D, colorTex);
this.gl.uniform1i(
this.programInfo.uniformLocs.tileColorSamp,
texUnit
);
this.gl.uniform2fv(
this.programInfo.uniformLocs.tileColorSampSize,
[x_tileColorSampSize, y_tileColorSampSize]
);
this.gl.uniform2fv(
this.programInfo.uniformLocs.viewPos,
[-this.x_viewPos, this.y_viewPos] //these change as you drag your cursor across the canvas
);
this.gl.uniformMatrix4fv(
this.programInfo.uniformLocs.projMat,
false,
this.projMat
);
this.gl.uniformMatrix4fv(
this.programInfo.uniformLocs.MVMat,
false,
this.MVMat
);
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
}
colorMapTexFromArray(width, height, colorArray) {
let float32Arr = Float32Array.from(colorArray);
let oldActive = this.gl.getParameter(this.gl.ACTIVE_TEXTURE);
//SO solution said "working register 31, thanks", next to next line
//not sure what that means but I think they're just looking for any
//arbitrary place to store the texture?
this.gl.activeTexture(this.gl.TEXTURE15);
var texture = this.gl.createTexture();
this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
this.gl.texImage2D(
this.gl.TEXTURE_2D, 0, this.gl.RGBA,
//if I replace width and height with certain magic numbers
//like 4 or 8 (all the way up to 32 for width and 16 for height)
//I will see colored tiles, though obviously they don't map correctly.
//I THINK I've only seen it work with a widths and heights that are
//a power of 2... could the issue be that I need my texture to have
//width and height equal to a power of 2?
width, height, 0,
this.gl.RGBA, this.gl.FLOAT, float32Arr
);
//use gl.NEAREST to prevent gl from blurring texture
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
this.gl.bindTexture(this.gl.TEXTURE_2D, null);
this.gl.activeTexture(oldActive);
return texture;
}
//I don't think the issue would be in the functions below, but I included them anyway
resizeCanvas(baseWidth, baseHeight) {
let widthMod = 0;
let heightMod = 0;
//...some math is done here to account for some DOM elements that consume window space...
this.canvas.width = baseWidth + widthMod;
this.canvas.height = baseHeight + heightMod;
this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
}
initBuffers() {
const posBuff = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, posBuff);
const positions = [
-1.0, 1.0,
1.0, 1.0,
-1.0, -1.0,
1.0, -1.0,
];
this.gl.bufferData(
this.gl.ARRAY_BUFFER,
new Float32Array(positions),
this.gl.STATIC_DRAW
);
return {
position: posBuff
};
}
buildShader(vsSource, fsSource) {
const vertShader = this.loadShader(this.gl.VERTEX_SHADER, vsSource);
const fragShader = this.loadShader(this.gl.FRAGMENT_SHADER, fsSource);
const shaderProg = this.gl.createProgram();
this.gl.attachShader(shaderProg, vertShader);
this.gl.attachShader(shaderProg, fragShader);
this.gl.linkProgram(shaderProg);
if (!this.gl.getProgramParameter(shaderProg, this.gl.LINK_STATUS)) {
console.error('Unable to initialize the shader program: ' + gl.getProgramInfoLog(shaderProg));
return null;
}
return shaderProg;
}
loadShader(type, source) {
const shader = this.gl.createShader(type);
this.gl.shaderSource(shader, source);
this.gl.compileShader(shader);
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
console.error('An error occurred compiling the shaders: ' + this.gl.getShaderInfoLog(shader));
this.gl.deleteShader(shader);
return null;
}
return shader;
}
//getTileColorArray as it appears in my code, in case you want to take a peak.
//every tileGrid[i][j] has a color, which is an array of 4 values between 0.0 and 1.0
//the fourth (last) value in tileGrid[i][j].color is always 1.0
getTileColorArray() {
let i_min = Math.max(0, Math.floor(this.x_pxPosToTilePos(this.x_viewPos)));
let i_max = Math.min(GLOBAL.map.worldWidth-1, i_min + Math.ceil(this.x_pxPosToTilePos(this.canvas.width)) + 1);
let j_min = Math.max(0, Math.floor(this.y_pxPosToTilePos(this.y_viewPos)));
let j_max = Math.min(GLOBAL.map.worldHeight-1, j_min + Math.ceil(this.y_pxPosToTilePos(this.canvas.height)) + 1);
let colorArray = [];
for (let i=i_min; i <= i_max; i++) {
for (let j=j_min; j <= j_max; j++) {
colorArray = colorArray.concat(GLOBAL.map.tileGrid[i][j].color);
}
}
return colorArray;
}
}
I've also included a pastebin of my full unaltered Gfx class in case you would like to look at that as well: https://pastebin.com/f0erR9qG
And a pastebin of my simplified code for the line numbers: https://pastebin.com/iB1pUZJa
WebGL 1.0 does not support texture wrapping on textures with non-power of two dimensions. There are two ways to solve this issue, one is to buffer the texture with enough extra data to make it have power of two dimensions, and the other solution it to simply turn off texture wrapping, like so:
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
I'm still getting strange behavior in my frag shader, but its at least showing tiles now. I think the additional strange behavior is just a result of my shader algorithm not matching what I have envisioned.

Change values of Phong Shader with sliders

I am trying to implement a 3D scene with WebGL and Javascript. The final scene is supposed to show a cuboid with smaller cuboids, pyramids and spheres on all sides. The smaller spheres have to rotate with the big cuboid. I implemented Phong Shading, this works fine. Now I want to change the values of shininess, lightPos, and lightIntensity with three sliders on the right of my canvas that displays the scene. The slider for shininess is apparently not working and I'm even more struggeling with the other two sliders, as lightPos and lightIntensity are vec3 elements that are constants. The code for the three variables looks like this:
const vec3 lightPos = vec3(1.0,-1.0,1.0);
float shininess = 16.0;
const vec3 lightIntensity = vec3(1.0, 1.0, 1.0);
At the moment the slider for shininess looks like this:
<input id="shininess" type="range" min="1" max="50"></input>
var shininessElement = document.getElementById("shininess");
shininessElement.onchange = function(){
shininess = shininessElement.value;
window.requestAnimationFrame(animate);
I'm pretty sure that I did something terribly wrong but a research didn't lead to any result and I've no idea what to do next, so I'd really appreciate your help.
If you need the complete code, please let me know.
You probably should read some other tutorials on WebGL. In particular you can't set shininess unless you make it a uniform, then look up the uniform's location and set it with gl.uniform???.
Here's simple example of using a slider to set a value and then sending that value to a shader by setting a uniform variable in the shader.
const gl = document.querySelector("canvas").getContext('webgl');
const vs = `
void main() {
gl_Position = vec4(0, 0, 0, 1);
gl_PointSize = 100.0;
}
`;
const fs = `
precision mediump float;
uniform float shininess;
void main() {
gl_FragColor = vec4(shininess, 0, 0, 1);
}
`;
// compiles shaders, links program
const prg = twgl.createProgram(gl, [vs, fs]);
const shininessLocation = gl.getUniformLocation(prg, "shininess");
let shininess = .5;
draw();
function draw() {
gl.useProgram(prg);
gl.uniform1f(shininessLocation, shininess);
gl.drawArrays(gl.POINTS, 0, 1);
}
document.querySelector("input").addEventListener('input', (e) => {
shininess = e.target.value / 100;
draw();
});
<script src="https://twgljs.org/dist/3.x/twgl.min.js"></script>
<canvas></canvas>
<input type="range" min="0" max="100" value="50" />

Three js Shader Material modify depth buffer

In Three js, I'm using a vertex shader to animate a large geometry.
I've also set up a Depth of Field effect on the output. The problem is that the Depth of Field effect doesn't seem to know about the changed positioning created in my vertex shader. It is responding as if the geometry is in the original position.
How can I update the depth information in my shader/material so that the DOF works correctly? THREE.Material has a depthWrite property, but it doesn't seem to be that...
My depth of field pass works like this:
renderer.render( this.originalScene, this.originalCamera, this.rtTextureColor, true );
this.originalScene.overrideMaterial = this.material_depth;
renderer.render( this.originalScene, this.originalCamera, this.rtTextureDepth, true );
rtTextureColor and rtTextureDepth are both WebGLRenderTargets. For some reason rtTextureColor is correct, but rtTextureDepth is not
here is my vertex shader:
int sphereIndex = int(floor(position.x/10.));
float displacementVal = displacement[sphereIndex].w;
vec3 rotationDisplacement = displacement[sphereIndex].xyz;
vNormal = normalize( normalMatrix * normal );
vec3 vNormel = normalize( normalMatrix * viewVector );
intensity = abs(pow( c - dot(vNormal, vNormel), p ));
float xVal = (displacementVal*orbitMultiplier) * sin(timeValue*rotationDisplacement.x);
float yVal = (displacementVal*orbitMultiplier) * cos(timeValue*rotationDisplacement.y);
float zVal = 0;
vec3 rotatePosition = vec3(xVal,yVal,zVal);
vec3 newPos = (position-vec3((10.*floor(position.x/10.)),0,0))+rotatePosition;
vec4 mvPosition;
mvPosition = (modelViewMatrix * vec4(newPos,1));
vViewPosition = -mvPosition.xyz;
vec4 p = projectionMatrix * mvPosition;
gl_Position = p;
Because you set the scene override material (this.originalScene.overrideMaterial = this.material_depth) before rendering into this.rtTextureDepth, the renderer doesn't use your custom vertex shader. The scene override material is a THREE.MeshDepthMaterial, which includes its own vertex shader.
One thing to try is writing a THREE.ShaderMaterial that works like THREE.MeshDepthMaterial but uses your custom vertex shader. Modifying built-in shaders isn't straightforward, but I would start from something like this:
var depthShader = THREE.ShaderLib['depth'];
var uniforms = THREE.UniformsUtils.clone(depthShader.uniforms);
var material = new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: /* your custom vertex shader */
fragmentShader: depthShader.fragmentShader
});
You'll have to add the uniforms for your custom vertex shader and also set the uniforms for the built-in depth shaders; search WebGLRenderer.js in the three.js source for MeshDepthMaterial.

Change color in middle of circle

I'm new to WebGL and I'm trying to create a black ring in the middle of this green circle without making additional circles. I believe I can do this by making the normal of those triangles go the other way but I'm not sure exactly how to do this. My friend suggested changing the texture coordinates but I don't really understand how this would help. Can anyone shine some light on these ideas and possible intuition?
_______HTML File__________
<!DOCTYPE html>
<html>
<head>
<script id="vertex-shader" type="x-shader/x-vertex">
attribute vec4 vPosition;
void
main()
{
gl_Position = vPosition;
}
</script>
<script id="fragment-shader" type="x-shader/x-fragment">
precision mediump float;
void
main()
{
gl_FragColor = vec4( 0.0, 1.0, 0.0, 1.0 );
}
</script>
<script type="text/javascript" src="../Common/webgl-utils.js"></script>
<script type="text/javascript" src="../Common/initShaders.js"></script>
<script type="text/javascript" src="../Common/MV.js"></script>
<script type="text/javascript" src="Circle.js"></script>
</head>
<body>
<canvas id="gl-canvas" width="512" height="512">
Oops ... your browser doesn't support the HTML5 canvas element
</canvas>
</body>
</html>
_____Javascript File______
var gl;
var points;
window.onload = function init()
{
var canvas = document.getElementById( "gl-canvas" );
gl = WebGLUtils.setupWebGL( canvas );
if ( !gl ) { alert( "WebGL isn't available" ); }
// The Vertices
var pi = 3.14159;
var x = 2*pi/100;
var y = 2*pi/100;
var r = 0.9;
points = [ vec2(0.0, 0.0) ]; //establish origin
//for loop to push points
for(var i = 0; i < 100; i++){
points.push(vec2(r*Math.cos(x*i), r*Math.sin(y*i)));
points.push(vec2(r*Math.cos(x*(i+1)), r*Math.sin(y*(i+1))));
}
//
// Configure WebGL
//
gl.viewport( 0, 0, canvas.width, canvas.height );
gl.clearColor( 0.3, 0.3, 0.3, 1.0 );
// Load shaders and initialize attribute buffers
var program = initShaders( gl, "vertex-shader", "fragment-shader" );
gl.useProgram( program );
// Load the data into the GPU
var bufferId = gl.createBuffer();
gl.bindBuffer( gl.ARRAY_BUFFER, bufferId );
gl.bufferData( gl.ARRAY_BUFFER, flatten(points), gl.STATIC_DRAW );
// Associate out shader variables with our data buffer
var vPosition = gl.getAttribLocation( program, "vPosition" );
gl.vertexAttribPointer( vPosition, 2, gl.FLOAT, false, 0, 0 );
gl.enableVertexAttribArray( vPosition );
render();
};
function render() {
gl.clear( gl.COLOR_BUFFER_BIT );
gl.drawArrays( gl.TRIANGLE_FAN, 0, points.length );
}
I assembled some part of your task as you requested. I tried to not change your code much, so you can understand all changes I have done. First small show:
Triangle with your code
Circle made out of 3 points
You made circle out of 100 points (vertices). Now you want to make another shape inside. It means use another 100 points, which is probably what you don't want to do. Instead of this, you would like to use normals. But from the point of view of shaders (which are responsible for drawing), normals, vertices and other things like texture coordinates are just data and you are the one who decides, if data means vertices, normals, texture coordinates or anything else.
If I understand good, you want to customize your object without adding too much additional data. I don't think normals or textures can help you.
There are few problems you will have to face with texture ...
First is, if circle will be too big (close to you), then it will be not that nice with just 100 points.
If circle will be too small (far from you), but there will be a lot circles, you will use too many points for nothing which will lower performance.
If you use texture for black ring inside, it will be fuzzy if you will be closer.
And if you use too large texture for a lot of small circles, it will again lower performance.
... and normals are used to do light reflection like this.
Way I think about the problem. You can define circle with just few params, radius and center. With webgl, you can draw only triangles (and points). But you can for example customize shader to draw inscribed circle in each triangle.
So I defined just radius and center:
var r = 0.9;
var middle = vec2(0.0, 0.0);
Then I generate 3 points of triangle around the circle (circle is inscribed circle of this new triangle):
function buildCircle(center, r) {
var points = [];
points.push(vec2((r * TRI_HEIGHT_MOD * Math.cos(0 * DEG_TO_RAD)) + center[0], (r * TRI_HEIGHT_MOD * Math.sin(0 * DEG_TO_RAD)) + center[1]));
points.push(vec2((r * TRI_HEIGHT_MOD * Math.cos(120 * DEG_TO_RAD)) + center[0], (r * TRI_HEIGHT_MOD * Math.sin(120 * DEG_TO_RAD) + center[1])));
points.push(vec2((r * TRI_HEIGHT_MOD * Math.cos(240 * DEG_TO_RAD)) + center[0], (r * TRI_HEIGHT_MOD * Math.sin(240 * DEG_TO_RAD)) + center[1]));
vertexPositions = points;
}
Then I pass middle, radius and triangle to my shader:
var vPosition = gl.getAttribLocation(program, "vPosition");
gl.vertexAttribPointer(vPosition, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(vPosition);
program.middle = gl.getUniformLocation(program, "middle");
gl.uniform2f(program.middle, middle[0], middle[1]);
program.r = gl.getUniformLocation(program, "r");
gl.uniform1f(program.r, r);
And then I just render it with same as you do, except I need to allow alpha drawing, because some parts of triangle will be invisible, so it will look as circle:
gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
gl.enable(gl.BLEND);
gl.disable(gl.DEPTH_TEST);
Ok now shaders.
There are few things you really need to know to continue, so please read about it here: http://webglfundamentals.org/webgl/lessons/webgl-how-it-works.html
My vertex shader is same as yours, except I need to pass interpolated vertex position to fragment shader:
varying vec4 pos;
...
void main() {
pos = vPosition;
My fragment shader needs to do only one thing and it is to decide, if pixel is in the circle or not. Simple equation:
If the left side is smaller then the right side, then pixel is inside the circle. If not, then it is outside, so invisible:
float inside = pow(pos.r - middle.r, 2.0) + pow(pos.g - middle.g, 2.0);
if (inside < pow(r, 2.0)) {
gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0);
} else {
gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
}
End
So now you might know how to make a circle just from few points. You can use similar way to draw a ring inside. Then you can draw thousands of them in any distance and make them move. Program will be still fast and shapes will be as sharp as possible.
Just one last thing. Usually you dont simplify shapes like that, but sometimes you might. Good example is Bézier curve which might help you to do crazy sharp shapes with just few points. But it all matters what would you like to do. One technique can't solve all problems and you have to keep looking for more solutions.
EDIT 1: "What is var middle = vec2(0.0, 0.0)? I meam, vec2?"
There are 3 other scripts in this question that I replicated in my solution (in jsfiddle on the left: External Resources). It wasnt part of this question, but it was easy to find theirs origin:
<script type="text/javascript" src="../Common/webgl-utils.js"></script>
<script type="text/javascript" src="../Common/initShaders.js"></script>
<script type="text/javascript" src="../Common/MV.js"></script>
MV.js is some supply javascript with basic math... or algebraic constructs like vectors and matrices. vec2 is function that returns array with length 2. So var middle = [0.0, 0.0]; is exactly the same thing. This is not part of native javascript, so you need some library for it (you don't need it, but it is very useful). I use glmatrix.
On the other hand in shaders, vectors and matrices are native. Find it out on your own in chapter 4.1 Basic Types.

Preventing Canvas Clear when Resizing Window

I'm trying to create a simple app that draws rectangles within the Canvas tag. I've got the Canvas resizing to fullscreen, but whenever I resize the viewport, Canvas clears. I'm trying to prevent it from clearing and just keeping the content that's within it. Any ideas?
http://mediajux.com/experiments/canvas/drawing/
/*
* This is the primary class used for the application
* #author Alvin Crespo
*/
var app = (function(){
var domBod = document.body;
var canvas = null;
var canvasWidth = null;
var canvasHeight = null;
return {
//Runs after the DOM has achieved an onreadystatechange of "complete"
initApplication: function()
{
//setup envrionment variables
canvas = document.getElementById('canvas') || null;
//we need to resize the canvas at the start of the app to be the full window
this.windowResized();
//only set the canvas height and width if it is not false/null
if(canvas)
{
canvasWidth = canvas.offsetWidth;
canvasHeight = canvas.offsetHeight;
}
//add window events
window.onresize = this.windowResized;
circles.canvas = canvas;
circles.canvasWidth = canvasWidth;
circles.canvasHeight = canvasHeight;
circles.generateCircles(10);
setInterval(function(){
circles.animateCircles();
}, 50);
},
/**
* Executes Resizing procedures on the canvas element
*/
windowResized: function()
{
(this.domBod === null) ? 'true' : 'false';
try{
console.log(canvas);
canvas.setAttribute('width', document.body.clientWidth);
canvas.setAttribute('height', document.body.clientHeight);
}catch(e) {
console.log(e.name + " :: " + e.message);
}
},
/**
* Returns the canvas element
* #returns canvas
*/
getCanvas: function()
{
return canvas;
}
};
})();
Setting the canvas width attribute will clear the canvas.
If you resize the style width (e.g. canvas.style.visibility), it will scale (usually not in such a pretty way).
If you want to make the canvas bigger but keep the elements in it as they are, I would suggest storing the canvas as an image -- e.g. call the toDataURL method to get the image, then draw that to the resized canvas with drawImage().
Here's how I solved this problem with JS3.
Internally, I store the main canvas and context as _canvas and _context respectively.
function resize(w, h){
// create a temporary canvas obj to cache the pixel data //
var temp_cnvs = document.createElement('canvas');
var temp_cntx = temp_cnvs.getContext('2d');
// set it to the new width & height and draw the current canvas data into it //
temp_cnvs.width = w;
temp_cnvs.height = h;
temp_cntx.fillStyle = _background; // the original canvas's background color
temp_cntx.fillRect(0, 0, w, h);
temp_cntx.drawImage(_canvas, 0, 0);
// resize & clear the original canvas and copy back in the cached pixel data //
_canvas.width = w;
_canvas.height = h;
_context.drawImage(temp_cnvs, 0, 0);
}
JS3 also provides an autoSize flag which will automatically resize your canvas to the browser window or the dimensions of its parent div.
Set canvas size with style (css) and do not change attributes.
After resize to fullscreen
Canvas will be resized and not cleared, but will be scaled, than to prevent scale - you need rescale after resize, here is math:
var oldWidth = $("canvas").css("width").replace("px", "");
var oldHeight = $("canvas").css("height").replace("px", "");
$("canvas").css({
"width" : window.innerWidth,
"height": window.innerHeight
});
var ratio1 = oldWidth/window.innerWidth;
var ratio2 = oldHeight/window.innerHeight;
canvas.ctx.scale(ratio1, ratio2);
Please note, that I made copy paste from my code and do some changes with ids and vars names for fast, so could have some small mistkaes like "canvas.ctx" or dom calls.
one way I solved this was:
const canvas = document.getElementById('ctx')
const ctx = canvas.getContext('2d')
var W = canvas.width, H = canvas.height
function resize() {
let temp = ctx.getImageData(0,0,W,H)
ctx.canvas.width = window.innerWidth - 99;
ctx.canvas.height = window.innerHeight - 99;
W = canvas.width, H = canvas.height
ctx.putImageData(temp,0,0)
}
the only issue is that on zooming back out you lose the data that was outside the canvas
I believe you have implement a listener for screen resize and redraw the canvas content when that listener fires.
I had the same issue with my canvas and I have resolved that issue. Please refer the below code. I hope you will resolved the issue using this.
Note : Set alwaysDraw: true in the parameters
HTML
<div id="top-wraper">
<div id="canvas"></div>
</div>
<!-- div used to create our plane -->
<div class="plane" data-vs-id="plane-vs" data-fs-id="plane-fs">
<!-- image that will be used as a texture by our plane -->
<img src="texture-img.png" alt="Leo Music - Music from the heart of a Lion"/>
</div>
JS
<script>
function loadAnimation() {
// set up our WebGL context and append the canvas to our wrapper
var webGLCurtain = new Curtains("canvas");
webGLCurtain.width = 50;
// if there's any error during init, we're going to catch it here
webGLCurtain.onError(function () {
// we will add a class to the document body to display original images
document.body.classList.add("no-curtains");
});
// get our plane element
var planeElement = document.getElementsByClassName("plane")[0];
// set our initial parameters (basic uniforms)
var params = {
vertexShaderID: "plane-vs", // our vertex shader ID
fragmentShaderID: "plane-fs", // our framgent shader ID
alwaysDraw: true,
//crossOrigin: "", // codepen specific
uniforms: {
time: {
name: "uTime", // uniform name that will be passed to our shaders
type: "1f", // this means our uniform is a float
value: 0,
},
}
}
// create our plane mesh
var plane = webGLCurtain.addPlane(planeElement, params);
// if our plane has been successfully created
// we use the onRender method of our plane fired at each requestAnimationFrame call
plane && plane.onRender(function () {
plane.uniforms.time.value++; // update our time uniform value
});
}
window.onload = function () {
loadAnimation();
}
</script>
<script id="plane-vs" type="x-shader/x-vertex">
#ifdef GL_ES
precision mediump float;
#endif
// those are the mandatory attributes that the lib sets
attribute vec3 aVertexPosition;
attribute vec2 aTextureCoord;
// those are mandatory uniforms that the lib sets and that contain our model view and projection matrix
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;
// our texture matrix uniform (this is the lib default name, but it could be changed)
uniform mat4 uTextureMatrix0;
// if you want to pass your vertex and texture coords to the fragment shader
varying vec3 vVertexPosition;
varying vec2 vTextureCoord;
void main() {
vec3 vertexPosition = aVertexPosition;
gl_Position = uPMatrix * uMVMatrix * vec4(vertexPosition, 1.0);
// set the varyings
// thanks to the texture matrix we will be able to calculate accurate texture coords
// so that our texture will always fit our plane without being distorted
vTextureCoord = (uTextureMatrix0 * vec4(aTextureCoord, 0.0, 1.0)).xy;
vVertexPosition = vertexPosition;
}
</script>
<script id="plane-fs" type="x-shader/x-fragment">
#ifdef GL_ES
precision mediump float;
#endif
// get our varyings
varying vec3 vVertexPosition;
varying vec2 vTextureCoord;
// the uniform we declared inside our javascript
uniform float uTime;
// our texture sampler (default name, to use a different name please refer to the documentation)
uniform sampler2D uSampler0;
void main() {
// get our texture coords
vec2 textureCoord = vTextureCoord;
// displace our pixels along both axis based on our time uniform and texture UVs
// this will create a kind of water surface effect
// try to comment a line or change the constants to see how it changes the effect
// reminder : textures coords are ranging from 0.0 to 1.0 on both axis
// const float PI = 3.141592;
const float PI = 2.0;
textureCoord.x += (
sin(textureCoord.x * 10.0 + ((uTime * (PI / 3.0)) * 0.031))
+ sin(textureCoord.y * 10.0 + ((uTime * (PI / 2.489)) * 0.017))
) * 0.0075;
textureCoord.y += (
sin(textureCoord.y * 20.0 + ((uTime * (PI / 2.023)) * 0.00))
+ sin(textureCoord.x * 20.0 + ((uTime * (PI / 3.1254)) * 0.0))
) * 0.0125;
gl_FragColor = texture2D(uSampler0, textureCoord);
}
</script>
<script src="https://www.curtainsjs.com/build/curtains.min.js" ></script>

Categories