As shown below, all I can think of is to describe all the points on a circle and then use triangulation to draw a ring with width
I can also think of a way to use overlay. First draw a circle and then draw a circle with a smaller radius
const TAU_SEGMENTS = 360;
const TAU = Math.PI * 2;
export function arc(x0: number, y0: number, radius: number, startAng = 0, endAng = Math.PI * 2) {
const ang = Math.min(TAU, endAng - startAng);
const ret = ang === TAU ? [] : [x0, y0];
const segments = Math.round(TAU_SEGMENTS * ang / TAU);
for(let i = 0; i <= segments; i++) {
const x = x0 + radius * Math.cos(startAng + ang * i / segments);
const y = y0 + radius * Math.sin(startAng + ang * i / segments);
ret.push(x, y);
}
return ret;
}
const gl = container.current.getContext("webgl2");
const program = initProgram(gl);
const position = new Float32Array(arc(0, 0, 1).concat(...arc(0, 0, 0.9)));
let buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, position, gl.STATIC_DRAW);
let gl_position = gl.getAttribLocation(program, "position");
gl.vertexAttribPointer(gl_position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(gl_position);
gl.drawArrays(gl.LINE_STRIP, 0, position.length / 2);
The final effect of the code I wrote is as follows. May I ask how I should modify it to become the same as the above picture
You have to add a color attributes for the vertices and you have to draw a gl.TRIANGLE_STRIP primitive instead of a gl.LINE_STRIP primitive.
The color can be calculated from the angle. Map the angle from the range [0, PI] to the range [0, 1] and use the formula for the hue value from the HSL and HSV color space:
function HUEtoRGB(hue) {
return [
Math.min(1, Math.max(0, Math.abs(hue * 6.0 - 3.0) - 1.0)),
Math.min(1, Math.max(0, 2.0 - Math.abs(hue * 6.0 - 2.0))),
Math.min(1, Math.max(0, 2.0 - Math.abs(hue * 6.0 - 4.0)))
];
}
Create vertices in pairs for the inner and outer arcs with the corresponding color attribute:
const TAU_SEGMENTS = 360;
const TAU = Math.PI * 2;
function arc(x0, y0, innerRadius, outerRadius, startAng = 0, endAng = Math.PI * 2) {
const ang = Math.min(TAU, endAng - startAng);
const position = ang === TAU ? [] : [x0, y0];
const color = []
const segments = Math.round(TAU_SEGMENTS * ang / TAU);
for(let i = 0; i <= segments; i++) {
const angle = startAng + ang * i / segments;
const x1 = x0 + innerRadius * Math.cos(angle);
const y1 = y0 + innerRadius * Math.sin(angle);
const x2 = x0 + outerRadius * Math.cos(angle);
const y2 = y0 + outerRadius * Math.sin(angle);
position.push(x1, y1, x2, y2);
let hue = (Math.PI/2 - angle) / (2 * Math.PI);
if (hue < 0) hue += 1;
const rgb = HUEtoRGB(hue);
color.push(...rgb);
color.push(...rgb);
}
return { 'position': position, 'color': color };
}
Create a shader with a color attribute and pass the color attribute from the vertex to the fragment shader:
#version 300 es
precision highp float;
in vec2 position;
in vec4 color;
out vec4 vColor;
void main()
{
vColor = color;
gl_Position = vec4(position.xy, 0.0, 1.0);
}
#version 300 es
precision highp float;
in vec4 vColor;
out vec4 fragColor;
void main()
{
fragColor = vColor;
}
Vertex specification:
const attributes = arc(0, 0, 0.6, 0.9);
position = new Float32Array(attributes.position);
color = new Float32Array(attributes.color);
let position_buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, position_buffer);
gl.bufferData(gl.ARRAY_BUFFER, position, gl.STATIC_DRAW);
let gl_position = gl.getAttribLocation(program, "position");
gl.vertexAttribPointer(gl_position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(gl_position);
let color_buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, color_buffer);
gl.bufferData(gl.ARRAY_BUFFER, color, gl.STATIC_DRAW);
let gl_color = gl.getAttribLocation(program, "color");
gl.vertexAttribPointer(gl_color, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(gl_color);
Complet and runnable example:
const canvas = document.getElementById( "ogl-canvas");
const gl = canvas.getContext("webgl2");
const program = gl.createProgram();
for (let i = 0; i < 2; ++i) {
let source = document.getElementById(i==0 ? "draw-shader-vs" : "draw-shader-fs").text;
let shaderObj = gl.createShader(i==0 ? gl.VERTEX_SHADER : gl.FRAGMENT_SHADER);
gl.shaderSource(shaderObj, source);
gl.compileShader(shaderObj);
let status = gl.getShaderParameter(shaderObj, gl.COMPILE_STATUS);
if (!status) alert(gl.getShaderInfoLog(shaderObj));
gl.attachShader(program, shaderObj);
gl.linkProgram(program);
}
status = gl.getProgramParameter(program, gl.LINK_STATUS);
if ( !status ) alert(gl.getProgramInfoLog(program));
gl.useProgram(program);
function HUEtoRGB(hue) {
return [
Math.min(1, Math.max(0, Math.abs(hue * 6.0 - 3.0) - 1.0)),
Math.min(1, Math.max(0, 2.0 - Math.abs(hue * 6.0 - 2.0))),
Math.min(1, Math.max(0, 2.0 - Math.abs(hue * 6.0 - 4.0)))
];
}
const TAU_SEGMENTS = 360;
const TAU = Math.PI * 2;
function arc(x0, y0, innerRadius, outerRadius, startAng = 0, endAng = Math.PI * 2) {
const ang = Math.min(TAU, endAng - startAng);
const position = ang === TAU ? [] : [x0, y0];
const color = []
const segments = Math.round(TAU_SEGMENTS * ang / TAU);
for(let i = 0; i <= segments; i++) {
const angle = startAng + ang * i / segments;
const x1 = x0 + innerRadius * Math.cos(angle);
const y1 = y0 + innerRadius * Math.sin(angle);
const x2 = x0 + outerRadius * Math.cos(angle);
const y2 = y0 + outerRadius * Math.sin(angle);
position.push(x1, y1, x2, y2);
let hue = (Math.PI/2 - angle) / (2 * Math.PI);
if (hue < 0) hue += 1;
const rgb = HUEtoRGB(hue);
color.push(...rgb);
color.push(...rgb);
}
return { 'position': position, 'color': color };
}
const attributes = arc(0, 0, 0.6, 0.9);
position = new Float32Array(attributes.position);
color = new Float32Array(attributes.color);
let position_buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, position_buffer);
gl.bufferData(gl.ARRAY_BUFFER, position, gl.STATIC_DRAW);
let gl_position = gl.getAttribLocation(program, "position");
gl.vertexAttribPointer(gl_position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(gl_position);
let color_buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, color_buffer);
gl.bufferData(gl.ARRAY_BUFFER, color, gl.STATIC_DRAW);
let gl_color = gl.getAttribLocation(program, "color");
gl.vertexAttribPointer(gl_color, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(gl_color);
gl.enable( gl.DEPTH_TEST );
gl.clearColor( 0.0, 0.0, 0.0, 1.0 );
//vp_size = [gl.drawingBufferWidth, gl.drawingBufferHeight];
vp_size = [window.innerWidth, window.innerHeight];
vp_size = [256, 256]
canvas.width = vp_size[0];
canvas.height = vp_size[1];
gl.viewport( 0, 0, canvas.width, canvas.height );
gl.clearColor(1, 1, 1, 1);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, attributes.position.length / 2);
<script id="draw-shader-vs" type="x-shader/x-vertex">#version 300 es
precision highp float;
in vec2 position;
in vec4 color;
out vec4 vColor;
void main()
{
vColor = color;
gl_Position = vec4(position.xy, 0.0, 1.0);
}
</script>
<script id="draw-shader-fs" type="x-shader/x-fragment">#version 300 es
precision highp float;
in vec4 vColor;
out vec4 fragColor;
void main()
{
fragColor = vColor;
}
</script>
<canvas id="ogl-canvas" style="border: none"></canvas>
I am trying to adapt a webgl code that generates firework explositions, where projectiles fade out from canvas by means of painting over with 0.1 alpha (as I understand it).
However, this technique somehow doesn't work for the explosition of white color.
You can see that white projectiles leave traces and never fully disappear, while other colors work fine (or maybe they also don't work, but I don't see the traces on black background).
Can somebody help me understand what is happening here?
// from https://codepen.io/towc/pen/oBvYEL
var gl = c.getContext('webgl2', {preserveDrawingBuffer: true})
, w = c.width = window.innerWidth
, h = c.height = window.innerHeight
, webgl = {}
, opts = {
projectileAlpha: .8,
projectileLineWidth: 2,
fireworkAngleSpan: .5,
baseFireworkVel: 3,
gravity: .02,
xFriction: .995,
baseShardVel: 1,
addedShardVel: .2,
fireworks: 1, // 1 firework for each 10x10 pixels,
baseShardsParFirework: 10,
addedShardsParFirework: 10,
shardFireworkVelMultiplier: .3
}
// updated to use WebGL2 and GL ES 3.0
// pass full color instead of just hue
// remove moving projectile, keep only the explosion
webgl.vertexShaderSource = `#version 300 es
uniform int u_mode;
uniform vec2 u_res;
in vec4 a_data;
out vec4 v_color;
vec3 c2rgb(int color_int){
int r = color_int >> 16;
int g = color_int >> 8 & 0xff;
int b = color_int & 0xff;
return vec3(r/255, g/255, b/255);
}
void clear(){
gl_Position = vec4( a_data.xy, 0, 1 );
v_color = vec4( 0, 0, 0, a_data.w );
}
void draw(){
gl_Position = vec4( vec2( 1, -1 ) * ( ( a_data.xy / u_res ) * 2. - 1. ), 0, 1 );
v_color = vec4( 1, 1, 1, a_data.w );
}
void main(){
if( u_mode == 0 )
draw();
else
clear();
}
`;
webgl.fragmentShaderSource = `#version 300 es
precision mediump float;
in vec4 v_color;
out vec4 fragColor;
void main(){
fragColor = v_color;
}
`;
webgl.vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(webgl.vertexShader, webgl.vertexShaderSource);
gl.compileShader(webgl.vertexShader);
if (!gl.getShaderParameter(webgl.vertexShader, gl.COMPILE_STATUS))
console.log(gl.getShaderInfoLog(webgl.vertexShader));
webgl.fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(webgl.fragmentShader, webgl.fragmentShaderSource);
gl.compileShader(webgl.fragmentShader);
if (!gl.getShaderParameter(webgl.fragmentShader, gl.COMPILE_STATUS))
console.log(gl.getShaderInfoLog(webgl.fragmentShader));
webgl.shaderProgram = gl.createProgram();
gl.attachShader(webgl.shaderProgram, webgl.vertexShader);
gl.attachShader(webgl.shaderProgram, webgl.fragmentShader);
gl.linkProgram(webgl.shaderProgram);
gl.useProgram(webgl.shaderProgram);
webgl.dataAttribLoc = gl.getAttribLocation(webgl.shaderProgram, 'a_data');
webgl.dataBuffer = gl.createBuffer();
gl.enableVertexAttribArray(webgl.dataAttribLoc);
gl.bindBuffer(gl.ARRAY_BUFFER, webgl.dataBuffer);
gl.vertexAttribPointer(webgl.dataAttribLoc, 4, gl.FLOAT, false, 0, 0);
webgl.resUniformLoc = gl.getUniformLocation(webgl.shaderProgram, 'u_res');
webgl.modeUniformLoc = gl.getUniformLocation(webgl.shaderProgram, 'u_mode');
gl.viewport(0, 0, w, h);
gl.uniform2f(webgl.resUniformLoc, w, h);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.enable(gl.BLEND);
gl.lineWidth(opts.projectileLineWidth);
webgl.data = [];
webgl.clear = function () {
gl.uniform1i(webgl.modeUniformLoc, 1);
let a = .1;
webgl.data = [
-1, -1, 0, 0.1,
1, -1, 0, 0.1,
-1, 1, 0, 0.1,
-1, 1, 0, 0.1,
1, -1, 0, 0.1,
1, 1, 0, 0.1
];
webgl.draw(gl.TRIANGLES);
gl.uniform1i(webgl.modeUniformLoc, 0);
webgl.data.length = 0;
}
webgl.draw = function (glType) {
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(webgl.data), gl.STATIC_DRAW);
gl.drawArrays(glType, 0, webgl.data.length / 4);
}
let fireworks = []
, tick = 0
, sins = []
, coss = []
, maxShardsParFirework = opts.baseShardsParFirework + opts.addedShardsParFirework
, tau = 6.283185307179586476925286766559;
for (let i = 0; i < maxShardsParFirework; ++i) {
sins[i] = Math.sin(tau * i / maxShardsParFirework);
coss[i] = Math.cos(tau * i / maxShardsParFirework);
}
function Firework() {
this.reset();
this.shards = [];
for (let i = 0; i < maxShardsParFirework; ++i)
this.shards.push(new Shard(this));
}
Firework.prototype.reset = function () {
let angle = -Math.PI / 2 + (Math.random() - .5) * opts.fireworkAngleSpan
, vel = opts.baseFireworkVel * Math.random();
this.mode = 0;
this.vx = vel * Math.cos(angle);
this.vy = vel * Math.sin(angle);
this.x = Math.random() * w;
this.y = Math.random() * h;
this.hue = 0;
let ph = this.hue
, px = this.x
, py = this.y;
webgl.data.push(
px, py, ph, opts.projectileAlpha * .2,
this.x, this.y, this.hue, opts.projectileAlpha * .2);
}
Firework.prototype.step = function () {
if (this.mode === 0) {
this.mode = 1;
this.shardAmount = opts.baseShardsParFirework + opts.addedShardsParFirework * Math.random() | 0;
let baseAngle = Math.random() * tau
, x = Math.cos(baseAngle)
, y = Math.sin(baseAngle)
, sin = sins[this.shardAmount]
, cos = coss[this.shardAmount];
for (let i = 0; i < this.shardAmount; ++i) {
let vel = opts.baseShardVel + opts.addedShardVel * Math.random();
this.shards[i].reset(x * vel, y * vel)
let X = x;
x = x * cos - y * sin;
y = y * cos + X * sin;
}
}
if (this.mode === 1) {
this.ph = this.hue
this.hue = 0;
let allDead = true;
for (let i = 0; i < this.shardAmount; ++i) {
let shard = this.shards[i];
if (!shard.dead) {
shard.step();
allDead = false;
}
}
if (allDead)
this.reset();
}
}
function Shard(parent) {
this.parent = parent;
}
Shard.prototype.reset = function (vx, vy) {
this.x = this.parent.x;
this.y = this.parent.y;
this.vx = this.parent.vx * opts.shardFireworkVelMultiplier + vx;
this.vy = this.parent.vy * opts.shardFireworkVelMultiplier + vy;
this.starty = this.y;
this.dead = false;
this.tick = 1;
}
Shard.prototype.step = function () {
this.tick += .05;
let px = this.x
, py = this.y;
this.x += this.vx *= opts.xFriction;
this.y += this.vy += opts.gravity;
var proportion = 1 - (this.y - this.starty) / (h - this.starty);
webgl.data.push(
px, py, this.parent.ph, opts.projectileAlpha / this.tick,
this.x, this.y, this.parent.hue, opts.projectileAlpha / this.tick);
if (this.y > h)
this.dead = true;
}
function anim() {
window.requestAnimationFrame(anim);
webgl.clear();
++tick;
if (fireworks.length < opts.fireworks)
fireworks.push(new Firework);
fireworks.map(function (firework) {
firework.step();
});
webgl.draw(gl.LINES);
}
anim();
window.addEventListener('resize', function () {
w = c.width = window.innerWidth;
h = c.height = window.innerHeight;
gl.viewport(0, 0, w, h);
gl.uniform2f(webgl.resUniformLoc, w, h);
});
body {
overflow: hidden;
margin:0;
}
#c {
position: absolute;
top: 0;
left: 0;
background-color: #111;
}
<canvas id="c"> </canvas>
Here is the demo.
// A set of utility functions for /common operations across our application
const utils = {
// Find and return a DOM element given an ID
getCanvas(id) {
const canvas = document.getElementById(id);
if (!canvas) {
console.error(`There is no canvas with id ${id} on this page.`);
return null;
}
return canvas;
},
// Given a canvas element, return the WebGL2 context
getGLContext(canvas) {
return canvas.getContext('webgl2') || console.error('WebGL2 is not available in your browser.');
},
// Given a canvas element, expand it to the size of the window
// and ensure that it automatically resizes as the window changes
autoResizeCanvas(canvas) {
const expandFullScreen = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
};
expandFullScreen();
// Resize screen when the browser has triggered the resize event
window.addEventListener('resize', expandFullScreen);
},
// Given a WebGL context and an id for a shader script,
// return a compiled shader
getShader(gl, id) {
const script = document.getElementById(id);
if (!script) {
return null;
}
const shaderString = script.text.trim();
let shader;
if (script.type === 'x-shader/x-vertex') {
shader = gl.createShader(gl.VERTEX_SHADER);
} else if (script.type === 'x-shader/x-fragment') {
shader = gl.createShader(gl.FRAGMENT_SHADER);
} else {
return null;
}
gl.shaderSource(shader, shaderString);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(shader));
return null;
}
return shader;
},
// Normalize colors from 0-255 to 0-1
normalizeColor(color) {
return color.map(c => c / 255);
},
// De-normalize colors from 0-1 to 0-255
denormalizeColor(color) {
return color.map(c => c * 255);
},
// Returns computed normals for provided vertices.
// Note: Indices have to be completely defined--NO TRIANGLE_STRIP only TRIANGLES.
calculateNormals(vs, ind) {
const
x = 0,
y = 1,
z = 2,
ns = [];
// For each vertex, initialize normal x, normal y, normal z
for (let i = 0; i < vs.length; i += 3) {
ns[i + x] = 0.0;
ns[i + y] = 0.0;
ns[i + z] = 0.0;
}
// We work on triads of vertices to calculate
for (let i = 0; i < ind.length; i += 3) {
// Normals so i = i+3 (i = indices index)
const v1 = [],
v2 = [],
normal = [];
// p2 - p1
v1[x] = vs[3 * ind[i + 2] + x] - vs[3 * ind[i + 1] + x];
v1[y] = vs[3 * ind[i + 2] + y] - vs[3 * ind[i + 1] + y];
v1[z] = vs[3 * ind[i + 2] + z] - vs[3 * ind[i + 1] + z];
// p0 - p1
v2[x] = vs[3 * ind[i] + x] - vs[3 * ind[i + 1] + x];
v2[y] = vs[3 * ind[i] + y] - vs[3 * ind[i + 1] + y];
v2[z] = vs[3 * ind[i] + z] - vs[3 * ind[i + 1] + z];
// Cross product by Sarrus Rule
normal[x] = v1[y] * v2[z] - v1[z] * v2[y];
normal[y] = v1[z] * v2[x] - v1[x] * v2[z];
normal[z] = v1[x] * v2[y] - v1[y] * v2[x];
// Update the normals of that triangle: sum of vectors
for (let j = 0; j < 3; j++) {
ns[3 * ind[i + j] + x] = ns[3 * ind[i + j] + x] + normal[x];
ns[3 * ind[i + j] + y] = ns[3 * ind[i + j] + y] + normal[y];
ns[3 * ind[i + j] + z] = ns[3 * ind[i + j] + z] + normal[z];
}
}
// Normalize the result.
// The increment here is because each vertex occurs.
for (let i = 0; i < vs.length; i += 3) {
// With an offset of 3 in the array (due to x, y, z contiguous values)
const nn = [];
nn[x] = ns[i + x];
nn[y] = ns[i + y];
nn[z] = ns[i + z];
let len = Math.sqrt((nn[x] * nn[x]) + (nn[y] * nn[y]) + (nn[z] * nn[z]));
if (len === 0) len = 1.0;
nn[x] = nn[x] / len;
nn[y] = nn[y] / len;
nn[z] = nn[z] / len;
ns[i + x] = nn[x];
ns[i + y] = nn[y];
ns[i + z] = nn[z];
}
return ns;
},
// A simpler API on top of the dat.GUI API, specifically
// designed for this book for a simpler codebase
configureControls(settings, options = {
width: 300
}) {
// Check if a gui instance is passed in or create one by default
const gui = options.gui || new dat.GUI(options);
const state = {};
const isAction = v => typeof v === 'function';
const isFolder = v =>
!isAction(v) &&
typeof v === 'object' &&
(v.value === null || v.value === undefined);
const isColor = v =>
(typeof v === 'string' && ~v.indexOf('#')) ||
(Array.isArray(v) && v.length >= 3);
Object.keys(settings).forEach(key => {
const settingValue = settings[key];
if (isAction(settingValue)) {
state[key] = settingValue;
return gui.add(state, key);
}
if (isFolder(settingValue)) {
// If it's a folder, recursively call with folder as root settings element
return utils.configureControls(settingValue, {
gui: gui.addFolder(key)
});
}
const {
value,
min,
max,
step,
options,
onChange = () => null,
} = settingValue;
// set state
state[key] = value;
let controller;
// There are many other values we can set on top of the dat.GUI
// API, but we'll only need a few for our purposes
if (options) {
controller = gui.add(state, key, options);
} else if (isColor(value)) {
controller = gui.addColor(state, key)
} else {
controller = gui.add(state, key, min, max, step)
}
controller.onChange(v => onChange(v, state))
});
},
// Calculate tangets for a given set of vertices
calculateTangents(vs, tc, ind) {
const tangents = [];
for (let i = 0; i < vs.length / 3; i++) {
tangents[i] = [0, 0, 0];
}
let
a = [0, 0, 0],
b = [0, 0, 0],
triTangent = [0, 0, 0];
for (let i = 0; i < ind.length; i += 3) {
const i0 = ind[i];
const i1 = ind[i + 1];
const i2 = ind[i + 2];
const pos0 = [vs[i0 * 3], vs[i0 * 3 + 1], vs[i0 * 3 + 2]];
const pos1 = [vs[i1 * 3], vs[i1 * 3 + 1], vs[i1 * 3 + 2]];
const pos2 = [vs[i2 * 3], vs[i2 * 3 + 1], vs[i2 * 3 + 2]];
const tex0 = [tc[i0 * 2], tc[i0 * 2 + 1]];
const tex1 = [tc[i1 * 2], tc[i1 * 2 + 1]];
const tex2 = [tc[i2 * 2], tc[i2 * 2 + 1]];
vec3.subtract(a, pos1, pos0);
vec3.subtract(b, pos2, pos0);
const c2c1b = tex1[1] - tex0[1];
const c3c1b = tex2[0] - tex0[1];
triTangent = [c3c1b * a[0] - c2c1b * b[0], c3c1b * a[1] - c2c1b * b[1], c3c1b * a[2] - c2c1b * b[2]];
vec3.add(triTangent, tangents[i0], triTangent);
vec3.add(triTangent, tangents[i1], triTangent);
vec3.add(triTangent, tangents[i2], triTangent);
}
// Normalize tangents
const ts = [];
tangents.forEach(tan => {
vec3.normalize(tan, tan);
ts.push(tan[0]);
ts.push(tan[1]);
ts.push(tan[2]);
});
return ts;
}
};
let
gl,
program,
modelViewMatrix = mat4.create(),
projectionMatrix = mat4.create(),
normalMatrix = mat4.create(),
vao,
indices,
sphereIndicesBuffer,
currentX,
currentY,
lastX,
lastY,
dragging,
rotateAxis = [0, 1, 0],
angle = 0,
shininess = 10,
clearColor = [0.9, 0.9, 0.9],
lightColor = [1, 1, 1, 1],
lightAmbient = [0.03, 0.03, 0.03, 1],
lightSpecular = [1, 1, 1, 1],
lightDirection = [-0.25, -0.25, -0.25],
materialDiffuse = [46 / 256, 99 / 256, 191 / 256, 1],
materialAmbient = [1, 1, 1, 1],
materialSpecular = [1, 1, 1, 1];
function initProgram() {
// Configure `canvas`
const canvas = utils.getCanvas('webgl-canvas');
utils.autoResizeCanvas(canvas);
// Configure `gl`
gl = utils.getGLContext(canvas);
gl.clearColor(...clearColor, 1);
gl.clearDepth(100);
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
// Shader source
const vertexShader = utils.getShader(gl, 'vertex-shader');
const fragmentShader = utils.getShader(gl, 'fragment-shader');
// Configure `program`
program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error('Could not initialize shaders');
}
gl.useProgram(program);
// Set locations onto `program` instance
program.aVertexPosition = gl.getAttribLocation(program, 'aVertexPosition');
program.aVertexNormal = gl.getAttribLocation(program, 'aVertexNormal');
program.uProjectionMatrix = gl.getUniformLocation(program, 'uProjectionMatrix');
program.uModelViewMatrix = gl.getUniformLocation(program, 'uModelViewMatrix');
program.uNormalMatrix = gl.getUniformLocation(program, 'uNormalMatrix');
program.uMaterialAmbient = gl.getUniformLocation(program, 'uMaterialAmbient');
program.uMaterialDiffuse = gl.getUniformLocation(program, 'uMaterialDiffuse');
program.uMaterialSpecular = gl.getUniformLocation(program, 'uMaterialSpecular');
program.uShininess = gl.getUniformLocation(program, 'uShininess');
program.uLightAmbient = gl.getUniformLocation(program, 'uLightAmbient');
program.uLightDiffuse = gl.getUniformLocation(program, 'uLightDiffuse');
program.uLightSpecular = gl.getUniformLocation(program, 'uLightSpecular');
program.uLightDirection = gl.getUniformLocation(program, 'uLightDirection');
canvas.onmousedown = event => onMouseDown(event);
canvas.onmouseup = event => onMouseUp(event);
canvas.onmousemove = event => onMouseMove(event);
}
// Configure lights
function initLights() {
gl.uniform4fv(program.uLightDiffuse, lightColor);
gl.uniform4fv(program.uLightAmbient, lightAmbient);
gl.uniform4fv(program.uLightSpecular, lightSpecular);
gl.uniform3fv(program.uLightDirection, lightDirection);
gl.uniform4fv(program.uMaterialDiffuse, materialDiffuse);
gl.uniform4fv(program.uMaterialAmbient, materialAmbient);
gl.uniform4fv(program.uMaterialSpecular, materialSpecular);
gl.uniform1f(program.uShininess, shininess);
}
function initBuffers() {
const vertices = [
1.5, 0, 0, -1.5, 1, 0, -1.5, 0.809017, 0.587785, -1.5, 0.309017, 0.951057, -1.5, -0.309017, 0.951057, -1.5, -0.809017, 0.587785, -1.5, -1, 0, -1.5, -0.809017, -0.587785, -1.5, -0.309017, -0.951057, -1.5, 0.309017, -0.951057, -1.5, 0.809017, -0.587785
];
indices = [
0, 1, 2,
0, 2, 3,
0, 3, 4,
0, 4, 5,
0, 5, 6,
0, 6, 7,
0, 7, 8,
0, 8, 9,
0, 9, 10,
0, 10, 1
];
const normals = utils.calculateNormals(vertices, indices);
// Create VAO
vao = gl.createVertexArray();
// Bind VAO
gl.bindVertexArray(vao);
// Vertices
const sphereVerticesBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, sphereVerticesBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
// Configure VAO instructions
gl.enableVertexAttribArray(program.aVertexPosition);
gl.vertexAttribPointer(program.aVertexPosition, 3, gl.FLOAT, false, 0, 0);
// Normals
const sphereNormalsBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, sphereNormalsBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW);
// Configure VAO instructions
gl.enableVertexAttribArray(program.aVertexNormal);
gl.vertexAttribPointer(program.aVertexNormal, 3, gl.FLOAT, false, 0, 0);
// Indices
sphereIndicesBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, sphereIndicesBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
// Clean
gl.bindVertexArray(null);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
}
function onMouseDown(event) {
dragging = true;
currentX = event.clientX;
currentY = event.clientY;
}
function onMouseMove(event) {
if (dragging) {
lastX = currentX;
lastY = currentY;
currentX = event.clientX;
currentY = event.clientY;
const dx = currentX - lastX;
rotateAxis = [0, 1, 0];
angle = dx;
}
}
function onMouseUp() {
dragging = false;
}
function draw() {
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
mat4.perspective(projectionMatrix, 45, gl.canvas.width / gl.canvas.height, 0.1, 10000);
mat4.identity(modelViewMatrix);
mat4.translate(modelViewMatrix, modelViewMatrix, [0, 0, -5]);
mat4.rotate(modelViewMatrix, modelViewMatrix, angle * Math.PI / 180, [0, 1, 0]);
mat4.copy(normalMatrix, modelViewMatrix);
mat4.invert(normalMatrix, normalMatrix);
mat4.transpose(normalMatrix, normalMatrix);
gl.uniformMatrix4fv(program.uNormalMatrix, false, normalMatrix);
gl.uniformMatrix4fv(program.uModelViewMatrix, false, modelViewMatrix);
gl.uniformMatrix4fv(program.uProjectionMatrix, false, projectionMatrix);
// We will start using the `try/catch` to capture any errors from our `draw` calls
try {
gl.bindVertexArray(vao);
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);
} catch (error) {
console.error(error);
}
}
function render() {
requestAnimationFrame(render);
draw();
}
function init() {
initProgram();
initBuffers();
initLights();
render();
}
init();
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.8.1/gl-matrix-min.js"></script>
<!-- vertex Shader -->
<script id="vertex-shader" type="x-shader/x-vertex">
#version 300 es
precision mediump float;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
uniform mat4 uNormalMatrix;
in vec3 aVertexPosition;
in vec3 aVertexNormal;
out vec3 vNormal;
out vec3 vEyeVector;
void main(void) {
vec4 vertex = uModelViewMatrix * vec4(aVertexPosition, 1.0);
// Set varyings to be used inside of fragment shader
vNormal = vec3(uNormalMatrix * vec4(aVertexNormal, 1.0));
vEyeVector = -vec3(vertex.xyz);
gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aVertexPosition, 1.0);
}
</script>
<!-- fragment Shader -->
<script id="fragment-shader" type="x-shader/x-fragment">
#version 300 es
precision mediump float;
uniform float uShininess;
uniform vec3 uLightDirection;
uniform vec4 uLightAmbient;
uniform vec4 uLightDiffuse;
uniform vec4 uLightSpecular;
uniform vec4 uMaterialAmbient;
uniform vec4 uMaterialDiffuse;
uniform vec4 uMaterialSpecular;
in vec3 vNormal;
in vec3 vEyeVector;
out vec4 fragColor;
void main(void) {
// Normalized light direction
vec3 L = normalize(uLightDirection);
// Normalized normal
vec3 N = normalize(vNormal);
float lambertTerm = dot(N, -L);
// Ambient
vec4 Ia = uLightAmbient * uMaterialAmbient;
// Diffuse
vec4 Id = vec4(0.0, 0.0, 0.0, 1.0);
// Specular
vec4 Is = vec4(0.0, 0.0, 0.0, 1.0);
if (lambertTerm > 0.0) {
Id = uLightDiffuse * uMaterialDiffuse * lambertTerm;
vec3 E = normalize(vEyeVector);
vec3 R = reflect(L, N);
float specular = pow( max(dot(R, E), 0.0), uShininess);
Is = uLightSpecular * uMaterialSpecular * specular;
}
// Final fargment color takes into account all light values that
// were computed within the fragment shader
fragColor = vec4(vec3(Ia + Id + Is), 1.0);
}
</script>
<canvas id="webgl-canvas"> </canvas>
The thing I wanted to achieve was, I wanted to use my mouse to drag this cone and rotate it around y axis.
so first I have my canvas events set up.
canvas.onmousedown = event => onMouseDown(event);
canvas.onmouseup = event => onMouseUp(event);
canvas.onmousemove = event => onMouseMove(event);
...
function onMouseDown(event) {
dragging = true;
currentX = event.clientX;
currentY = event.clientY;
}
function onMouseMove(event) {
if(dragging) {
lastX = currentX;
lastY = currentY;
currentX = event.clientX;
currentY = event.clientY;
const dx = currentX - lastX;
rotateAxis = [0,1,0];
angle = dx;
}
}
function onMouseUp() {
dragging = false;
}
The logic is, when the mouse is pressed, I set the flag dragging to be true and store the screen coordinates to currentX and currentY, then moving the mouse, I will calculate the difference between the current coordinates and the previous coordinates and increase or decrease the rotation angle. And then the mouse is not pressed, i.e. it is up, I set the flag to be false.
The problem I have is, even though it works, but it kinda have this not-smooth feel to it. Can someone please help me improve it?
The fix is:
angle += dx;, as you want to accumulate the rotation deltas. Currently you just overwrite the angle with latest dx.
Updated example below:
// A set of utility functions for /common operations across our application
const utils = {
// Find and return a DOM element given an ID
getCanvas(id) {
const canvas = document.getElementById(id);
if (!canvas) {
console.error(`There is no canvas with id ${id} on this page.`);
return null;
}
return canvas;
},
// Given a canvas element, return the WebGL2 context
getGLContext(canvas) {
return canvas.getContext('webgl2') || console.error('WebGL2 is not available in your browser.');
},
// Given a canvas element, expand it to the size of the window
// and ensure that it automatically resizes as the window changes
autoResizeCanvas(canvas) {
const expandFullScreen = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
};
expandFullScreen();
// Resize screen when the browser has triggered the resize event
window.addEventListener('resize', expandFullScreen);
},
// Given a WebGL context and an id for a shader script,
// return a compiled shader
getShader(gl, id) {
const script = document.getElementById(id);
if (!script) {
return null;
}
const shaderString = script.text.trim();
let shader;
if (script.type === 'x-shader/x-vertex') {
shader = gl.createShader(gl.VERTEX_SHADER);
} else if (script.type === 'x-shader/x-fragment') {
shader = gl.createShader(gl.FRAGMENT_SHADER);
} else {
return null;
}
gl.shaderSource(shader, shaderString);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(shader));
return null;
}
return shader;
},
// Normalize colors from 0-255 to 0-1
normalizeColor(color) {
return color.map(c => c / 255);
},
// De-normalize colors from 0-1 to 0-255
denormalizeColor(color) {
return color.map(c => c * 255);
},
// Returns computed normals for provided vertices.
// Note: Indices have to be completely defined--NO TRIANGLE_STRIP only TRIANGLES.
calculateNormals(vs, ind) {
const
x = 0,
y = 1,
z = 2,
ns = [];
// For each vertex, initialize normal x, normal y, normal z
for (let i = 0; i < vs.length; i += 3) {
ns[i + x] = 0.0;
ns[i + y] = 0.0;
ns[i + z] = 0.0;
}
// We work on triads of vertices to calculate
for (let i = 0; i < ind.length; i += 3) {
// Normals so i = i+3 (i = indices index)
const v1 = [],
v2 = [],
normal = [];
// p2 - p1
v1[x] = vs[3 * ind[i + 2] + x] - vs[3 * ind[i + 1] + x];
v1[y] = vs[3 * ind[i + 2] + y] - vs[3 * ind[i + 1] + y];
v1[z] = vs[3 * ind[i + 2] + z] - vs[3 * ind[i + 1] + z];
// p0 - p1
v2[x] = vs[3 * ind[i] + x] - vs[3 * ind[i + 1] + x];
v2[y] = vs[3 * ind[i] + y] - vs[3 * ind[i + 1] + y];
v2[z] = vs[3 * ind[i] + z] - vs[3 * ind[i + 1] + z];
// Cross product by Sarrus Rule
normal[x] = v1[y] * v2[z] - v1[z] * v2[y];
normal[y] = v1[z] * v2[x] - v1[x] * v2[z];
normal[z] = v1[x] * v2[y] - v1[y] * v2[x];
// Update the normals of that triangle: sum of vectors
for (let j = 0; j < 3; j++) {
ns[3 * ind[i + j] + x] = ns[3 * ind[i + j] + x] + normal[x];
ns[3 * ind[i + j] + y] = ns[3 * ind[i + j] + y] + normal[y];
ns[3 * ind[i + j] + z] = ns[3 * ind[i + j] + z] + normal[z];
}
}
// Normalize the result.
// The increment here is because each vertex occurs.
for (let i = 0; i < vs.length; i += 3) {
// With an offset of 3 in the array (due to x, y, z contiguous values)
const nn = [];
nn[x] = ns[i + x];
nn[y] = ns[i + y];
nn[z] = ns[i + z];
let len = Math.sqrt((nn[x] * nn[x]) + (nn[y] * nn[y]) + (nn[z] * nn[z]));
if (len === 0) len = 1.0;
nn[x] = nn[x] / len;
nn[y] = nn[y] / len;
nn[z] = nn[z] / len;
ns[i + x] = nn[x];
ns[i + y] = nn[y];
ns[i + z] = nn[z];
}
return ns;
},
// A simpler API on top of the dat.GUI API, specifically
// designed for this book for a simpler codebase
configureControls(settings, options = {
width: 300
}) {
// Check if a gui instance is passed in or create one by default
const gui = options.gui || new dat.GUI(options);
const state = {};
const isAction = v => typeof v === 'function';
const isFolder = v =>
!isAction(v) &&
typeof v === 'object' &&
(v.value === null || v.value === undefined);
const isColor = v =>
(typeof v === 'string' && ~v.indexOf('#')) ||
(Array.isArray(v) && v.length >= 3);
Object.keys(settings).forEach(key => {
const settingValue = settings[key];
if (isAction(settingValue)) {
state[key] = settingValue;
return gui.add(state, key);
}
if (isFolder(settingValue)) {
// If it's a folder, recursively call with folder as root settings element
return utils.configureControls(settingValue, {
gui: gui.addFolder(key)
});
}
const {
value,
min,
max,
step,
options,
onChange = () => null,
} = settingValue;
// set state
state[key] = value;
let controller;
// There are many other values we can set on top of the dat.GUI
// API, but we'll only need a few for our purposes
if (options) {
controller = gui.add(state, key, options);
} else if (isColor(value)) {
controller = gui.addColor(state, key)
} else {
controller = gui.add(state, key, min, max, step)
}
controller.onChange(v => onChange(v, state))
});
},
// Calculate tangets for a given set of vertices
calculateTangents(vs, tc, ind) {
const tangents = [];
for (let i = 0; i < vs.length / 3; i++) {
tangents[i] = [0, 0, 0];
}
let
a = [0, 0, 0],
b = [0, 0, 0],
triTangent = [0, 0, 0];
for (let i = 0; i < ind.length; i += 3) {
const i0 = ind[i];
const i1 = ind[i + 1];
const i2 = ind[i + 2];
const pos0 = [vs[i0 * 3], vs[i0 * 3 + 1], vs[i0 * 3 + 2]];
const pos1 = [vs[i1 * 3], vs[i1 * 3 + 1], vs[i1 * 3 + 2]];
const pos2 = [vs[i2 * 3], vs[i2 * 3 + 1], vs[i2 * 3 + 2]];
const tex0 = [tc[i0 * 2], tc[i0 * 2 + 1]];
const tex1 = [tc[i1 * 2], tc[i1 * 2 + 1]];
const tex2 = [tc[i2 * 2], tc[i2 * 2 + 1]];
vec3.subtract(a, pos1, pos0);
vec3.subtract(b, pos2, pos0);
const c2c1b = tex1[1] - tex0[1];
const c3c1b = tex2[0] - tex0[1];
triTangent = [c3c1b * a[0] - c2c1b * b[0], c3c1b * a[1] - c2c1b * b[1], c3c1b * a[2] - c2c1b * b[2]];
vec3.add(triTangent, tangents[i0], triTangent);
vec3.add(triTangent, tangents[i1], triTangent);
vec3.add(triTangent, tangents[i2], triTangent);
}
// Normalize tangents
const ts = [];
tangents.forEach(tan => {
vec3.normalize(tan, tan);
ts.push(tan[0]);
ts.push(tan[1]);
ts.push(tan[2]);
});
return ts;
}
};
let
gl,
program,
modelViewMatrix = mat4.create(),
projectionMatrix = mat4.create(),
normalMatrix = mat4.create(),
vao,
indices,
sphereIndicesBuffer,
currentX,
currentY,
lastX,
lastY,
dragging,
rotateAxis = [0, 1, 0],
angle = 0,
shininess = 10,
clearColor = [0.9, 0.9, 0.9],
lightColor = [1, 1, 1, 1],
lightAmbient = [0.03, 0.03, 0.03, 1],
lightSpecular = [1, 1, 1, 1],
lightDirection = [-0.25, -0.25, -0.25],
materialDiffuse = [46 / 256, 99 / 256, 191 / 256, 1],
materialAmbient = [1, 1, 1, 1],
materialSpecular = [1, 1, 1, 1];
function initProgram() {
// Configure `canvas`
const canvas = utils.getCanvas('webgl-canvas');
utils.autoResizeCanvas(canvas);
// Configure `gl`
gl = utils.getGLContext(canvas);
gl.clearColor(...clearColor, 1);
gl.clearDepth(100);
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
// Shader source
const vertexShader = utils.getShader(gl, 'vertex-shader');
const fragmentShader = utils.getShader(gl, 'fragment-shader');
// Configure `program`
program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error('Could not initialize shaders');
}
gl.useProgram(program);
// Set locations onto `program` instance
program.aVertexPosition = gl.getAttribLocation(program, 'aVertexPosition');
program.aVertexNormal = gl.getAttribLocation(program, 'aVertexNormal');
program.uProjectionMatrix = gl.getUniformLocation(program, 'uProjectionMatrix');
program.uModelViewMatrix = gl.getUniformLocation(program, 'uModelViewMatrix');
program.uNormalMatrix = gl.getUniformLocation(program, 'uNormalMatrix');
program.uMaterialAmbient = gl.getUniformLocation(program, 'uMaterialAmbient');
program.uMaterialDiffuse = gl.getUniformLocation(program, 'uMaterialDiffuse');
program.uMaterialSpecular = gl.getUniformLocation(program, 'uMaterialSpecular');
program.uShininess = gl.getUniformLocation(program, 'uShininess');
program.uLightAmbient = gl.getUniformLocation(program, 'uLightAmbient');
program.uLightDiffuse = gl.getUniformLocation(program, 'uLightDiffuse');
program.uLightSpecular = gl.getUniformLocation(program, 'uLightSpecular');
program.uLightDirection = gl.getUniformLocation(program, 'uLightDirection');
canvas.onmousedown = event => onMouseDown(event);
canvas.onmouseup = event => onMouseUp(event);
canvas.onmousemove = event => onMouseMove(event);
}
// Configure lights
function initLights() {
gl.uniform4fv(program.uLightDiffuse, lightColor);
gl.uniform4fv(program.uLightAmbient, lightAmbient);
gl.uniform4fv(program.uLightSpecular, lightSpecular);
gl.uniform3fv(program.uLightDirection, lightDirection);
gl.uniform4fv(program.uMaterialDiffuse, materialDiffuse);
gl.uniform4fv(program.uMaterialAmbient, materialAmbient);
gl.uniform4fv(program.uMaterialSpecular, materialSpecular);
gl.uniform1f(program.uShininess, shininess);
}
function initBuffers() {
const vertices = [
1.5, 0, 0, -1.5, 1, 0, -1.5, 0.809017, 0.587785, -1.5, 0.309017, 0.951057, -1.5, -0.309017, 0.951057, -1.5, -0.809017, 0.587785, -1.5, -1, 0, -1.5, -0.809017, -0.587785, -1.5, -0.309017, -0.951057, -1.5, 0.309017, -0.951057, -1.5, 0.809017, -0.587785
];
indices = [
0, 1, 2,
0, 2, 3,
0, 3, 4,
0, 4, 5,
0, 5, 6,
0, 6, 7,
0, 7, 8,
0, 8, 9,
0, 9, 10,
0, 10, 1
];
const normals = utils.calculateNormals(vertices, indices);
// Create VAO
vao = gl.createVertexArray();
// Bind VAO
gl.bindVertexArray(vao);
// Vertices
const sphereVerticesBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, sphereVerticesBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
// Configure VAO instructions
gl.enableVertexAttribArray(program.aVertexPosition);
gl.vertexAttribPointer(program.aVertexPosition, 3, gl.FLOAT, false, 0, 0);
// Normals
const sphereNormalsBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, sphereNormalsBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW);
// Configure VAO instructions
gl.enableVertexAttribArray(program.aVertexNormal);
gl.vertexAttribPointer(program.aVertexNormal, 3, gl.FLOAT, false, 0, 0);
// Indices
sphereIndicesBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, sphereIndicesBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
// Clean
gl.bindVertexArray(null);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
}
function onMouseDown(event) {
dragging = true;
currentX = event.clientX;
currentY = event.clientY;
}
function onMouseMove(event) {
if (dragging) {
lastX = currentX;
lastY = currentY;
currentX = event.clientX;
currentY = event.clientY;
const dx = currentX - lastX;
rotateAxis = [0, 1, 0];
angle += dx;
}
}
function onMouseUp() {
dragging = false;
}
function draw() {
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
mat4.perspective(projectionMatrix, 45, gl.canvas.width / gl.canvas.height, 0.1, 10000);
mat4.identity(modelViewMatrix);
mat4.translate(modelViewMatrix, modelViewMatrix, [0, 0, -5]);
mat4.rotate(modelViewMatrix, modelViewMatrix, angle * Math.PI / 180, [0, 1, 0]);
mat4.copy(normalMatrix, modelViewMatrix);
mat4.invert(normalMatrix, normalMatrix);
mat4.transpose(normalMatrix, normalMatrix);
gl.uniformMatrix4fv(program.uNormalMatrix, false, normalMatrix);
gl.uniformMatrix4fv(program.uModelViewMatrix, false, modelViewMatrix);
gl.uniformMatrix4fv(program.uProjectionMatrix, false, projectionMatrix);
// We will start using the `try/catch` to capture any errors from our `draw` calls
try {
gl.bindVertexArray(vao);
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);
} catch (error) {
console.error(error);
}
}
function render() {
requestAnimationFrame(render);
draw();
}
function init() {
initProgram();
initBuffers();
initLights();
render();
}
init();
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.8.1/gl-matrix-min.js"></script>
<!-- vertex Shader -->
<script id="vertex-shader" type="x-shader/x-vertex">
#version 300 es
precision mediump float;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
uniform mat4 uNormalMatrix;
in vec3 aVertexPosition;
in vec3 aVertexNormal;
out vec3 vNormal;
out vec3 vEyeVector;
void main(void) {
vec4 vertex = uModelViewMatrix * vec4(aVertexPosition, 1.0);
// Set varyings to be used inside of fragment shader
vNormal = vec3(uNormalMatrix * vec4(aVertexNormal, 1.0));
vEyeVector = -vec3(vertex.xyz);
gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aVertexPosition, 1.0);
}
</script>
<!-- fragment Shader -->
<script id="fragment-shader" type="x-shader/x-fragment">
#version 300 es
precision mediump float;
uniform float uShininess;
uniform vec3 uLightDirection;
uniform vec4 uLightAmbient;
uniform vec4 uLightDiffuse;
uniform vec4 uLightSpecular;
uniform vec4 uMaterialAmbient;
uniform vec4 uMaterialDiffuse;
uniform vec4 uMaterialSpecular;
in vec3 vNormal;
in vec3 vEyeVector;
out vec4 fragColor;
void main(void) {
// Normalized light direction
vec3 L = normalize(uLightDirection);
// Normalized normal
vec3 N = normalize(vNormal);
float lambertTerm = dot(N, -L);
// Ambient
vec4 Ia = uLightAmbient * uMaterialAmbient;
// Diffuse
vec4 Id = vec4(0.0, 0.0, 0.0, 1.0);
// Specular
vec4 Is = vec4(0.0, 0.0, 0.0, 1.0);
if (lambertTerm > 0.0) {
Id = uLightDiffuse * uMaterialDiffuse * lambertTerm;
vec3 E = normalize(vEyeVector);
vec3 R = reflect(L, N);
float specular = pow( max(dot(R, E), 0.0), uShininess);
Is = uLightSpecular * uMaterialSpecular * specular;
}
// Final fargment color takes into account all light values that
// were computed within the fragment shader
fragColor = vec4(vec3(Ia + Id + Is), 1.0);
}
</script>
<canvas id="webgl-canvas"> </canvas>
I am trying to implement my own WebGL based 3D graph plotting app.
As I might by dealing with a lot of data points, I hope I can make an interactive grid to help visualize number of different order of magnitude with scrolling support(for scaling).
Here is my fragment shader for my grid cube.
precision highp float;
varying vec3 pos;
uniform float u_size;
uniform float u_scale;
float my_fmod(float inp_val, float inp_m) {
float m = inp_m;
float val = inp_val + 201.0 * m;
return abs(val - float(int(val/m)) * m);
}
float gridline(float nPos0, float nPos1, float fac, float very_small_number) {
if ( my_fmod(nPos0, 1./fac) < very_small_number || my_fmod(nPos1, 1./fac) < very_small_number) return 1.0;
return 0.0;
}
void main() {
float very_small_number = 0.015 / u_scale;
float size = u_size;
vec3 nPos = pos / u_scale;
vec3 color = vec3(0.,0.,0.5);
float sqrt5 = 2.236068;
gl_FragColor = vec4(0.,0.,0.,1.);
float n = 5.;
float sLine = 1.;
float t1 = 1.;
float t2 = 1.;
if (pos.x == -size || pos.x == size) {
if (gridline(nPos.y, nPos.z, n, very_small_number) > 0.5) gl_FragColor.xyz += color * t1;
if (gridline(nPos.y*n, nPos.z*n, n, very_small_number) > 0.5) gl_FragColor.xyz += color * t2;
} else if (pos.y == -size || pos.y == size) {
if (gridline(nPos.x, nPos.z, n, very_small_number) > 0.5) gl_FragColor.xyz += color * t1;
if (gridline(nPos.x*n, nPos.z*n, n, very_small_number) > 0.5) gl_FragColor.xyz += color * t2;
} else if (pos.z == -size || pos.z == size) {
if (gridline(nPos.x, nPos.y, n, very_small_number) > 0.5) gl_FragColor.xyz += color * t1;
if (gridline(nPos.x*n, nPos.y*n, n, very_small_number) > 0.5) gl_FragColor.xyz += color * t2;
}
}
As you can see, I drew two sets of grid. They represents two different order of magnitudes. At this moment, it can scale perfectly but I want to add a feature which allows fading between different order of magnitudes and only shows grids that are in certain scale.
Here is the original look of the App,
It looks nice. And when I zoom in,
The grid looks larger. But I hope that my shader can draw new grids inside the small grids and not to draw large grids which is not visible any more.
When I zoom out,
There are too many grids shown and it affects the user's experience.
So, how can I achieve transitioning between different order of magnitudes and apply fading between the transitions? Sorry for my poor English. Any help is appreciated. Have a nice day.
edit 1:
Here is the full code for experimenting.
https://github.com/Jonathan-D-Ip/WebGLPlottingApp/blob/master/Display.html
If it was me I wouldn't be using a fragment shader for a grid I'd be using lines. I'd draw one grid at one size and if I was close to the transition and needed a new grid I'd draw another grid at another scale. Both grids would have an alpha setting so I could fade them out.
Here's an example, use the mousewheel or equivalent to zoom. Maybe you can adapt the ideas to your preferred solution. The important part is probably this part
const gridLevel = Math.log10(zoom * zoomAdjust);
const gridFract = euclideanModulo(gridLevel, 1);
const gridZoom = Math.pow(10, Math.floor(gridLevel));
const alpha1 = Math.max((1 - gridFract) * 1);
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
drawGrid(viewProjection, gridZoom, [0, alpha1, 0, alpha1]);
const alpha2 = Math.max(gridFract * 10) - 1;
if (alpha2 > 0) {
drawGrid(viewProjection, gridZoom * 10, [0, alpha2, 0, alpha2],);
}
zoom goes from 0.0001 to 10000 and represents the distance from the target. The code uses Math.log10 to find out 10 to the what power is needed to get that zoom level. In other words if zoom is 100 then gridLevel = 2. If zoom is 1000 then gridLevel = 3. From that we can get the fractional amount between powers of 10 in gridFract which will always be in the range of 0 to 1 as we move between zoom levels.
gridZoom tells us what scale to draw one of our grids (we just remove the fractional part of gridLevel) and then raise 10 to that power. gridZoom * 10 is the next largest grid size.
alpha1 is the alpha for the grid. alpha2 is the alpha for the second grid.
const m4 = twgl.m4;
const gl = document.querySelector("canvas").getContext("webgl", {alpha: false});
const zoomElem = document.querySelector("#zoom");
const zoomAdjust = 1; // change to adjust when things start/end. Try 5 or .5 for example
let zoom = 1;
const gridVS = `
attribute vec4 position;
uniform mat4 matrix;
void main() {
gl_Position = matrix * position;
}
`;
const gridFS = `
precision mediump float;
uniform vec4 color;
void main() {
gl_FragColor = color;
}
`;
const gridProgramInfo = twgl.createProgramInfo(gl, [gridVS, gridFS]);
const gridPlaneLines = [];
const numLines = 100;
for (let i = 0; i <= 100; ++i) {
gridPlaneLines.push(0, i, 100, i);
gridPlaneLines.push(i, 0, i, 100);
}
const gridPlaneBufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: { numComponents: 2, data: gridPlaneLines },
});
function drawGrid(viewProjection, scale, color) {
gl.useProgram(gridProgramInfo.program);
twgl.setBuffersAndAttributes(gl, gridProgramInfo, gridPlaneBufferInfo);
const scaling = [scale, scale, scale];
// draw Z plane
{
let matrix = m4.scale(viewProjection, scaling);
matrix = m4.rotateY(matrix, Math.PI);
twgl.setUniforms(gridProgramInfo, {
matrix,
color,
});
twgl.drawBufferInfo(gl, gridPlaneBufferInfo, gl.LINES);
}
// draw X plane
{
let matrix = m4.scale(viewProjection, scaling);
matrix = m4.rotateY(matrix, Math.PI * .5);
twgl.setUniforms(gridProgramInfo, {
matrix,
color,
});
twgl.drawBufferInfo(gl, gridPlaneBufferInfo, gl.LINES);
}
// draw Y plane
{
let matrix = m4.scale(viewProjection, scaling);
matrix = m4.rotateY(matrix, Math.PI);
matrix = m4.rotateX(matrix, Math.PI * .5);
twgl.setUniforms(gridProgramInfo, {
matrix,
color,
});
twgl.drawBufferInfo(gl, gridPlaneBufferInfo, gl.LINES);
}
}
function render() {
twgl.resizeCanvasToDisplaySize(gl.canvas, window.devicePixelRatio);
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
zoomElem.textContent = zoom.toFixed(5);
const fov = degToRad(60);
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const zNear = zoom / 100;
const zFar = zoom * 100;
const projection = m4.perspective(fov, aspect, zNear, zFar);
const eye = [zoom * -10, zoom * 5, zoom * -10];
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 gridLevel = Math.log10(zoom * zoomAdjust);
const gridFract = euclideanModulo(gridLevel, 1);
const gridZoom = Math.pow(10, Math.floor(gridLevel));
const alpha1 = Math.max((1 - gridFract) * 1);
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
drawGrid(viewProjection, gridZoom, [0, alpha1, 0, alpha1]);
const alpha2 = Math.max(gridFract * 10) - 1;
if (alpha2 > 0) {
drawGrid(viewProjection, gridZoom * 10, [0, alpha2, 0, alpha2],);
}
}
render();
function euclideanModulo(n, m) {
return ((n % m) + m) % m;
};
function degToRad(deg) {
return deg * Math.PI / 180;
}
window.addEventListener('wheel', (e) => {
e.preventDefault();
const amount = e.deltaY;
if (e.deltaY < 0) {
zoom *= 1 - clamp(e.deltaY / -500, 0, 1);
} else {
zoom *= 1 + clamp(e.deltaY / 500, 0, 1);
}
zoom = clamp(zoom, 0.0001, 10000);
render();
}, {passive: false});
window.addEventListener('resize', render);
function clamp(v, min, max) {
return Math.max(min, Math.min(max, v));
}
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
#ui { position: absolute; left: 1em; top: 1em; padding: 1em; color: white; }
<canvas></canvas>
<script src="https://twgljs.org/dist/4.x/twgl-full.js"></script>
<div id="ui">
<div>zoom:<span id="zoom"></span></div>
</div>