How to draw 3d objects on a 2d canvas - javascript
here's the full code, I had to remove spaces from some of the functions that weren't related to the problem to make sure im in the 30k character limit of stack overflow
const EPSILON = 0.000001;
const mat4 = { rotateZ: function(out, a, rad) { let s = Math.sin(rad); let c = Math.cos(rad); let a00 = a[0]; let a01 = a[1]; let a02 = a[2]; let a03 = a[3]; let a10 = a[4]; let a11 = a[5]; let a12 = a[6]; let a13 = a[7]; if (a !== out) { out[8] = a[8]; out[9] = a[9]; out[10] = a[10]; out[11] = a[11]; out[12] = a[12]; out[13] = a[13]; out[14] = a[14]; out[15] = a[15]; } out[0] = a00 * c + a10 * s; out[1] = a01 * c + a11 * s; out[2] = a02 * c + a12 * s; out[3] = a03 * c + a13 * s; out[4] = a10 * c - a00 * s; out[5] = a11 * c - a01 * s; out[6] = a12 * c - a02 * s; out[7] = a13 * c - a03 * s; return out; }, create: function() { let out = new Float32Array(16); out[0] = 1; out[5] = 1; out[10] = 1; out[15] = 1; return out; }, perspective: function(out, fovy, aspect, near, far) { let f = 1.0 / Math.tan(fovy / 2), nf; out[0] = f / aspect; out[1] = 0; out[2] = 0; out[3] = 0; out[4] = 0; out[5] = f; out[6] = 0; out[7] = 0; out[8] = 0; out[9] = 0; out[11] = -1; out[12] = 0; out[13] = 0; out[15] = 0; if (far !== null && far !== Infinity) { nf = 1 / (near - far); out[10] = (far + near) * nf; out[14] = (2 * far * near) * nf; } else { out[10] = -1; out[14] = -2 * near; } return out; }, translate: function(out, a, v) { let x = v[0], y = v[1], z = v[2]; if (a === out) { out[12] = a[0] * x + a[4] * y + a[8] * z + a[12]; out[13] = a[1] * x + a[5] * y + a[9] * z + a[13]; out[14] = a[2] * x + a[6] * y + a[10] * z + a[14]; out[15] = a[3] * x + a[7] * y + a[11] * z + a[15]; return out; } else { let a00, a01, a02, a03; let a10, a11, a12, a13; let a20, a21, a22, a23; a00 = a[0]; a01 = a[1]; a02 = a[2]; a03 = a[3]; a10 = a[4]; a11 = a[5]; a12 = a[6]; a13 = a[7]; a20 = a[8]; a21 = a[9]; a22 = a[10]; a23 = a[11]; out[0] = a00; out[1] = a01; out[2] = a02; out[3] = a03; out[4] = a10; out[5] = a11; out[6] = a12; out[7] = a13; out[8] = a20; out[9] = a21; out[10] = a22; out[11] = a23; out[12] = a00 * x + a10 * y + a20 * z + a[12]; out[13] = a01 * x + a11 * y + a21 * z + a[13]; out[14] = a02 * x + a12 * y + a22 * z + a[14]; out[15] = a03 * x + a13 * y + a23 * z + a[15]; return out; } }, scale: function(out, a, v) { let x = v[0], y = v[1], z = v[2]; out[0] = a[0] * x; out[1] = a[1] * x; out[2] = a[2] * x; out[3] = a[3] * x; out[4] = a[4] * y; out[5] = a[5] * y; out[6] = a[6] * y; out[7] = a[7] * y; out[8] = a[8] * z; out[9] = a[9] * z; out[10] = a[10] * z; out[11] = a[11] * z; out[12] = a[12]; out[13] = a[13]; out[14] = a[14]; out[15] = a[15]; return out; }, multiply: function(out, a, b) { let a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3]; let a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7]; let a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11]; let a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15]; let b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3]; out[0] = b0*a00 + b1*a10 + b2*a20 + b3*a30; out[1] = b0*a01 + b1*a11 + b2*a21 + b3*a31; out[2] = b0*a02 + b1*a12 + b2*a22 + b3*a32; out[3] = b0*a03 + b1*a13 + b2*a23 + b3*a33; b0 = b[4]; b1 = b[5]; b2 = b[6]; b3 = b[7]; out[4] = b0*a00 + b1*a10 + b2*a20 + b3*a30; out[5] = b0*a01 + b1*a11 + b2*a21 + b3*a31; out[6] = b0*a02 + b1*a12 + b2*a22 + b3*a32; out[7] = b0*a03 + b1*a13 + b2*a23 + b3*a33; b0 = b[8]; b1 = b[9]; b2 = b[10]; b3 = b[11]; out[8] = b0*a00 + b1*a10 + b2*a20 + b3*a30; out[9] = b0*a01 + b1*a11 + b2*a21 + b3*a31; out[10] = b0*a02 + b1*a12 + b2*a22 + b3*a32; out[11] = b0*a03 + b1*a13 + b2*a23 + b3*a33; b0 = b[12]; b1 = b[13]; b2 = b[14]; b3 = b[15]; out[12] = b0*a00 + b1*a10 + b2*a20 + b3*a30; out[13] = b0*a01 + b1*a11 + b2*a21 + b3*a31; out[14] = b0*a02 + b1*a12 + b2*a22 + b3*a32; out[15] = b0*a03 + b1*a13 + b2*a23 + b3*a33; return out; }, lookAt: function(out, eye, center, up) { let x0, x1, x2, y0, y1, y2, z0, z1, z2, len; let eyex = eye[0]; let eyey = eye[1]; let eyez = eye[2]; let upx = up[0]; let upy = up[1]; let upz = up[2]; let centerx = center[0]; let centery = center[1]; let centerz = center[2]; if (Math.abs(eyex - centerx) < EPSILON && Math.abs(eyey - centery) < EPSILON && Math.abs(eyez - centerz) < EPSILON) { return identity(out); } z0 = eyex - centerx; z1 = eyey - centery; z2 = eyez - centerz; len = 1 / Math.hypot(z0, z1, z2); z0 *= len; z1 *= len; z2 *= len; x0 = upy * z2 - upz * z1; x1 = upz * z0 - upx * z2; x2 = upx * z1 - upy * z0; len = Math.hypot(x0, x1, x2); if (!len) { x0 = 0; x1 = 0; x2 = 0; } else { len = 1 / len; x0 *= len; x1 *= len; x2 *= len; } y0 = z1 * x2 - z2 * x1; y1 = z2 * x0 - z0 * x2; y2 = z0 * x1 - z1 * x0; len = Math.hypot(y0, y1, y2); if (!len) { y0 = 0; y1 = 0; y2 = 0; } else { len = 1 / len; y0 *= len; y1 *= len; y2 *= len; } out[0] = x0; out[1] = y0; out[2] = z0; out[3] = 0; out[4] = x1; out[5] = y1; out[6] = z1; out[7] = 0; out[8] = x2; out[9] = y2; out[10] = z2; out[11] = 0; out[12] = -(x0 * eyex + x1 * eyey + x2 * eyez); out[13] = -(y0 * eyex + y1 * eyey + y2 * eyez); out[14] = -(z0 * eyex + z1 * eyey + z2 * eyez); out[15] = 1; return out; }, moveToVec3: function(out, v) { out[12] = v[0]; out[13] = v[1]; out[14] = v[2]; } }; const mat3 = { clone: function(a) { let out = new Float32Array(9); out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[3]; out[4] = a[4]; out[5] = a[5]; out[6] = a[6]; out[7] = a[7]; out[8] = a[8]; return out; }, create: function() { let out = new Float32Array(9); out[0] = 1; out[4] = 1; out[8] = 1; return out; } }; const vec3 = { multiply: function(out, a, b) { out[0] = a[0] * b[0]; out[1] = a[1] * b[1]; return out; }, create: function() { return new Float32Array(3);; }, copy: function(out, a) { out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; return out; } }; const vec2 = { create: function() { return new Float32Array(2);; }, copy: function(out, a) { out[0] = a[0]; out[1] = a[1]; return out; }, fromValues: function(x, y) { let out = new Float32Array(2); out[0] = x; out[1] = y; return out; }, multiply: function(out, a, b) { out[0] = a[0] * b[0]; out[1] = a[1] * b[1]; return out; }, add: function(out, a, b) { out[0] = a[0] + b[0]; out[1] = a[1] + b[1]; return out; } };
const FRAGMENT_SHADER = ` precision highp float; varying highp vec2 vTextureCoord; varying lowp vec4 vColor; uniform sampler2D uSampler; uniform bool aUseText; void main(void) { if( aUseText ){ gl_FragColor = texture2D(uSampler, vTextureCoord); } else { gl_FragColor = vColor; } } `;
const VERTEX_SHADER = ` attribute vec4 aVertexPosition; attribute vec4 aVertexColor; attribute vec2 aTextureCoord; uniform mat4 uModelViewMatrix; uniform mat4 uProjectionMatrix; uniform mat3 uTextMatrix; uniform float uPointSize; varying lowp vec4 vColor; varying highp vec2 vTextureCoord; void main(void) { gl_PointSize = uPointSize; gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition; vColor = aVertexColor; vTextureCoord = (vec3(aTextureCoord, 1)*uTextMatrix).xy; } `;
class WebglEntity {
constructor() {
this.matrix = mat4.create();
this.coords = vec3.create();
}
translate(newCoords) {
const {
matrix,
coords
} = this;
mat4.translate(matrix, matrix, newCoords);
vec3.copy(coords, [matrix[12], matrix[13], matrix[14]]);
return this;
}
move(newCoords) {
const {
matrix,
coords
} = this;
vec3.copy(coords, newCoords);
mat4.moveToVec3(matrix, coords);
return this;
}
}
class Camera extends WebglEntity {
constructor(fieldOfView, aspect, zNear, zFar) {
super();
this.projection = mat4.perspective(mat4.create(), fieldOfView, aspect, zNear, zFar);
}
lookAt(lookAt) {
const {
matrix,
projection,
coords
} = this;
mat4.lookAt(matrix, coords, lookAt, [0, 1, 0]);
mat4.multiply(matrix, projection, matrix);
return this;
}
}
class Rect extends WebglEntity{
constructor(){
super();
this.positionsBuffer = undefined;
this.fragColorPos = undefined;
this.strokeColorPos = undefined;
this.strokePositionBuffer = undefined;
this.vertexAttribInfo = undefined;
this.vertextColorAttribInfo = undefined;
this.vertexCount = undefined;
this.textureInfo = undefined;
this.multiTextures = false;
this.strokeSize = 1;
this.fillers = {
fill: false,
texture: false,
stroke: false
};
}
setup(matrix, positionsBuffer, strokePositionBuffer, vertexAttribInfo, vertextColorAttribInfo, vertexCount){
this.matrix = matrix;
this.positionsBuffer = positionsBuffer;
this.strokePositionBuffer = strokePositionBuffer;
this.vertexAttribInfo = vertexAttribInfo;
this.vertextColorAttribInfo = vertextColorAttribInfo;
this.vertexCount = vertexCount;
return this;
}
}
class Display{
constructor(gl, programInfo, zAxis, texture){
this.gl = gl;
this.programInfo = programInfo;
this.canvas = gl.canvas;
this.currentCamera = new Camera(45 * Math.PI / 180, gl.canvas.width/gl.canvas.height, 0.1, 100.0);
this.currentCamera.translate([0, 0, zAxis]).lookAt([0, 0, 0]);
this.zAxis = zAxis;
this.drawZAxis = 0;
this.last = {};
texture.textAttribInfo = {
numComponents: 2,
type: gl.FLOAT,
normalize: false,
stride: 0,
offset: 0
};
this.texture = texture;
this.spriteSheets = [];
const context = texture.context;
const canvas = texture.canvas;
this.images = {};
}
clear(color){
const gl = this.gl;
gl.clearColor(0.1, 0.1, 0.1, 1);
gl.clearDepth(1.0);
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}
rect(x, y, w, h){
const {rect, stroke} = this.createRectPos(w, h);
const square = new Rect();
square.setup(...this.getRectInfo(x, y, rect, stroke));
return square;
}
fillRect(rect, color){
const {createStaticDrawBuffer, gl, parseColor} = this;
rect.fillers.fill = true;
if(color){
rect.fragColorPos = createStaticDrawBuffer(gl, [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1]);
}
}
createRectPos(w, h){
const rect = [ w/2, h/2, -w/2, h/2, w/2, -h/2, -w/2, -h/2 ];
const stroke = [ -w/2, h/2, w/2, h/2, w/2, -h/2, -w/2, -h/2, ];
return {rect, stroke};
}
getRectInfo(x, y, rect, stroke){
return this.createSquareBuffer(rect, stroke, [x, y, this.drawZAxis]);
}
createStaticDrawBuffer(gl, data){
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
return buffer;
}
createSquareBuffer(positions, strokePosition, coords) {
const {gl, createStaticDrawBuffer} = this;
const positionsBuffer = createStaticDrawBuffer(gl, positions);
const strokePositionBuffer = createStaticDrawBuffer(gl, strokePosition);
const modelViewMatrix = mat4.create();
mat4.translate(modelViewMatrix, modelViewMatrix, coords);
return [modelViewMatrix, positionsBuffer, strokePositionBuffer, this.createAttribInfo(2, gl.FLOAT, false, 0, 0), this.createAttribInfo(4, gl.FLOAT, false, 0, 0), positions.length/2]; }
createAttribInfo(numComponents, type, normalize, stride, offset){
return { numComponents, type, normalize, stride, offset};
}
enableAttrib(buffer, attrib, gl, {numComponents, type, normalize, stride, offset}){
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.vertexAttribPointer(attrib, numComponents,type,normalize,stride,offset);
gl.enableVertexAttribArray(attrib);
}
drawBuffer(buffer){
const {gl, drawTexture, enableAttrib, createStaticDrawBuffer, currentCamera, texture: {context, canvas, textAttribInfo}, programInfo: {uniformLocations, program, attribLocations: {vertexPosition, vertexColor, textureCoord}}} = this;
const cameraMatrix = currentCamera.matrix;
const {positionsBuffer, fragColorPos, strokeColorPos, strokePositionBuffer, matrix, vertexAttribInfo, vertextColorAttribInfo, vertexCount, fragTextPos, fillers: {fill, stroke, texture}, strokeSize, textureInfo, multiTextures} = buffer;
gl.uniformMatrix4fv(uniformLocations.projectionMatrix, false, cameraMatrix);
gl.uniformMatrix4fv(uniformLocations.modelViewMatrix, false, matrix);
if(fill){
enableAttrib(positionsBuffer, vertexPosition, gl, vertexAttribInfo);
enableAttrib(fragColorPos, vertexColor, gl, vertextColorAttribInfo);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertexCount);
gl.disableVertexAttribArray(vertexColor);
}
}
static loadShader(gl, program, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
gl.attachShader(program, shader);
}
static async create(canvas, width, height, zAxis = 6){
canvas.width = width;
canvas.height = height;
const gl = canvas.getContext("webgl");
const shaderProgram = gl.createProgram();
Display.loadShader(gl, shaderProgram, gl.VERTEX_SHADER, VERTEX_SHADER);
Display.loadShader(gl, shaderProgram, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);
gl.linkProgram(shaderProgram);
const programInfo = {
program: shaderProgram,
attribLocations: {
vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'),
vertexColor: gl.getAttribLocation(shaderProgram, 'aVertexColor'),
textureCoord: gl.getAttribLocation(shaderProgram, 'aTextureCoord'),
},
uniformLocations: {
projectionMatrix: gl.getUniformLocation(shaderProgram, 'uProjectionMatrix'),
modelViewMatrix: gl.getUniformLocation(shaderProgram, 'uModelViewMatrix'),
textMatrix: gl.getUniformLocation(shaderProgram, 'uTextMatrix'),
sampler: gl.getUniformLocation(shaderProgram, 'uSampler'),
useText: gl.getUniformLocation(shaderProgram, 'aUseText'),
pointSize: gl.getUniformLocation(shaderProgram, 'uPointSize'),
},
};
gl.useProgram(programInfo.program);
gl.uniform1f(programInfo.uniformLocations.pointSize, 1.0);
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
const textureBuffer = gl.createTexture();
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, textureBuffer);
gl.uniform1i(programInfo.uniformLocations.uSampler, 0);
const textureCanvas = document.createElement("canvas");
textureCanvas.width = 0;
textureCanvas.height = 0;
let texture = {
canvas: textureCanvas,
buffer: textureBuffer,
context: textureCanvas.getContext("2d"),
};
return new Display(gl, programInfo, zAxis, texture);
}
}
class Engine { constructor(time_step, update, render, allowedSkippedFrames) { this.accumulated_time = 0; this.animation_frame_request = undefined, this.time = undefined, this.time_step = time_step, this.updated = false; this.update = update; this.render = render; this.allowedSkippedFrames = allowedSkippedFrames; this.run = this.run.bind(this); this.end = false; } run(time_stamp) { const { accumulated_time, time, time_step, updated, update, render, allowedSkippedFrames, end } = this; this.accumulated_time += time_stamp - time; this.time = time_stamp; if (accumulated_time > time_stamp * allowedSkippedFrames) { this.accumulated_time = time_stamp; } while (this.accumulated_time >= time_step) { this.accumulated_time -= time_step; update(time_stamp); this.updated = true; } if (updated) { this.updated = false; render(time_stamp); } if (end) { return; } this.animation_frame_request = requestAnimationFrame(this.run); } start() { this.accumulated_time = this.time_step; this.time = performance.now(); this.animation_frame_request = requestAnimationFrame(this.run); } stop() { this.end = true; cancelAnimationFrame(this.animation_frame_request); } }
class Entity extends Rect {
constructor(){
super();
this.velocity = vec2.create();
this.area = undefined;
this.mass = 2;
this.updateFillers = {};
this.delete = false;
this.draw = true;
}
setup(w, h, ...args){
this.area = vec2.fromValues(w, h);
super.setup(...args);
return this;
}
fill(...args){
this.updateFillers.fill = args;
}
update(deltaTime, speed){
return this;
}
move(x, y){
super.move([x, y, this.coords[2]]);
return this;
}
}
class Quixotic{
constructor(display){
this.display = display;
this.engine = undefined;
this.render = undefined;
this.update = undefined;
this.frameRate = undefined;
this.time = 0; this.speed = 1;
this.world = {
objects: {},
objectsCollisionInfo: {},
objectsArray: [],
classesInfo: {}
};
this.timePassed = 0;
}
createEntity(Class, ...args){
const display = this.display; const {rect, stroke} = display.createRectPos(5, 5); Class = Class ? Class : Entity; const className = Class.name; if(className !== "Entity" && !Entity.prototype.isPrototypeOf(Class.prototype)){ throw new TypeError("Expected extended class of Entity. Instead got: " + className); } let instance; const {objectsArray, classesInfo, objects} = this.world; const classInfo = classesInfo[className]; if(classInfo){ if(classInfo.args){ instance = new Class(...[...classInfo.args, ...args]); } else { instance = new Class(...args); } const name = classInfo.name; if(Array.isArray(objects[name])){ objects[name].push(instance); instance.name = name; } else { console.warn("Didn't save object in world.objects object, object wouldn't detect collision"); } } else { instance = new Class(...args); } instance.setup(5, 5, ...display.getRectInfo(0, 0, rect, stroke, "#000")); objectsArray.push(instance); return instance; }
createBackground(objects){
const buffer = document.createElement("canvas").getContext("2d");
const bufferRect = this.createEntity();
let {zAxis, canvas: {width, height}} = this.display;
zAxis--;
const halfZ = zAxis/2;
let {coords: [x, y], area: [w, h]} = objects[objects.length - 1];
let [mX, mY, mW, mH] = [x, y, w, h];
for(let i = objects.length-1; i--;){
const {coords: [_x, _y], area: [_w, _h]} = objects[i];
x < _x ? _x : x;
y < _y ? _y : y;
if(mX < _x){
mX = _x;
mW = _w;
}
if(mY < _y){
mY = _y;
mH = _h;
}
}
buffer.canvas.width = width;
buffer.canvas.height = height;
for(let i = objects.length; i--;){
const {coords: [_x, _y], area: [_w, _h]} = objects[i];
buffer.fillRect(((_x-halfZ-_w*2)/zAxis+1)*width, ((-_y-halfZ-_h*2)/zAxis+1)*height, _w*2/zAxis*width, _h*2/zAxis*height);
}
document.body.appendChild(buffer.canvas)
}
buildWorld({objects, classes, tileMap}){
const world = this.world;
if(Array.isArray(objects)){
for(let i = objects.length - 1; i > -1; i --){
const object = objects[i];
const {name, array, amount, position, collision, args, area} = object;
let createClass;
if(!object.class){
createClass = Entity;
}
const _args = args ? args : [];
let pos;
if(position){
let p = amount;
if(array){
const positions = position.positions;
pos = function(){
p--;
return positions[p];
};
} else {
pos = function(){
return position.position;
};
}
}
if(array){
let _array = [];
for(let j = amount; j--;){
const instance = this.createEntity(createClass, ..._args);
instance.name = name;
if(position){
instance.move(...pos());
}
if(area){
instance.setSize(area);
}
_array.push(instance);
}
world.objects[name] = _array;
world.objectsArray.push(..._array);
}
}
}
return;
}
setup(game){
const {style: {backgroundColor, backgroundImage, stroke}, world, engine: {frameRate, update, render}, setup} = game; this.buildWorld(world); const {display, entitySystem, world: {objectsArray, objects}} = this; if(backgroundImage){ display.gl.canvas.style.background = `url(${backgroundImage})`; if(repeatX || repeatY){ console.log("not read yet"); } } this.frameRate = frameRate; let lastUpdated = 0; this.update = (time) =>{ let deltaTime = time - lastUpdated; lastUpdated = time; const speed = this.speed; this.timePassed += deltaTime*speed; for(let i = objectsArray.length; i--;){ const object = objectsArray[i]; if(object.delete){ objectsArray.splice(i, 1); } object.update(deltaTime/1000, speed); } update(deltaTime/1000, this); }; let lastRendered = 0; this.render = (timeStamp) => { const deltaTime = timeStamp - lastRendered; lastRendered = timeStamp; if(backgroundColor) display.clear(backgroundColor); const length = objectsArray.length; for(let i = objectsArray.length; i--; ){ const object = objectsArray[length - i - 1]; if(object.draw){ const updateFillers = Object.entries(object.updateFillers); const fillersLength = updateFillers.length; if(fillersLength){ for(let i = fillersLength; i--;){ const [func, args] = updateFillers[fillersLength - i - 1]; display[func + "Rect"](object, ...args); } object.updateFillers = {}; } display.drawBuffer(object); } } const speed = this.speed; const spriteSheets = display.spriteSheets; for(let i = spriteSheets.length; i--;){ spriteSheets[i].update(deltaTime/1000*speed); } render(display, this); }; setup(this, display, this.world); this.engine = new Engine(this.frameRate, this.update, this.render, 3); this.engine.start(); return game;
}
static async create({display: {canvas, width, height, zAxis}, homeURL}){
const display = await Display.create(canvas, width, height, zAxis);
return new Quixotic(display);
}
}
const fps = document.querySelector("#fps");
const minLength = innerWidth > innerHeight ? innerHeight : innerWidth;
const game = {
create: {
display: {
canvas: document.querySelector("#canvas"),
zAxis: 96,
width: minLength,
height: minLength,
},
homeURL: "/src"
},
style: {
backgroundColor: "#111122"
},
world: {
objects: [
{
name: "trees",
array: true,
amount: 5,
position: {
type: "set",
positions: [ [-37.5, 37.5], [0,0], [-37.5,-37.5], [37.5,-37.5], [37.5,37.5], [10,10], [15,10], [20,10], [25,10], [30,10]]
}
}
]
},
engine: {
frameRate: 1000/30,
update: function(deltaTime, engine){
fps.innerText = 1/deltaTime;
},
render: function(display){}
},
setup: function(engine, display, {objects: {trees}}){
trees.forEach(tree => {
tree.fill("#00ff00")
})
engine.createBackground(trees);
}
};
Quixotic.create(game.create)
.then(engine => {
engine.setup(game);
});
* {
box-sizing:border-box;
margin:0;
padding:0;
}
body {
background-color: #111c31;
overflow: hidden;
align-items:space-around;
display:grid;
height:100%;
width:100%;
}
#canvas {
background-color: #152646;
/* justify-self: center; */
}
#fps {
position: fixed;
color: white;
right: 0;
}
canvas {
position: fixed
}
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>webgl x 2dCanvas</title>
</head>
<body>
<canvas id="canvas" width="300" height="300"></canvas>
<p id = "fps"></p>
</body>
</html>
Here's code from line 374 where the problem is happening
createBackground(objects){ //method
const buffer = document.createElement("canvas").getContext("2d");
const bufferRect = this.createEntity();
let {zAxis, canvas: {width, height}} = this.display;
zAxis--; //zAxis is where the camera is at, currently 96, but with webgl the objects have to be 1 point lower, so 95.
const halfZ = zAxis/2;
let {coords: [x, y], area: [w, h]} = objects[objects.length - 1];
let [mX, mY, mW, mH] = [x, y, w, h];
for(let i = objects.length-1; i--;){
const {coords: [_x, _y], area: [_w, _h]} = objects[i];
x < _x ? _x : x;
y < _y ? _y : y;
if(mX < _x){
mX = _x;
mW = _w;
}
if(mY < _y){
mY = _y;
mH = _h;
}
}
buffer.canvas.width = ((mX-halfZ+mW*2)/zAxis+1)*width;
buffer.canvas.height = ((mY-halfZ+mH*2)/zAxis+1)*height;
for(let i = objects.length; i--;){
const {coords: [_x, _y], area: [_w, _h]} = objects[i];
buffer.fillRect(((_x-halfZ-_w*2)/zAxis+1)*width, ((_y-halfZ-_h*2)/zAxis+1)*height, _w*2/zAxis*width, _h*2/zAxis*height);
}
document.body.appendChild(buffer.canvas)
}
I have this function that takes objects that are being drawn with webgl on a 3d world with a couple vectors and matrices, basically I get all their positions and volumes to draw them on a 2d canvas, heres the result I got so far
the green squares are the ones being drawn with webgl and the black squares are the ones being draw on a canvas rendering 2d, the end result should be the black squares covering the green squares but my math is off somewhere.
The full code can be found here
https://github.com/bahaaaldin214/Quixotic-Engine/tree/test
The shaders are in src/modules/webgl/shaders
other information
camera position: 96,
green squares positions:
[
[-37.5, 37.5], //bottom left
[0,0], //center
[-37.5,-37.5], //top left
[37.5,-37.5], //bottom right
[37.5,37.5], //top right
]
Well now that I've seen the code. First off, my bad but I didn't make it clear you should post minimal code. There is lots of unneeded code. Also I'm not sure if that's your own math library or if it's paired down glmatrix. If it's the latter you can just <script src="cdn/to/glmatrix"></script> to use it.
In any case you're positioning the squares using a perspective matrix and view matrix (the camera) so you need to use the same math for the 2D canvas.
const worldViewProjection = mat4.create();
buffer.canvas.width = width;
buffer.canvas.height = height;
for (let i = objects.length; i--;) {
const {
coords: [_x, _y],
area: [_w, _h]
} = objects[i];
mat4.multiply(worldViewProjection, this.display.currentCamera.matrix, objects[i].matrix);
const points = [
[-_w / 2, -_h / 2, 0],
[ _w / 2, _h / 2, 0],
].map(p => {
const ndc = vec3.transformMat4([], p, worldViewProjection);
return [
(ndc[0] * 0.5 + 0.5) * width,
(ndc[1] * -0.5 + 0.5) * height,
];
});
const ww = points[1][0] - points[0][0];
const hh = points[1][1] - points[0][1];
buffer.strokeStyle = 'red';
buffer.strokeRect(...points[0], ww, hh);
}
const EPSILON = 0.000001;
const FRAGMENT_SHADER = ` precision highp float; varying highp vec2 vTextureCoord; varying lowp vec4 vColor; uniform sampler2D uSampler; uniform bool aUseText; void main(void) { if( aUseText ){ gl_FragColor = texture2D(uSampler, vTextureCoord); } else { gl_FragColor = vColor; } } `;
const VERTEX_SHADER = ` attribute vec4 aVertexPosition; attribute vec4 aVertexColor; attribute vec2 aTextureCoord; uniform mat4 uModelViewMatrix; uniform mat4 uProjectionMatrix; uniform mat3 uTextMatrix; uniform float uPointSize; varying lowp vec4 vColor; varying highp vec2 vTextureCoord; void main(void) { gl_PointSize = uPointSize; gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition; vColor = aVertexColor; vTextureCoord = (vec3(aTextureCoord, 1)*uTextMatrix).xy; } `;
mat4.moveToVec3 = function(out, v) {
out[12] = v[0];
out[13] = v[1];
out[14] = v[2];
};
class WebglEntity {
constructor() {
this.matrix = mat4.create();
this.coords = vec3.create();
}
translate(newCoords) {
const {
matrix,
coords
} = this;
mat4.translate(matrix, matrix, newCoords);
vec3.copy(coords, [matrix[12], matrix[13], matrix[14]]);
return this;
}
move(newCoords) {
const {
matrix,
coords
} = this;
vec3.copy(coords, newCoords);
mat4.moveToVec3(matrix, coords);
return this;
}
}
class Camera extends WebglEntity {
constructor(fieldOfView, aspect, zNear, zFar) {
super();
this.projection = mat4.perspective(mat4.create(), fieldOfView, aspect, zNear, zFar);
}
lookAt(lookAt) {
const {
matrix,
projection,
coords
} = this;
mat4.lookAt(matrix, coords, lookAt, [0, 1, 0]);
mat4.multiply(matrix, projection, matrix);
return this;
}
}
class Rect extends WebglEntity {
constructor() {
super();
this.positionsBuffer = undefined;
this.fragColorPos = undefined;
this.strokeColorPos = undefined;
this.strokePositionBuffer = undefined;
this.vertexAttribInfo = undefined;
this.vertextColorAttribInfo = undefined;
this.vertexCount = undefined;
this.textureInfo = undefined;
this.multiTextures = false;
this.strokeSize = 1;
this.fillers = {
fill: false,
texture: false,
stroke: false
};
}
setup(matrix, positionsBuffer, strokePositionBuffer, vertexAttribInfo, vertextColorAttribInfo, vertexCount) {
this.matrix = matrix;
this.positionsBuffer = positionsBuffer;
this.strokePositionBuffer = strokePositionBuffer;
this.vertexAttribInfo = vertexAttribInfo;
this.vertextColorAttribInfo = vertextColorAttribInfo;
this.vertexCount = vertexCount;
return this;
}
}
class Display {
constructor(gl, programInfo, zAxis, texture) {
this.gl = gl;
this.programInfo = programInfo;
this.canvas = gl.canvas;
this.currentCamera = new Camera(45 * Math.PI / 180, gl.canvas.width / gl.canvas.height, 0.1, 100.0);
this.currentCamera.translate([0, 0, zAxis]).lookAt([0, 0, 0]);
this.zAxis = zAxis;
this.drawZAxis = 0;
this.last = {};
texture.textAttribInfo = {
numComponents: 2,
type: gl.FLOAT,
normalize: false,
stride: 0,
offset: 0
};
this.texture = texture;
this.spriteSheets = [];
const context = texture.context;
const canvas = texture.canvas;
this.images = {};
}
clear(color) {
const gl = this.gl;
gl.clearColor(0.1, 0.1, 0.1, 1);
gl.clearDepth(1.0);
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}
rect(x, y, w, h) {
const {
rect,
stroke
} = this.createRectPos(w, h);
const square = new Rect();
square.setup(...this.getRectInfo(x, y, rect, stroke));
return square;
}
fillRect(rect, color) {
const {
createStaticDrawBuffer,
gl,
parseColor
} = this;
rect.fillers.fill = true;
if (color) {
rect.fragColorPos = createStaticDrawBuffer(gl, [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1]);
}
}
createRectPos(w, h) {
const rect = [w / 2, h / 2, -w / 2, h / 2, w / 2, -h / 2, -w / 2, -h / 2];
const stroke = [-w / 2, h / 2, w / 2, h / 2, w / 2, -h / 2, -w / 2, -h / 2, ];
return {
rect,
stroke
};
}
getRectInfo(x, y, rect, stroke) {
return this.createSquareBuffer(rect, stroke, [x, y, this.drawZAxis]);
}
createStaticDrawBuffer(gl, data) {
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
return buffer;
}
createSquareBuffer(positions, strokePosition, coords) {
const {
gl,
createStaticDrawBuffer
} = this;
const positionsBuffer = createStaticDrawBuffer(gl, positions);
const strokePositionBuffer = createStaticDrawBuffer(gl, strokePosition);
const modelViewMatrix = mat4.create();
mat4.translate(modelViewMatrix, modelViewMatrix, coords);
return [modelViewMatrix, positionsBuffer, strokePositionBuffer, this.createAttribInfo(2, gl.FLOAT, false, 0, 0), this.createAttribInfo(4, gl.FLOAT, false, 0, 0), positions.length / 2];
}
createAttribInfo(numComponents, type, normalize, stride, offset) {
return {
numComponents,
type,
normalize,
stride,
offset
};
}
enableAttrib(buffer, attrib, gl, {
numComponents,
type,
normalize,
stride,
offset
}) {
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.vertexAttribPointer(attrib, numComponents, type, normalize, stride, offset);
gl.enableVertexAttribArray(attrib);
}
drawBuffer(buffer) {
const {
gl,
drawTexture,
enableAttrib,
createStaticDrawBuffer,
currentCamera,
texture: {
context,
canvas,
textAttribInfo
},
programInfo: {
uniformLocations,
program,
attribLocations: {
vertexPosition,
vertexColor,
textureCoord
}
}
} = this;
const cameraMatrix = currentCamera.matrix;
const {
positionsBuffer,
fragColorPos,
strokeColorPos,
strokePositionBuffer,
matrix,
vertexAttribInfo,
vertextColorAttribInfo,
vertexCount,
fragTextPos,
fillers: {
fill,
stroke,
texture
},
strokeSize,
textureInfo,
multiTextures
} = buffer;
gl.uniformMatrix4fv(uniformLocations.projectionMatrix, false, cameraMatrix);
gl.uniformMatrix4fv(uniformLocations.modelViewMatrix, false, matrix);
if (fill) {
enableAttrib(positionsBuffer, vertexPosition, gl, vertexAttribInfo);
enableAttrib(fragColorPos, vertexColor, gl, vertextColorAttribInfo);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertexCount);
gl.disableVertexAttribArray(vertexColor);
}
}
static loadShader(gl, program, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
gl.attachShader(program, shader);
}
static async create(canvas, width, height, zAxis = 6) {
canvas.width = width;
canvas.height = height;
const gl = canvas.getContext("webgl");
const shaderProgram = gl.createProgram();
Display.loadShader(gl, shaderProgram, gl.VERTEX_SHADER, VERTEX_SHADER);
Display.loadShader(gl, shaderProgram, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);
gl.linkProgram(shaderProgram);
const programInfo = {
program: shaderProgram,
attribLocations: {
vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'),
vertexColor: gl.getAttribLocation(shaderProgram, 'aVertexColor'),
textureCoord: gl.getAttribLocation(shaderProgram, 'aTextureCoord'),
},
uniformLocations: {
projectionMatrix: gl.getUniformLocation(shaderProgram, 'uProjectionMatrix'),
modelViewMatrix: gl.getUniformLocation(shaderProgram, 'uModelViewMatrix'),
textMatrix: gl.getUniformLocation(shaderProgram, 'uTextMatrix'),
sampler: gl.getUniformLocation(shaderProgram, 'uSampler'),
useText: gl.getUniformLocation(shaderProgram, 'aUseText'),
pointSize: gl.getUniformLocation(shaderProgram, 'uPointSize'),
},
};
gl.useProgram(programInfo.program);
gl.uniform1f(programInfo.uniformLocations.pointSize, 1.0);
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
const textureBuffer = gl.createTexture();
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, textureBuffer);
gl.uniform1i(programInfo.uniformLocations.uSampler, 0);
const textureCanvas = document.createElement("canvas");
textureCanvas.width = 0;
textureCanvas.height = 0;
let texture = {
canvas: textureCanvas,
buffer: textureBuffer,
context: textureCanvas.getContext("2d"),
};
return new Display(gl, programInfo, zAxis, texture);
}
}
class Engine {
constructor(time_step, update, render, allowedSkippedFrames) {
this.accumulated_time = 0;
this.animation_frame_request = undefined, this.time = undefined, this.time_step = time_step, this.updated = false;
this.update = update;
this.render = render;
this.allowedSkippedFrames = allowedSkippedFrames;
this.run = this.run.bind(this);
this.end = false;
}
run(time_stamp) {
const {
accumulated_time,
time,
time_step,
updated,
update,
render,
allowedSkippedFrames,
end
} = this;
this.accumulated_time += time_stamp - time;
this.time = time_stamp;
if (accumulated_time > time_stamp * allowedSkippedFrames) {
this.accumulated_time = time_stamp;
}
while (this.accumulated_time >= time_step) {
this.accumulated_time -= time_step;
update(time_stamp);
this.updated = true;
}
if (updated) {
this.updated = false;
render(time_stamp);
}
if (end) {
return;
}
this.animation_frame_request = requestAnimationFrame(this.run);
}
start() {
this.accumulated_time = this.time_step;
this.time = performance.now();
this.animation_frame_request = requestAnimationFrame(this.run);
}
stop() {
this.end = true;
cancelAnimationFrame(this.animation_frame_request);
}
}
class Entity extends Rect {
constructor() {
super();
this.velocity = vec2.create();
this.area = undefined;
this.mass = 2;
this.updateFillers = {};
this.delete = false;
this.draw = true;
}
setup(w, h, ...args) {
this.area = vec2.fromValues(w, h);
super.setup(...args);
return this;
}
fill(...args) {
this.updateFillers.fill = args;
}
update(deltaTime, speed) {
return this;
}
move(x, y) {
super.move([x, y, this.coords[2]]);
return this;
}
}
class Quixotic {
constructor(display) {
this.display = display;
this.engine = undefined;
this.render = undefined;
this.update = undefined;
this.frameRate = undefined;
this.time = 0;
this.speed = 1;
this.world = {
objects: {},
objectsCollisionInfo: {},
objectsArray: [],
classesInfo: {}
};
this.timePassed = 0;
}
createEntity(Class, ...args) {
const display = this.display;
const {
rect,
stroke
} = display.createRectPos(5, 5);
Class = Class ? Class : Entity;
const className = Class.name;
if (className !== "Entity" && !Entity.prototype.isPrototypeOf(Class.prototype)) {
throw new TypeError("Expected extended class of Entity. Instead got: " + className);
}
let instance;
const {
objectsArray,
classesInfo,
objects
} = this.world;
const classInfo = classesInfo[className];
if (classInfo) {
if (classInfo.args) {
instance = new Class(...[...classInfo.args, ...args]);
} else {
instance = new Class(...args);
}
const name = classInfo.name;
if (Array.isArray(objects[name])) {
objects[name].push(instance);
instance.name = name;
} else {
console.warn("Didn't save object in world.objects object, object wouldn't detect collision");
}
} else {
instance = new Class(...args);
}
instance.setup(5, 5, ...display.getRectInfo(0, 0, rect, stroke, "#000"));
objectsArray.push(instance);
return instance;
}
createBackground(objects) {
const buffer = document.createElement("canvas").getContext("2d");
const bufferRect = this.createEntity();
let {
zAxis,
canvas: {
width,
height
}
} = this.display;
zAxis--;
const halfZ = zAxis / 2;
let {
coords: [x, y],
area: [w, h]
} = objects[objects.length - 1];
const worldViewProjection = mat4.create();
buffer.canvas.width = width;
buffer.canvas.height = height;
for (let i = objects.length; i--;) {
const {
coords: [_x, _y],
area: [_w, _h]
} = objects[i];
mat4.multiply(worldViewProjection, this.display.currentCamera.matrix, objects[i].matrix);
const points = [
[-_w / 2, -_h / 2, 0],
[_w / 2, _h / 2, 0],
].map(p => {
const ndc = vec3.transformMat4([], p, worldViewProjection);
return [
(ndc[0] * 0.5 + 0.5) * width,
(ndc[1] * -0.5 + 0.5) * height,
];
});
const ww = points[1][0] - points[0][0];
const hh = points[1][1] - points[0][1];
buffer.strokeStyle = 'red';
buffer.strokeRect(...points[0], ww, hh);
}
document.body.appendChild(buffer.canvas)
}
buildWorld({
objects,
classes,
tileMap
}) {
const world = this.world;
if (Array.isArray(objects)) {
for (let i = objects.length - 1; i > -1; i--) {
const object = objects[i];
const {
name,
array,
amount,
position,
collision,
args,
area
} = object;
let createClass;
if (!object.class) {
createClass = Entity;
}
const _args = args ? args : [];
let pos;
if (position) {
let p = amount;
if (array) {
const positions = position.positions;
pos = function() {
p--;
return positions[p];
};
} else {
pos = function() {
return position.position;
};
}
}
if (array) {
let _array = [];
for (let j = amount; j--;) {
const instance = this.createEntity(createClass, ..._args);
instance.name = name;
if (position) {
instance.move(...pos());
}
if (area) {
instance.setSize(area);
}
_array.push(instance);
}
world.objects[name] = _array;
world.objectsArray.push(..._array);
}
}
}
return;
}
setup(game) {
const {
style: {
backgroundColor,
backgroundImage,
stroke
},
world,
engine: {
frameRate,
update,
render
},
setup
} = game;
this.buildWorld(world);
const {
display,
entitySystem,
world: {
objectsArray,
objects
}
} = this;
if (backgroundImage) {
display.gl.canvas.style.background = `url(${backgroundImage})`;
if (repeatX || repeatY) {
console.log("not read yet");
}
}
this.frameRate = frameRate;
let lastUpdated = 0;
this.update = (time) => {
let deltaTime = time - lastUpdated;
lastUpdated = time;
const speed = this.speed;
this.timePassed += deltaTime * speed;
for (let i = objectsArray.length; i--;) {
const object = objectsArray[i];
if (object.delete) {
objectsArray.splice(i, 1);
}
object.update(deltaTime / 1000, speed);
}
update(deltaTime / 1000, this);
};
let lastRendered = 0;
this.render = (timeStamp) => {
const deltaTime = timeStamp - lastRendered;
lastRendered = timeStamp;
if (backgroundColor) display.clear(backgroundColor);
const length = objectsArray.length;
for (let i = objectsArray.length; i--;) {
const object = objectsArray[length - i - 1];
if (object.draw) {
const updateFillers = Object.entries(object.updateFillers);
const fillersLength = updateFillers.length;
if (fillersLength) {
for (let i = fillersLength; i--;) {
const [func, args] = updateFillers[fillersLength - i - 1];
display[func + "Rect"](object, ...args);
}
object.updateFillers = {};
}
display.drawBuffer(object);
}
}
const speed = this.speed;
const spriteSheets = display.spriteSheets;
for (let i = spriteSheets.length; i--;) {
spriteSheets[i].update(deltaTime / 1000 * speed);
}
render(display, this);
};
setup(this, display, this.world);
this.engine = new Engine(this.frameRate, this.update, this.render, 3);
this.engine.start();
return game;
}
static async create({
display: {
canvas,
width,
height,
zAxis
},
homeURL
}) {
const display = await Display.create(canvas, width, height, zAxis);
return new Quixotic(display);
}
}
const fps = document.querySelector("#fps");
const minLength = innerWidth > innerHeight ? innerHeight : innerWidth;
const game = {
create: {
display: {
canvas: document.querySelector("#canvas"),
zAxis: 96,
width: minLength,
height: minLength,
},
homeURL: "/src"
},
style: {
backgroundColor: "#111122"
},
world: {
objects: [{
name: "trees",
array: true,
amount: 5,
position: {
type: "set",
positions: [
[-37.5, 37.5],
[0, 0],
[-37.5, -37.5],
[37.5, -37.5],
[37.5, 37.5],
[10, 10],
[15, 10],
[20, 10],
[25, 10],
[30, 10]
]
}
}]
},
engine: {
frameRate: 1000 / 30,
update: function(deltaTime, engine) {
fps.innerText = 1 / deltaTime;
},
render: function(display) {}
},
setup: function(engine, display, {
objects: {
trees
}
}) {
trees.forEach(tree => {
tree.fill("#00ff00")
})
engine.createBackground(trees);
}
};
Quixotic.create(game.create)
.then(engine => {
engine.setup(game);
});
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: #111c31;
overflow: hidden;
align-items: space-around;
display: grid;
height: 100%;
width: 100%;
}
#canvas {
background-color: #152646;
/* justify-self: center; */
}
#fps {
position: fixed;
color: white;
right: 0;
}
canvas {
position: fixed
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.8.1/gl-matrix-min.js"></script>
<canvas id="canvas" width="300" height="300"></canvas>
<p id="fps"></p>
note: the code only works because the camera is not rotated, nor are the squares. If you did rotate the camera or the squares you'd need to draw triangles with canvas 2d after transforming each set of 3 vertices, just like WebGL does.
Related
WebGL fade-out does not work for white color
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>
Change the color in WebGL + JavaScript
I am using the following code to generate an animation of particles. I would like to change the colors of individual particles to random colors in each frame. Is it possible to do this and, if so, how can I achieve it? Additionally, is it possible to change the rotation of the particles in each render? Any help with either of these questions would be greatly appreciated. <!doctype html> <head> <title>Triangles</title> <style> html, body { background: #000; height: 100%; margin: 0; } canvas { width: 1280px; height: 720px; position: absolute; margin: auto; top: 0; right: 0; left: 0; bottom: 0; } </style> </head> <body> <script> 'use strict'; const triangleCount = 2e5; const antialias = true; const generateTriangles = (count, width, height) => { const coords = new Float32Array(9 * count); for (var i = 0; i < coords.length;) { const x = Math.random() * 2 - 1; const y = Math.random() * 2 - 1; const z = Math.random() * 2 - 1; const theta = Math.random() * Math.PI; const ax = 10 * Math.cos(theta) / width; const ay = 10 * Math.sin(theta) / height; const bx = 10 * Math.cos(theta + 0.1) / width; const by = 10 * Math.sin(theta + 0.1) / height; coords[i++] = x + ax; coords[i++] = y + ay; coords[i++] = z; coords[i++] = x + bx; coords[i++] = y + by; coords[i++] = z; coords[i++] = x - ax; coords[i++] = y - ay; coords[i++] = z; coords[i++] = x - ax; coords[i++] = y - ay; coords[i++] = z; coords[i++] = x - bx; coords[i++] = y - by; coords[i++] = z; coords[i++] = x + ax; coords[i++] = y + ay; coords[i++] = z; } return coords; }; const vertexShaderSource = ` precision lowp float; attribute vec3 aPosition; uniform float uWobble; void main() { float p = 0.1 / (0.3 * aPosition.z - 0.14 + 0.1 * uWobble); gl_Position = vec4(p * aPosition.x, p * aPosition.y, aPosition.z, 1); } `; const fragmentShaderSource = ` precision lowp float; void main() { float z = gl_FragCoord.z; gl_FragColor = vec4(1.5 * z, z * z, z, 1.7); } `; const canvas = document.createElement('canvas'); document.body.appendChild(canvas); canvas.width = canvas.clientWidth; canvas.height = canvas.clientHeight; const gl = canvas.getContext('webgl', { alpha: false, antialias }); const vertexShader = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vertexShader, vertexShaderSource); gl.compileShader(vertexShader); const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(fragmentShader, fragmentShaderSource); gl.compileShader(fragmentShader); const program = gl.createProgram(); gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); gl.linkProgram(program); gl.useProgram(program); const aVertexPosition = gl.getAttribLocation(program, 'aPosition'); gl.enableVertexAttribArray(aVertexPosition); const uWobble = gl.getUniformLocation(program, 'uWobble'); gl.uniform1f(uWobble, 1); const vertices = generateTriangles(triangleCount, canvas.width, canvas.height); const vertexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); gl.vertexAttribPointer(aVertexPosition, 3, gl.FLOAT, false, 0, 0); const render = (timestamp) => { requestAnimationFrame(render); gl.uniform1f(uWobble, Math.sin(0.00002 * timestamp)); gl.drawArrays(gl.TRIANGLES, 0, vertices.length / 3); }; window.requestAnimationFrame(render); </script> </body>
Here is solution to get semi-random color for each particle for each frame - change your fragmentShaderSource to below: const fragmentShaderSource = ` precision lowp float; uniform float uWobble; void main() { float r = fract(sin(uWobble*10000.0*gl_FragCoord.z)); float g = fract(cos(uWobble*10000.0*gl_FragCoord.z)*43758.5453); float b = fract(cos(uWobble*10000.0*gl_FragCoord.z)*12.9898); gl_FragColor = vec4(r, g, b, 1.7); } `; Working example here on fiddle (for easy edit) or below: 'use strict'; const triangleCount = 2e5; const antialias = true; const generateTriangles = (count, width, height) => { const coords = new Float32Array(9 * count); for (var i = 0; i < coords.length;) { const x = Math.random() * 2 - 1; const y = Math.random() * 2 - 1; const z = Math.random() * 2 - 1; const theta = Math.random() * Math.PI; const ax = 10 * Math.cos(theta) / width; const ay = 10 * Math.sin(theta) / height; const bx = 10 * Math.cos(theta + 0.1) / width; const by = 10 * Math.sin(theta + 0.1) / height; coords[i++] = x + ax; coords[i++] = y + ay; coords[i++] = z; coords[i++] = x + bx; coords[i++] = y + by; coords[i++] = z; coords[i++] = x - ax; coords[i++] = y - ay; coords[i++] = z; coords[i++] = x - ax; coords[i++] = y - ay; coords[i++] = z; coords[i++] = x - bx; coords[i++] = y - by; coords[i++] = z; coords[i++] = x + ax; coords[i++] = y + ay; coords[i++] = z; } return coords; }; const vertexShaderSource = ` precision lowp float; attribute vec3 aPosition; uniform float uWobble; void main() { float p = 0.1 / (0.3 * aPosition.z - 0.14 + 0.1 * uWobble); gl_Position = vec4(p * aPosition.x, p * aPosition.y, aPosition.z, 1); } `; const fragmentShaderSource = ` precision lowp float; uniform float uWobble; void main() { float r = fract(sin(uWobble*10000.0*gl_FragCoord.z)); float g = fract(cos(uWobble*10000.0*gl_FragCoord.z)*43758.5453); float b = fract(cos(uWobble*10000.0*gl_FragCoord.z)*12.9898); gl_FragColor = vec4(r, g, b, 1.7); } `; const canvas = document.createElement('canvas'); document.body.appendChild(canvas); canvas.width = canvas.clientWidth; canvas.height = canvas.clientHeight; const gl = canvas.getContext('webgl', { alpha: false, antialias }); const vertexShader = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vertexShader, vertexShaderSource); gl.compileShader(vertexShader); const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(fragmentShader, fragmentShaderSource); gl.compileShader(fragmentShader); const program = gl.createProgram(); gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); gl.linkProgram(program); gl.useProgram(program); const aVertexPosition = gl.getAttribLocation(program, 'aPosition'); gl.enableVertexAttribArray(aVertexPosition); const uWobble = gl.getUniformLocation(program, 'uWobble'); gl.uniform1f(uWobble, 1); const vertices = generateTriangles(triangleCount, canvas.width, canvas.height); const vertexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); gl.vertexAttribPointer(aVertexPosition, 3, gl.FLOAT, false, 0, 0); const render = (timestamp) => { requestAnimationFrame(render); gl.uniform1f(uWobble, Math.sin(0.00002 * timestamp)); gl.drawArrays(gl.TRIANGLES, 0, vertices.length / 3); }; window.requestAnimationFrame(render); html, body { background: #000; height: 100%; margin: 0; } canvas { width: 1280px; height: 720px; position: absolute; margin: auto; top: 0; right: 0; left: 0; bottom: 0; }
How to get rid of gray overlay on Three.js transition?
I have a transition between two pngs using three.js. The images have a white background, but they appear to have a gray tint. I know it is probably something obvious but I can't find where in the code this is specified. How do I get rid of this? Here is a demo on Codepen: https://codepen.io/mastroneel/pen/Oazzyo window.onload = init; console.ward = function() {}; // what warnings? function init() { var root = new THREERoot({ createCameraControls: !true, antialias: (window.devicePixelRatio === 1), fov: 80 }); root.renderer.setClearColor(0x000000, 0); root.renderer.setPixelRatio(window.devicePixelRatio || 1); root.camera.position.set(0, 0, 60); var width = 100; var height = 100; var slide = new Slide(width, height, 'out'); var l1 = new THREE.ImageLoader(); l1.setCrossOrigin('Anonymous'); slide.setImage(l1.load('https://image.ibb.co/f6mVsA/helmet.png')); root.scene.add(slide); var slide2 = new Slide(width, height, 'in'); var l2 = new THREE.ImageLoader(); l2.setCrossOrigin('Anonymous'); slide2.setImage(l2.load('https://image.ibb.co/mb1KkV/player.png')); root.scene.add(slide2); var tl = new TimelineMax({ repeat: -1, repeatDelay: 1.0, yoyo: true }); tl.add(slide.transition(), 0); tl.add(slide2.transition(), 0); createTweenScrubber(tl); window.addEventListener('keyup', function(e) { if (e.keyCode === 80) { tl.paused(!tl.paused()); } }); } //////////////////// // CLASSES //////////////////// function Slide(width, height, animationPhase) { var plane = new THREE.PlaneGeometry(width, height, width * 2, height * 2); THREE.BAS.Utils.separateFaces(plane); var geometry = new SlideGeometry(plane); geometry.bufferUVs(); var aAnimation = geometry.createAttribute('aAnimation', 2); var aStartPosition = geometry.createAttribute('aStartPosition', 3); var aControl0 = geometry.createAttribute('aControl0', 3); var aControl1 = geometry.createAttribute('aControl1', 3); var aEndPosition = geometry.createAttribute('aEndPosition', 3); var i, i2, i3, i4, v; var minDuration = 0.8; var maxDuration = 1.2; var maxDelayX = 0.9; var maxDelayY = 0.125; var stretch = 0.11; this.totalDuration = maxDuration + maxDelayX + maxDelayY + stretch; var startPosition = new THREE.Vector3(); var control0 = new THREE.Vector3(); var control1 = new THREE.Vector3(); var endPosition = new THREE.Vector3(); var tempPoint = new THREE.Vector3(); function getControlPoint0(centroid) { var signY = Math.sign(centroid.y); tempPoint.x = THREE.Math.randFloat(0.1, 0.3) * 50; tempPoint.y = signY * THREE.Math.randFloat(0.1, 0.3) * 70; tempPoint.z = THREE.Math.randFloatSpread(20); return tempPoint; } function getControlPoint1(centroid) { var signY = Math.sign(centroid.y); tempPoint.x = THREE.Math.randFloat(0.3, 0.6) * 50; tempPoint.y = -signY * THREE.Math.randFloat(0.3, 0.6) * 70; tempPoint.z = THREE.Math.randFloatSpread(20); return tempPoint; } for (i = 0, i2 = 0, i3 = 0, i4 = 0; i < geometry.faceCount; i++, i2 += 6, i3 += 9, i4 += 12) { var face = plane.faces[i]; var centroid = THREE.BAS.Utils.computeCentroid(plane, face); // animation var duration = THREE.Math.randFloat(minDuration, maxDuration); var delayX = THREE.Math.mapLinear(centroid.x, -width * 0.5, width * 0.5, 0.0, maxDelayX); var delayY; if (animationPhase === 'in') { delayY = THREE.Math.mapLinear(Math.abs(centroid.y), 0, height * 0.5, 0.0, maxDelayY) } else { delayY = THREE.Math.mapLinear(Math.abs(centroid.y), 0, height * 0.5, maxDelayY, 0.0) } for (v = 0; v < 6; v += 2) { aAnimation.array[i2 + v] = delayX + delayY + (Math.random() * stretch * duration); aAnimation.array[i2 + v + 1] = duration; } // positions endPosition.copy(centroid); startPosition.copy(centroid); if (animationPhase === 'in') { control0.copy(centroid).sub(getControlPoint0(centroid)); control1.copy(centroid).sub(getControlPoint1(centroid)); } else { // out control0.copy(centroid).add(getControlPoint0(centroid)); control1.copy(centroid).add(getControlPoint1(centroid)); } for (v = 0; v < 9; v += 3) { aStartPosition.array[i3 + v] = startPosition.x; aStartPosition.array[i3 + v + 1] = startPosition.y; aStartPosition.array[i3 + v + 2] = startPosition.z; aControl0.array[i3 + v] = control0.x; aControl0.array[i3 + v + 1] = control0.y; aControl0.array[i3 + v + 2] = control0.z; aControl1.array[i3 + v] = control1.x; aControl1.array[i3 + v + 1] = control1.y; aControl1.array[i3 + v + 2] = control1.z; aEndPosition.array[i3 + v] = endPosition.x; aEndPosition.array[i3 + v + 1] = endPosition.y; aEndPosition.array[i3 + v + 2] = endPosition.z; } } var material = new THREE.BAS.BasicAnimationMaterial({ shading: THREE.FlatShading, side: THREE.DoubleSide, uniforms: { uTime: { type: 'f', value: 0 } }, shaderFunctions: [ THREE.BAS.ShaderChunk['cubic_bezier'], //THREE.BAS.ShaderChunk[(animationPhase === 'in' ? 'ease_out_cubic' : 'ease_in_cubic')], THREE.BAS.ShaderChunk['ease_in_out_cubic'], THREE.BAS.ShaderChunk['quaternion_rotation'] ], shaderParameters: [ 'uniform float uTime;', 'attribute vec2 aAnimation;', 'attribute vec3 aStartPosition;', 'attribute vec3 aControl0;', 'attribute vec3 aControl1;', 'attribute vec3 aEndPosition;', ], shaderVertexInit: [ 'float tDelay = aAnimation.x;', 'float tDuration = aAnimation.y;', 'float tTime = clamp(uTime - tDelay, 0.0, tDuration);', 'float tProgress = ease(tTime, 0.0, 1.0, tDuration);' //'float tProgress = tTime / tDuration;' ], shaderTransformPosition: [ (animationPhase === 'in' ? 'transformed *= tProgress;' : 'transformed *= 1.0 - tProgress;'), 'transformed += cubicBezier(aStartPosition, aControl0, aControl1, aEndPosition, tProgress);' ] }, { map: new THREE.Texture(), }); THREE.Mesh.call(this, geometry, material); this.frustumCulled = false; } Slide.prototype = Object.create(THREE.Mesh.prototype); Slide.prototype.constructor = Slide; Object.defineProperty(Slide.prototype, 'time', { get: function() { return this.material.uniforms['uTime'].value; }, set: function(v) { this.material.uniforms['uTime'].value = v; } }); Slide.prototype.setImage = function(image) { this.material.uniforms.map.value.image = image; this.material.uniforms.map.value.needsUpdate = true; }; Slide.prototype.transition = function() { return TweenMax.fromTo(this, 3.0, { time: 0.0 }, { time: this.totalDuration, ease: Power0.easeInOut }); }; function SlideGeometry(model) { THREE.BAS.ModelBufferGeometry.call(this, model); } SlideGeometry.prototype = Object.create(THREE.BAS.ModelBufferGeometry.prototype); SlideGeometry.prototype.constructor = SlideGeometry; SlideGeometry.prototype.bufferPositions = function() { var positionBuffer = this.createAttribute('position', 3).array; for (var i = 0; i < this.faceCount; i++) { var face = this.modelGeometry.faces[i]; var centroid = THREE.BAS.Utils.computeCentroid(this.modelGeometry, face); var a = this.modelGeometry.vertices[face.a]; var b = this.modelGeometry.vertices[face.b]; var c = this.modelGeometry.vertices[face.c]; positionBuffer[face.a * 3] = a.x - centroid.x; positionBuffer[face.a * 3 + 1] = a.y - centroid.y; positionBuffer[face.a * 3 + 2] = a.z - centroid.z; positionBuffer[face.b * 3] = b.x - centroid.x; positionBuffer[face.b * 3 + 1] = b.y - centroid.y; positionBuffer[face.b * 3 + 2] = b.z - centroid.z; positionBuffer[face.c * 3] = c.x - centroid.x; positionBuffer[face.c * 3 + 1] = c.y - centroid.y; positionBuffer[face.c * 3 + 2] = c.z - centroid.z; } }; function THREERoot(params) { params = utils.extend({ fov: 60, zNear: 10, zFar: 100000, createCameraControls: true }, params); this.renderer = new THREE.WebGLRenderer({ antialias: params.antialias, alpha: true }); this.renderer.setPixelRatio(Math.min(2, window.devicePixelRatio || 1)); document.getElementById('three-container').appendChild(this.renderer.domElement); this.camera = new THREE.PerspectiveCamera( params.fov, window.innerWidth / window.innerHeight, params.zNear, params.zfar ); this.scene = new THREE.Scene(); if (params.createCameraControls) { this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement); } this.resize = this.resize.bind(this); this.tick = this.tick.bind(this); this.resize(); this.tick(); window.addEventListener('resize', this.resize, false); } THREERoot.prototype = { tick: function() { this.update(); this.render(); requestAnimationFrame(this.tick); }, update: function() { this.controls && this.controls.update(); }, render: function() { this.renderer.render(this.scene, this.camera); }, resize: function() { this.camera.aspect = window.innerWidth / window.innerHeight; this.camera.updateProjectionMatrix(); this.renderer.setSize(window.innerWidth, window.innerHeight); } }; //////////////////// // UTILS //////////////////// var utils = { extend: function(dst, src) { for (var key in src) { dst[key] = src[key]; } return dst; }, randSign: function() { return Math.random() > 0.5 ? 1 : -1; }, ease: function(ease, t, b, c, d) { return b + ease.getRatio(t / d) * c; }, fibSpherePoint: (function() { var vec = { x: 0, y: 0, z: 0 }; var G = Math.PI * (3 - Math.sqrt(5)); return function(i, n, radius) { var step = 2.0 / n; var r, phi; vec.y = i * step - 1 + (step * 0.5); r = Math.sqrt(1 - vec.y * vec.y); phi = i * G; vec.x = Math.cos(phi) * r; vec.z = Math.sin(phi) * r; radius = radius || 1; vec.x *= radius; vec.y *= radius; vec.z *= radius; return vec; } })(), spherePoint: (function() { return function(u, v) { u === undefined && (u = Math.random()); v === undefined && (v = Math.random()); var theta = 2 * Math.PI * u; var phi = Math.acos(2 * v - 1); var vec = {}; vec.x = (Math.sin(phi) * Math.cos(theta)); vec.y = (Math.sin(phi) * Math.sin(theta)); vec.z = (Math.cos(phi)); return vec; } })() }; function createTweenScrubber(tween, seekSpeed) { seekSpeed = seekSpeed || 0.001; function stop() { TweenMax.to(tween, 1, { timeScale: 0 }); } function resume() { TweenMax.to(tween, 1, { timeScale: 1 }); } function seek(dx) { var progress = tween.progress(); var p = THREE.Math.clamp((progress + (dx * seekSpeed)), 0, 1); tween.progress(p); } var _cx = 0; // desktop var mouseDown = false; document.body.style.cursor = 'pointer'; window.addEventListener('mousedown', function(e) { mouseDown = true; document.body.style.cursor = 'ew-resize'; _cx = e.clientX; stop(); }); window.addEventListener('mouseup', function(e) { mouseDown = false; document.body.style.cursor = 'pointer'; resume(); }); window.addEventListener('mousemove', function(e) { if (mouseDown === true) { var cx = e.clientX; var dx = cx - _cx; _cx = cx; seek(dx); } }); // mobile window.addEventListener('touchstart', function(e) { _cx = e.touches[0].clientX; stop(); e.preventDefault(); }); window.addEventListener('touchend', function(e) { resume(); e.preventDefault(); }); window.addEventListener('touchmove', function(e) { var cx = e.touches[0].clientX; var dx = cx - _cx; _cx = cx; seek(dx); e.preventDefault(); }); } body { margin: 0; background: #fff; } canvas { background: #fff; } <script src="//cdnjs.cloudflare.com/ajax/libs/three.js/r75/three.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/TweenMax.min.js"></script> <script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/175711/bas.js"></script> <script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/175711/OrbitControls-2.js"></script> <div id="three-container"></div> Thanks in advance!
WebGL - importing MD2 Model, problems with textures
I'm making a webgl demo with md2 models. I know that it is an old format but I need to use only md2. I read the documentation for the md2 format. I watched how this site works: and used part of it's sources in my program (i saved the author's name in the comments) I made vertex loading, the model loads great! But when I'm trying to map the texutre, something strange happens: First, I thought that it's the problem of fragment shader, but I made an export by using assimp2json, used the data from it and it was drawn as it should draw. The problem is that assimp2json changed the order vertices in triangles and uvs order, so I can't debug the program using it. Maybe, someone could help to find the bug, to point to the error in my code? P. S. since there is no animation, I only use the first frame The most interesting is that if I will pass unindexed data (just the uvs from the file) it looks more right than the indexed textures: The problem is that it is wrong in some places, like here: Full source code and models: ShaderProgram.js class ShaderProgram { constructor(gl, VSSource, FSSource) // VS - Vertex Shader, FS - Fragment Shader { this.gl = gl; let vertexShader = this.getShader(VSSource, gl.VERTEX_SHADER); let fragmentShader = this.getShader(FSSource, gl.FRAGMENT_SHADER); this.shaderProgram = gl.createProgram(); gl.attachShader(this.shaderProgram, vertexShader); gl.attachShader(this.shaderProgram, fragmentShader); gl.linkProgram(this.shaderProgram); if (!gl.getProgramParameter(this.shaderProgram, gl.LINK_STATUS)) { alert("Can't load shaders"); } this.enableAttributes(); this.getUniforms(); } getShader(source, type) { let gl = this.gl; let shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { alert("Error compilation: " + gl.getShaderInfoLog(shader)); gl.deleteShader(shader); return null; } return shader; } getUniforms() { this.model = gl.getUniformLocation(this.shaderProgram, "model"); this.view = gl.getUniformLocation(this.shaderProgram, "view"); this.projection = gl.getUniformLocation(this.shaderProgram, "projection"); } enableAttributes() { let gl = this.gl; let shaderProgram = this.shaderProgram; } use() { this.gl.useProgram(this.shaderProgram); } } Textures.js function initTexture(filename) { let texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 0, 255])); let image = new Image(); image.onload = function() { handleTextureLoaded(image, texture); } image.src = filename; return texture; } function handleTextureLoaded(image, texture) { gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); gl.generateMipmap(gl.TEXTURE_2D); gl.bindTexture(gl.TEXTURE_2D, null); } MD2 Import: // BinaryReader // Refactored by Vjeux <vjeuxx#gmail.com> // http://blog.vjeux.com/2010/javascript/javascript-binary-reader.html // Original //+ Jonas Raoni Soares Silva //# http://jsfromhell.com/classes/binary-parser [rev. #1] BinaryReader = function (data) { this._buffer = data; this._pos = 0; }; BinaryReader.prototype = { /* Public */ readInt8: function (){ return this._decodeInt(8, true); }, readUInt8: function (){ return this._decodeInt(8, false); }, readInt16: function (){ return this._decodeInt(16, true); }, readUInt16: function (){ return this._decodeInt(16, false); }, readInt32: function (){ return this._decodeInt(32, true); }, readUInt32: function (){ return this._decodeInt(32, false); }, readFloat: function (){ return this._decodeFloat(23, 8); }, readDouble: function (){ return this._decodeFloat(52, 11); }, readChar: function () { return this.readString(1); }, readString: function (length) { this._checkSize(length * 8); var result = this._buffer.substr(this._pos, length); this._pos += length; return result; }, seek: function (pos) { this._pos = pos; this._checkSize(0); }, getPosition: function () { return this._pos; }, getSize: function () { return this._buffer.length; }, /* Private */ _decodeFloat: function(precisionBits, exponentBits) { return this._decodeFloat2(precisionBits, exponentBits); var length = precisionBits + exponentBits + 1; var size = length >> 3; this._checkSize(length); var bias = Math.pow(2, exponentBits - 1) - 1; var signal = this._readBits(precisionBits + exponentBits, 1, size); var exponent = this._readBits(precisionBits, exponentBits, size); var significand = 0; var divisor = 2; var curByte = length + (-precisionBits >> 3) - 1; do { var byteValue = this._readByte(++curByte, size); var startBit = precisionBits % 8 || 8; var mask = 1 << startBit; while (mask >>= 1) { if (byteValue & mask) { significand += 1 / divisor; } divisor *= 2; } } while (precisionBits -= startBit); this._pos += size; return exponent == (bias << 1) + 1 ? significand ? NaN : signal ? -Infinity : +Infinity : (1 + signal * -2) * (exponent || significand ? !exponent ? Math.pow(2, -bias + 1) * significand : Math.pow(2, exponent - bias) * (1 + significand) : 0); }, // I added this because _decodeFloat gave me some real inaccuarate results? -Terry Butler _decodeFloat2: function(precisionBits, exponentBits) { var length = precisionBits + exponentBits + 1; var value = this._decodeInt(length); var sign = (value >> 31) & 0x1; var allZero = 1; var mantissa = 0.0; var exponent = 0.0; // Mantissa for (var i = 22; i > -1; i--) { var test = 1.0 / Math.pow(2, 23-i); if ((value >> i & 0x1) == 1) { mantissa += test; allZero = 0; } } if (allZero == 0) mantissa += 1.0; // Exponent for (var i = 30; i > 22; i--) { var test = Math.pow(2, i - 23); if ((value >> i & 0x1) == 1) { exponent += test; } } exponent -= 127.0; // var total = Math.pow(2.0, exponent) * mantissa; // if (sign == 1) { total *= -1.0; } return total; }, _decodeInt: function(bits, signed){ var x = this._readBits(0, bits, bits / 8), max = Math.pow(2, bits); var result = signed && x >= max / 2 ? x - max : x; this._pos += bits / 8; return result; }, //shl fix: Henri Torgemane ~1996 (compressed by Jonas Raoni) _shl: function (a, b){ for (++b; --b; a = ((a %= 0x7fffffff + 1) & 0x40000000) == 0x40000000 ? a * 2 : (a - 0x40000000) * 2 + 0x7fffffff + 1); return a; }, _readByte: function (i, size) { return this._buffer.charCodeAt(this._pos + size - i - 1) & 0xff; }, _readBits: function (start, length, size) { var offsetLeft = (start + length) % 8; var offsetRight = start % 8; var curByte = size - (start >> 3) - 1; var lastByte = size + (-(start + length) >> 3); var diff = curByte - lastByte; var sum = (this._readByte(curByte, size) >> offsetRight) & ((1 << (diff ? 8 - offsetRight : length)) - 1); if (diff && offsetLeft) { sum += (this._readByte(lastByte++, size) & ((1 << offsetLeft) - 1)) << (diff-- << 3) - offsetRight; } while (diff) { sum += this._shl(this._readByte(lastByte++, size), (diff-- << 3) - offsetRight); } return sum; }, _checkSize: function (neededBits) { if (!(this._pos + Math.ceil(neededBits / 8) < this._buffer.length)) { throw new Error("Index out of bound"); } } }; /** * #author oosmoxiecode * based on http://www.terrybutler.co.uk/web-development/html5-canvas-md2- renderer/ * and * http://tfc.duke.free.fr/coding/md2-specs-en.html * * dependant on binaryReader.js * * Returns a object like: {string: json_string, info: {status: "Success", faces: 10, vertices: 10, frames: 5 }} * **/ // Library is modified for this program by me function MD2_converter (file) { var scope = this; // Create the Binary Reader var reader = new BinaryReader(file); // Setup var header = {}; var frames = []; var st = []; var triag = []; var string = ""; var info = {}; var returnObject = {string: string, info: info}; // Ident and version header.ident = reader.readString(4); header.version = reader.readInt32(); // Valid MD2 file? if (header.ident != "IDP2" || header.version != 8) { info.status = "Not a valid MD2 file"; return returnObject; } // header header.skinwidth = reader.readInt32(); // texture width header.skinheight = reader.readInt32(); // texture height header.framesize = reader.readInt32(); // size in bytes of a frame header.num_skins = reader.readInt32(); // number of skins header.num_vertices = reader.readInt32(); // number of vertices per frame header.num_st = reader.readInt32(); // number of texture coordinates header.num_tris = reader.readInt32(); // number of triangles header.num_glcmds = reader.readInt32(); // number of opengl commands header.num_frames = reader.readInt32(); // number of frames header.offset_skins = reader.readInt32(); // offset skin data header.offset_st = reader.readInt32(); // offset texture coordinate data header.offset_tris = reader.readInt32(); // offset triangle data header.offset_frames = reader.readInt32(); // offset frame data header.offset_glcmds = reader.readInt32(); // offset OpenGL command data header.offset_end = reader.readInt32(); // offset end of file // faulty size if (reader.getSize() != header.offset_end) { info.status = "Corrupted MD2 file"; return returnObject; } // texture coordinates let count = 0; reader.seek(header.offset_st); for (var i = 0; i < header.num_st; i++) { var s = reader.readInt16(); var t = reader.readInt16(); st[i] = { s: s / header.skinwidth, t: t / header.skinheight }; } reader.seek(header.offset_tris); for (var i = 0; i < header.num_tris; i++) { var a = reader.readInt16(); var b = reader.readInt16(); var c = reader.readInt16(); var uva_i = reader.readUInt16(); var uvb_i = reader.readUInt16(); var uvc_i = reader.readUInt16(); triag[i] = {}; triag[i].vertex = []; triag[i].st = []; triag[i].vertex[0] = a; triag[i].vertex[1] = b; triag[i].vertex[2] = c; triag[i].st[0] = uva_i; triag[i].st[1] = uvb_i; triag[i].st[2] = uvc_i; } // frames reader.seek(header.offset_frames); for (var f = 0; f < header.num_frames; f++) { var frame = {}; frame.name = ""; frame.scale = {}; frame.translate = {}; frame.scale.x = reader.readFloat(); frame.scale.y = reader.readFloat(); frame.scale.z = reader.readFloat(); frame.translate.x = reader.readFloat(); frame.translate.y = reader.readFloat(); frame.translate.z = reader.readFloat(); frame.vertices = []; frame.name = reader.readString(16).replace(/[^a-z0-9]/gi,''); // 4+4+4 4+4+4 (12 + 12) = 24 + 16 = 40 for (var v = 0; v < header.num_vertices; v++) { var tempX = reader.readUInt8(); var tempY = reader.readUInt8(); var tempZ = reader.readUInt8(); var normal = reader.readUInt8(); var xx = frame.scale.x * tempX + frame.translate.x; var yy = frame.scale.z * tempZ + frame.translate.z; var zz = frame.scale.y * tempY + frame.translate.y; let vertex = []; vertex[0] = xx; vertex[1] = yy; vertex[2] = zz; frame.vertices.push(vertex); } frames.push(frame); } let res = {}; res.st = st; res.triag = triag; res.frame = []; for (var i=0; i<frames.length; ++i ) res.frame[i]=frames[i]; res.faces_count = header.num_tris; res.vertices_count = header.num_vertices; res.frames_count = header.num_frames; return res; } main.js let gl; let shaderProgram; let firstMouse = true; let mouseDown = false; let lastTime = 0; let deltaTime; let test; let lastX= 0; let lastY = 0; let DIRS = { Forward: 0, Backward: 1, Left: 2, Right: 3 }; let KeyCodes = { Up: 38, Down : 40, Left : 37, Right : 39, W : 87, S : 83, A : 65, D : 68, Q : 81, E : 69, PageDown : 34, PageUp : 33 }; let pressedKeys = []; let vertexShaderSource = ` precision highp float; attribute vec3 aPos; attribute vec2 aTexCoord; uniform mat4 model; uniform mat4 view; uniform mat4 projection; varying vec2 vTexCoord; void main(void) { gl_Position = projection * view * model * vec4(aPos, 1.0); vTexCoord = aTexCoord; } `; let fragmentShaderSource = ` precision highp float; varying highp vec2 vTexCoord; uniform sampler2D uSampler; void main(void) { gl_FragColor = texture2D(uSampler, vec2(vTexCoord.s,1.0-vTexCoord.t)); } `; let camera; function initGL() { let canvas = document.getElementById("Canvas3D"); canvas.width = window.innerWidth; canvas.height = window.innerHeight; try { gl = canvas.getContext("webgl2") || canvas.getContext("experimental-webgl2"); } catch(e) { alert("Your browser don't support WebGL"); } gl.viewportWidth = canvas.width; gl.viewportHeight = canvas.height; gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight); gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LESS); shaderProgram = new ShaderProgram(gl,vertexShaderSource,fragmentShaderSource); shaderProgram.use(); camera = { Init(shaderProgram) { this.shaderProgram = shaderProgram; this.pos = vec3.create(); this.up = vec3.create(); this.front = vec3.create(); this.right = vec3.create(); this.WorldUp = vec3.create(); this.pos = [51.656294013839215, 36.9293564686086, -28.23351748054847]; this.front = [-0.7667674915573891, -0.6401096994849556, 0.04824092159224934]; this.up = [-0.6388465891741784, 0.7682835235935235, 0.040192821190338686]; this.WorldUp = [0,1,0]; this.yaw = 176.39999999999998; this.pitch = -39.8; this.speed = 25; this.sensivity = 0.1; this.zoom = 100; this.view = mat4.create(); this.projection = mat4.create(); mat4.perspective(this.projection, Math.PI/180.0*this.zoom, gl.viewportWidth/gl.viewportHeight, 0.1, 200); this.shaderProgram.gl.uniformMatrix4fv(this.shaderProgram.projection, false, this.projection); this.updateCameraVectors(); this.updateView(); }, updateView: function() { let tmp = vec3.create(); vec3.add(tmp, this.pos, this.front); mat4.lookAt(this.view, this.pos, tmp, this.up); this.shaderProgram.gl.uniformMatrix4fv(this.shaderProgram.view, false, this.view); }, processKeyboard(deltaTime) { let velocity = this.speed * deltaTime; let tmp = vec3.create(); if (pressedKeys[KeyCodes.W] || pressedKeys[KeyCodes.Up]) { vec3.scale(tmp, this.front, velocity); vec3.add(this.pos,this.pos,tmp); } if (pressedKeys[KeyCodes.S] || pressedKeys[KeyCodes.Down]) { velocity *=(-1); vec3.scale(tmp, this.front, velocity); vec3.add(this.pos,this.pos,tmp); } if (pressedKeys[KeyCodes.A] || pressedKeys[KeyCodes.Left]) { velocity *=(-1); vec3.scale(tmp, this.right, velocity); vec3.add(this.pos,this.pos,tmp); } if (pressedKeys[KeyCodes.D] || pressedKeys[KeyCodes.Right]) { vec3.scale(tmp, this.right, velocity); vec3.add(this.pos,this.pos,tmp); } }, processMouseMovement(xoffset, yoffset) { this.yaw += (xoffset*this.sensivity); this.pitch += (yoffset*this.sensivity); if (this.pitch > 89.0) this.pitch = 89.0; if (this.pitch < -89.0) this.pitch = -89.0; this.updateCameraVectors(); }, updateCameraVectors() { let ToRads = Math.PI/180; let yaw = this.yaw; let pitch = this.pitch; this.front[0] = Math.cos(ToRads*yaw) * Math.cos(ToRads*pitch); this.front[1] = Math.sin(ToRads*pitch); this.front[2] = Math.sin(ToRads*yaw) * Math.cos(ToRads*pitch); vec3.normalize(this.front, this.front); vec3.cross(this.right, this.front, this.WorldUp); vec3.normalize(this.right, this.right); vec3.cross(this.up, this.right, this.front); vec3.normalize(this.up, this.up); } }; function load_binary_resource(url) { let req = new XMLHttpRequest(); req.open('GET', url, false); req.overrideMimeType('text/plain; charset=x-user-defined'); // No unicode data req.send(null); if (req.status != 200) return ''; return req.responseText; } let file = load_binary_resource('../models/rhino/Tris.md2'); let res = MD2_converter(file); let texture = initTexture("../models/rhino/rhino.png"); let verts = []; let inds = []; let uvs = []; for (let i =0;i<res.frame[0].vertices.length;i++) { let vert = res.frame[0].vertices[i]; verts.push(vert[0]); verts.push(vert[1]); verts.push(vert[2]); } for (let i =0;i<res.triag.length;i++) { let triag = res.triag[i]; inds.push(triag.vertex[0]); inds.push(triag.vertex[1]); inds.push(triag.vertex[2]); uvs.push(res.st[triag.st[0]].s); uvs.push(res.st[triag.st[0]].t); uvs.push(res.st[triag.st[1]].s); uvs.push(res.st[triag.st[1]].t); uvs.push(res.st[triag.st[2]].s); uvs.push(res.st[triag.st[2]].t); } test = new Drawable(shaderProgram, verts, inds, uvs, texture); camera.Init(shaderProgram); requestAnimationFrame(gameCycle); } function recalculateFPS(gotTime) { deltaTime = (gotTime - lastTime)/1000; lastTime = gotTime; } function gameCycle(gotTime) { recalculateFPS(gotTime); gl.clearColor(0,0,0,1); gl.clear(gl.COLOR_BUFFER_BIT); camera.processKeyboard(deltaTime); camera.updateView(); test.draw(); requestAnimationFrame(gameCycle); } window.onload=function() { initGL(); }; document.onkeydown = function(e) { pressedKeys[e.keyCode]=true; }; document.onkeyup = function(e) { pressedKeys[e.keyCode]=false; }; document.body.onmousedown = function(event) { mouseDown = true; lastX = event.clientX; lastY = event.clientY; }; document.body.onmouseup = function(event) { mouseDown = false; }; document.body.onmouseout = function(event) { mouseDown = false; }; document.body.onmousemove = function (e) { if (!mouseDown) return; let xpos = e.clientX; let ypos = e.clientY; if(firstMouse) { lastX = xpos; lastY = ypos; firstMouse = false; } let xoffset = xpos - lastX; let yoffset = lastY - ypos; lastX = xpos; lastY = ypos; camera.processMouseMovement(xoffset,yoffset); } main.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body scroll="no" style="overflow: hidden"> <canvas id="Canvas3D"></canvas> <script src="../js/glmatrix/dist/gl-matrix.js"></script> <script src="../js/MD2Import.js"></script> <script src="../js/ShaderProgram.js"></script> <script src="../js/Textures.js"></script> <script src="../js/Drawable.js"></script> <script src="../js/main.js"></script> </body> </html> Drawable.js class Drawable { constructor(shaderProgram, vertices, indices, texCoords, texture) { this.gl = shaderProgram.gl; let gl = shaderProgram.gl; this.shaderProgram = shaderProgram; shaderProgram.aTexCoord = gl.getAttribLocation(shaderProgram.shaderProgram, "aTexCoord"); shaderProgram.aPos = gl.getAttribLocation(shaderProgram.shaderProgram, "aPos"); this.VAO = gl.createVertexArray(); this.texture = texture; gl.bindVertexArray(this.VAO); this.vertexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); gl.vertexAttribPointer(shaderProgram.aPos, 3, gl.FLOAT, false, 12, 0); gl.enableVertexAttribArray(shaderProgram.aPos); this.EBO = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.EBO); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW); this.VBOTexCoords = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, this.VBOTexCoords); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(texCoords),gl.STATIC_DRAW); gl.vertexAttribPointer(shaderProgram.aTexCoord, 2, gl.FLOAT, false, 8, 0); gl.enableVertexAttribArray(shaderProgram.aTexCoord); gl.bindVertexArray(null); this.pos = vec3.create(); this.rot = vec3.create(); // в Градусах this.scale = vec3.create(); this.pos = [0,0,0]; this.rot = [0,0,0]; this.scale=[1,1,1]; this.vertCount = indices.length; this.model = mat4.create(); } updateModel() { mat4.identity(this.model); mat4.translate(this.model, this.model, this.pos); mat4.rotateX(this.model, this.model, Math.PI / 180 * this.rot[0]); mat4.rotateY(this.model, this.model, Math.PI / 180 * this.rot[1]); mat4.rotateZ(this.model, this.model, Math.PI / 180 * this.rot[2]); mat4.scale(this.model, this.model, this.scale); } translate(transX, transY, transZ) { this.pos[0] += transX; this.pos[1] += transY; this.pos[2] += transZ; } rotate(rotX, rotY, rotZ) { this.rot[0] += rotX; this.rot[1] += rotY; this.rot[2] += rotZ; } draw() { this.updateModel(); let gl =this.gl; gl.uniformMatrix4fv(this.shaderProgram.model, false, this.model); gl.bindVertexArray(this.VAO); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, this.texture); gl.uniform1i(gl.getUniformLocation(shaderProgram.shaderProgram, "uSampler"), 0); //gl.drawArrays(gl.TRIANGLES, 0, this.vertCount); gl.drawElements(gl.TRIANGLES, this.vertCount,gl.UNSIGNED_SHORT,0); } } Tried using gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true), result: By the way, it shouldn't work, because I already flipped the textures in the shader: gl_FragColor = texture2D(uSampler, vec2(vTexCoord.s,1.0 - vTexCoord.t)); rhino.png (textures) The md2 file: http://dropmefiles.com/zhQFU The problem solved when I deleted the index buffer and placed the vertices and uvs in one huge buffer. What it could be?
The solution was to delete index buffer and place everything in the vertex buffer. It worked for me
Low FPS in WebGL (working as 2d)
I have script https://drive.google.com/file/d/0B4Cqle1HMxo8LXl6YktuMF9EVWc/view?usp=sharing It's working as WebGL 2D When it draws 50 images (texture) -- FPS 60, but when it draws 150 and more -- FPS 20-30 Why? How can I solve this problem with WebGL? UPD jsFiddle html <canvas id="spirit_canvas"></canvas> <div id="fps" style="position:absolute;top:0;left:0;background:rgba(0,0,0,0.1);color:#111;padding:1px 2px;font-size:10px;font-family:sans-serif;z-index:5"></div> js function WebGL2d(id) { this._el = document.getElementById(id); this._gl = null; this._vertexShader = null; this._fragmentShader = null; this._program = null; this._p = { positionLocation: null, translationLocation: null, resolutionLocation: null, colorLocation: null, texCoordLocation: null, v_t: null }; this._vertexShaderSrc = "\ attribute vec2 a_position;\n\ uniform vec2 u_resolution;\n\ uniform vec2 u_translation;\n\ attribute vec2 a_texCoord;\n\ varying vec2 v_texCoord;\n\ void main() {\n\ vec2 position = a_position + u_translation;\n\ vec2 zeroToOne = position / u_resolution;\n\ vec2 zeroToTwo = zeroToOne * 2.0;\n\ vec2 clipSpace = zeroToTwo - 1.0;\n\ gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);\n\ gl_PointSize = 2.;\n\ v_texCoord = a_texCoord;\n\ }\n\ "; this._fragmentShaderSrc = "\ precision mediump float;\n\ uniform vec4 u_color;\n\ uniform sampler2D u_image;\n\ varying vec2 v_texCoord;\n\ uniform int v_t;\n\ void main() {\n\ gl_FragColor = u_color;\n\ if (v_t == 1) {\n\ gl_FragColor = texture2D(u_image, v_texCoord);\n\ }\n\ }\n\ "; this._canvas2d = null; this._canvas2dCache = null; this._canvasPathBuffer = []; this._isPointInPath = false; this.txtr = {}; this._vertexBuffer = null; this._indexBuffer = null; this._uvBuffer = null; this._colorBuffer = null; // ------------------- this.fillStyle = '#000'; this.strokeStyle = '#000'; this.lineWidth = 1; //----||| this._init(); } WebGL2d.prototype = { _getCanvas: function(w,h) { var canvas = document.createElement("canvas"); canvas.width = w; canvas.height = h; return canvas.getContext("2d"); }, _hexToRgbArray: function (hex) { var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; hex = hex.replace(shorthandRegex, function (m, r, g, b) { return r + r + g + g + b + b; }); var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? [ parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16), 1 ] : [0,0,0,1]; }, _rgbToArray: function(rgba) { var result = /(?:rgb|rgba)\((\d+),\s?(\d+),\s?(\d+)(?:,\s?(\d+|\d.\d+))?\)/g.exec(rgba); if(result) { return result.slice(1).filter(isFinite).map(parseFloat); } else { return [0,0,0,1]; } }, _context: function () { var names = ["webgl","experimental-webgl"]; var context = null; for (var ii = 0; ii < names.length; ++ii) { try { context = this._el.getContext(names[ii], {/*alpha: true, premultipliedAlpha: true, antialiasing: true*/}); } catch (e) { } if (context) { break; } } if (context === null) { console.error('WebGL2d don\'t init'); return; } this._gl = context; console.log('--'); }, _loadShader: function (src, type) { var shader = this._gl.createShader(type); this._gl.shaderSource(shader, src); this._gl.compileShader(shader); var compiled = this._gl.getShaderParameter(shader, this._gl.COMPILE_STATUS); if (!compiled) { lastError = this._gl.getShaderInfoLog(shader); console.error("*** Error compiling shader '" + shader + "':" + lastError); this._gl.deleteShader(shader); return null; } return shader; }, _loadProgram: function (shaders) { var program = this._gl.createProgram(); for (var i = 0; i < shaders.length; ++i) { this._gl.attachShader(program, shaders[i]); } this._gl.linkProgram(program); var linked = this._gl.getProgramParameter(program, this._gl.LINK_STATUS); if (!linked) { lastError = this._gl.getProgramInfoLog(program); log.error("Error in program linking:" + lastError); this._gl.deleteProgram(program); return null; } return program; }, _init: function () { this._context(); this._canvas2d = this._getCanvas(this._el.width,this._el.height); //this._gl.disable(this._gl.DEPTH_TEST); this._gl.enable(this._gl.BLEND); this._gl.blendFunc(this._gl.SRC_ALPHA, this._gl.ONE_MINUS_SRC_ALPHA); //this._gl.pixelStorei(this._gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); //this._gl.clearDepth (0.0); //this._gl.clearColor(0, 0, 0, 1.0); //this._gl.clear(this._gl.COLOR_BUFFER_BIT | this._gl.DEPTH_BUFFER_BIT); //this._gl.clear(this._gl.COLOR_BUFFER_BITT); this._vertexShader = this._loadShader(this._vertexShaderSrc, this._gl.VERTEX_SHADER); this._fragmentShader = this._loadShader(this._fragmentShaderSrc, this._gl.FRAGMENT_SHADER); this._program = this._loadProgram([this._vertexShader, this._fragmentShader]); this._gl.useProgram(this._program); this._p.positionLocation = this._gl.getAttribLocation(this._program, "a_position"); this._p.translationLocation = this._gl.getAttribLocation(this._program, "u_translation"); this._p.resolutionLocation = this._gl.getUniformLocation(this._program, "u_resolution"); this._p.colorLocation = this._gl.getUniformLocation(this._program, "u_color"); // texture this._p.texCoordLocation = this._gl.getAttribLocation(this._program, "a_texCoord"); this._p.v_t = this._gl.getUniformLocation(this._program, "v_t"); this._gl.uniform2f(this._p.resolutionLocation, this._el.width, this._el.height); this._initBuff(); }, _initBuff: function() { this._vertexBuffer = this._gl.createBuffer(); this._indexBuffer = this._gl.createBuffer(); this._uvBuffer = this._gl.createBuffer(); this._colorBuffer = this._gl.createBuffer(); }, _drawArrTriangle: function(num) { this._gl.drawArrays(this._gl.TRIANGLE_STRIP, 0,num); }, // ========== _setColor: function (color) { this._gl.uniform1i(this._p.v_t, 0); if (!(color instanceof Array)) { if (color.indexOf('r') === 0 || color.indexOf('R') === 0) { color = this._rgbToArray(color); } else { color = this._hexToRgbArray(color); } } color[0] = Math.round(color[0] / 255 * 100)/100; color[1] = Math.round(color[1] / 255 * 100)/100; color[2] = Math.round(color[2] / 255 * 100)/100; this._gl.uniform4f(this._p.colorLocation, color[0], color[1], color[2], color[3]); }, _buff: function (arr) { this._gl.enableVertexAttribArray(this._p.positionLocation); this._gl.bindBuffer(this._gl.ARRAY_BUFFER, this._vertexBuffer); this._gl.bufferData(this._gl.ARRAY_BUFFER, new Float32Array(arr), this._gl.DYNAMIC_DRAW); this._gl.vertexAttribPointer(this._p.positionLocation, 2, this._gl.FLOAT, false, 0, 0); this._gl.bindBuffer(this._gl.ARRAY_BUFFER, null); }, _buffTexture: function(image) { this._gl.bindBuffer(this._gl.ARRAY_BUFFER, this._uvBuffer); this._gl.bufferData(this._gl.ARRAY_BUFFER, new Float32Array([ 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0]), this._gl.STATIC_DRAW); this._gl.enableVertexAttribArray(this._p.texCoordLocation); this._gl.vertexAttribPointer(this._p.texCoordLocation, 2, this._gl.FLOAT, false, 0, 0); this._gl.bindBuffer(this._gl.ARRAY_BUFFER, null); if (!(image.src in this.txtr)) { this.txtr[image.src] = this._gl.createTexture(); this._gl.bindTexture(this._gl.TEXTURE_2D, this.txtr[image.src]); // Set the parameters so we can render any size image. this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_S, this._gl.CLAMP_TO_EDGE); this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_T, this._gl.CLAMP_TO_EDGE); this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_MIN_FILTER, this._gl.LINEAR); this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_MAG_FILTER, this._gl.LINEAR); this._gl.texImage2D(this._gl.TEXTURE_2D, 0, this._gl.RGBA, this._gl.RGBA, this._gl.UNSIGNED_BYTE, image); } else { this._gl.bindTexture(this._gl.TEXTURE_2D, this.txtr[image.src]); } }, //-------------------------------------------- fillRect: function(x,y,w,h,color,image) { var translation = [0, 0]; translation[0] = x; translation[1] = y; var x2 = x + w; var y2 = h + y; this._buff( [ x,y, x2,y, x,y2, x,y2, x2,y, x2,y2 ] ); if (!image) { if (color) { this._setColor(color); } else { this._setColor(this.fillStyle); } } this._drawArrTriangle(6); }, strokeRect: function(x, y, w, h) { this.fillRect( x, y, w, this.lineWidth, this.strokeStyle ); this.fillRect( (x + w - this.lineWidth), y, this.lineWidth, h, this.strokeStyle ); this.fillRect( x, (y + h - this.lineWidth), w, this.lineWidth, this.strokeStyle ); this.fillRect( x, y, this.lineWidth, h, this.strokeStyle ); }, clearRect: function(x, y, w, h) { //this._gl.clearDepth (1.0); this._gl.clearColor(0, 0, 0, 1.0); //this._gl.clear(this._gl.COLOR_BUFFER_BIT | this._gl.DEPTH_BUFFER_BIT); //this._gl.clear(this._gl.COLOR_BUFFER_BIT); }, beginPath: function(isPointInPath) { if (isPointInPath == true) { this._isPointInPath = true; this._canvas2d.beginPath(); } else { this._isPointInPath = false; this._canvasPathBuffer = []; } }, moveTo: function(x,y) { if (this._isPointInPath == true) { this._canvas2d.moveTo(x,y); } else { this._canvasPathBuffer.push([x,y]); } }, lineTo: function(x,y) { if (this._isPointInPath == true) { this._canvas2d.lineTo(x,y); } else { this._canvasPathBuffer.push([x,y]); } }, fill: function() { if (this._canvasPathBuffer.length == 4) { this._buff( [ this._canvasPathBuffer[0][0], this._canvasPathBuffer[0][2], this._canvasPathBuffer[1][0], this._canvasPathBuffer[1][3], this._canvasPathBuffer[2][0], this._canvasPathBuffer[2][4], this._canvasPathBuffer[2][0], this._canvasPathBuffer[2][5], this._canvasPathBuffer[3][0], this._canvasPathBuffer[3][6], this._canvasPathBuffer[0][0], this._canvasPathBuffer[0][7] ] ); this._setColor(this.fillStyle); this._drawArrTriangle(6); } }, closePath: function() { if (this._isPointInPath == true) { this._canvas2d.closePath(); } }, isPointInPath: function(x,y) { return this._canvas2d.isPointInPath(x,y); }, text: function (text,x,y,size,color,fontStyle,fontFamily,borderColor) { return false; }, fillText: function(text,x,y,maxWidth) { return false; }, drawImage: function(img,x,y, w, h) { this._gl.uniform1i(this._p.v_t, 1); this._buffTexture(img); this.fillRect(x,y,w,h,false,true); } }; function microtime() { return new Date().getTime(); } function round(s,exp) { exp = exp || 0; return Math.round(s * Math.pow(10,exp)) / Math.pow(10,exp); } window.requestAnimFrame = (function(){ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(callback, element){ window.setTimeout(callback, 1000 / 60); }; })(); var canvas; var image; var fpsLastCalledTime; var height = 400; var width = 700; function main() { var $el = $('#spirit_canvas'); $('body').css('width',(width + 'px')); $el.css({'width':(width + 'px'),'height':(height + 'px')}).attr('width',width).attr('height',height); canvas = new WebGL2d('spirit_canvas'); image = new Image(); image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAlCAYAAABcZvm2AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAWNJREFUeNrsV8sNwjAMbUqBBWACxB2pQ8AKcGALTsAJuDEFB1gBhuDAuWICmICPQh01pXWdJqEFcaglRGRbfonjPLuMc+5QwhjLGEJfZusjxZOL9akZKye9G98vPMfvsAx4qBfKwfzBL9s6uUHpI6U/u7+BKGkNb/H6umtk7MczF0HyfKS4zo/k/4AgTV8DOizrqX8oECgC+MGa8lGJp9sJDiAB8nyqYoglvJOPbP97IqoATGxWVZeXJlMQwYHA3piF8wJIblOVNBBxe3TPMLoHIKtxrbS7AAbBrA4Y5NaPAXf8LjN6wKZ0RaZOnlAFZnuXInVR4FTE6eYp0olPhhshtXsAwY3PquoAJNkIY33U7HTs7hYBwV24ItUKqDwgKF3VzAZ6k8HF+B1BMF8xRJbeJoqMXHZAAQ1kwoluURCdzepEugGEImBrIADB7I4lyfbJLlw92FKE6b5hVd+ktv4vAQYASMWxvlAAvcsAAAAASUVORK5CYII="; image.onload = function() { play(); } } function play() { //this.time = microtime(); canvas.clearRect(); draw(); drawFps(); requestAnimFrame(play.bind(window)); } drawFps = function() { var fps; if(!fpsLastCalledTime) { fpsLastCalledTime = microtime(); fps = 0; } else { fps = round(1000/(microtime() - fpsLastCalledTime)); fpsLastCalledTime = microtime(); } $('#fps').html('fps: ' + fps); }; function draw() { canvas.fillRect(0,0,width,height,'#eee'); var __x = 0; var __y = 0; var __h = 37; var __w = 26; var x, y, h, w; for(var i = 0; i < 200; ++i) { if (i % 26 == 0) { __y = __y + __h; __x = 0; } else { __x = __x + __w; //__y = __y; } h = __h; w = __w; x = __x; y = __y; //canvas.fillRect(x,y, w, h,'#050'); canvas.drawImage(image,x,y, w, h); } } main();
In general you only want to call gl.bufferData and gl.texImage2d at init time. AFAICT you're calling them for every draw call.