305 lines
11 KiB
JavaScript
305 lines
11 KiB
JavaScript
'use strict';
|
|
|
|
/*
|
|
|
|
Small and fast dynamic webgl rendering engine for Dr1v3n Wild
|
|
|
|
Features
|
|
- batch rendering
|
|
- direct and ambient lighting
|
|
- fog with alpha blending
|
|
- texture mapping
|
|
- vertex color
|
|
|
|
Potential improvements
|
|
- everything is using dynamic buffer, which is slow but flexible
|
|
- it would be faster to use static buffers for static geometry
|
|
- the colors could be passed in as 32 bit integers rather then vec4s
|
|
- specular lighting would also be pretty easy to include
|
|
- the fog calculation could possibly be moved to the vertex shader
|
|
- a mip map of the passed in texture could be auto generated for smoother scaling
|
|
- additive blending would also be easy to implement
|
|
- there should be an easier way to set the fog range
|
|
|
|
*/
|
|
|
|
const glRenderScale = 100; // fixes floating point issues on some devices
|
|
const glSpecular = 0; // experimental specular test
|
|
let glCanvas, glContext, glShader, glVertexData;
|
|
let glBatchCount, glBatchCountTotal, glDrawCalls;
|
|
let glEnableLighting, glLightDirection, glLightColor, glAmbientColor;
|
|
let glEnableFog, glFogColor;
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// webgl setup
|
|
|
|
function glInit()
|
|
{
|
|
// create the canvas
|
|
const hasAlpha = false; // there should be no alpha for the background texture
|
|
document.body.appendChild(glCanvas = document.createElement('canvas'));
|
|
glContext = glCanvas.getContext('webgl2', {alpha: hasAlpha});
|
|
ASSERT(glContext, 'Failed to create WebGL canvas!');
|
|
|
|
// setup vertex and fragment shaders
|
|
glShader = glCreateProgram(
|
|
'#version 300 es\n' + // specify GLSL ES version
|
|
'precision highp float;'+ // use highp for better accuracy
|
|
'uniform vec4 l,g,a,f;' + // light direction, color, ambient light, fog
|
|
'uniform mat4 m,o;'+ // projection matrix, object matrix
|
|
'in vec4 p,n,u,c;'+ // in: position, normal, uv, color
|
|
'out vec4 v,d,q;'+ // out: uv, color, fog
|
|
'void main(){'+ // shader entry point
|
|
'gl_Position=m*o*p;'+ // transform position
|
|
'v=u,q=f;'+ // pass uv and fog to fragment shader
|
|
'd=c*vec4(a.xyz+g.xyz*max(0.,dot(l.xyz,'+ // lighting
|
|
'normalize((transpose(inverse(o))*n).xyz))),1);' + // transform light
|
|
'}' // end of shader
|
|
,
|
|
'#version 300 es\n' + // specify GLSL ES version
|
|
'precision highp float;'+ // use highp for better accuracy
|
|
'in vec4 v,d,q;'+ // uv, color, fog
|
|
'uniform sampler2D s;'+ // texture
|
|
'out vec4 c;'+ // out color
|
|
'void main(){'+ // shader entry point
|
|
'c=v.z>0.?d:texture(s,v.xy)*d;'+ // color or texture
|
|
'float f=gl_FragCoord.z/gl_FragCoord.w;'+ // fog depth
|
|
'v.w>0.?c:c=vec4(mix(c.xyz,q.xyz,clamp(f*f/1e10,0.,1.)),'+ // fog color
|
|
'c.a*clamp(4.-f/2e4,0.,1.));'+ // fog alpha
|
|
//'c.w);'+ // disable fog alpha
|
|
//'if (c.a == 0.) discard;'+ // discard if no alpha
|
|
'}' // end of shader
|
|
);
|
|
|
|
// set up the shader
|
|
glContext.useProgram(glShader);
|
|
glContext.bindBuffer(gl_ARRAY_BUFFER, glContext.createBuffer());
|
|
glContext.bufferData(gl_ARRAY_BUFFER, gl_VERTEX_BUFFER_SIZE, gl_DYNAMIC_DRAW);
|
|
glContext.blendFunc(gl_SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA);
|
|
glSetCapability(gl_BLEND);
|
|
glSetCapability(gl_CULL_FACE); // not culling causeses thin black lines sometimes
|
|
glVertexData = new Float32Array(new ArrayBuffer(gl_VERTEX_BUFFER_SIZE));
|
|
|
|
// set vertex attributes
|
|
let offset = 0;
|
|
const vertexAttribute = (name)=>
|
|
{
|
|
const type = gl_FLOAT, stride = gl_VERTEX_BYTE_STRIDE;
|
|
const size = 4, byteCount = 4;
|
|
const location = glContext.getAttribLocation(glShader, name);
|
|
glContext.enableVertexAttribArray(location);
|
|
glContext.vertexAttribPointer(location, size, type, 0, stride, offset);
|
|
offset += size*byteCount;
|
|
}
|
|
vertexAttribute('p'); // position
|
|
vertexAttribute('n'); // normal
|
|
vertexAttribute('u'); // uv
|
|
vertexAttribute('c'); // color
|
|
}
|
|
|
|
function glCompileShader(source, type)
|
|
{
|
|
// build the shader
|
|
const shader = glContext.createShader(type);
|
|
glContext.shaderSource(shader, source);
|
|
glContext.compileShader(shader);
|
|
|
|
// check for errors
|
|
if (debug && !glContext.getShaderParameter(shader, gl_COMPILE_STATUS))
|
|
throw glContext.getShaderInfoLog(shader);
|
|
return shader;
|
|
}
|
|
|
|
function glCreateProgram(vsSource, fsSource)
|
|
{
|
|
// build the program
|
|
const program = glContext.createProgram();
|
|
glContext.attachShader(program, glCompileShader(vsSource, gl_VERTEX_SHADER));
|
|
glContext.attachShader(program, glCompileShader(fsSource, gl_FRAGMENT_SHADER));
|
|
glContext.linkProgram(program);
|
|
|
|
// check for errors
|
|
if (debug && !glContext.getProgramParameter(program, gl_LINK_STATUS))
|
|
throw glContext.getProgramInfoLog(program);
|
|
return program;
|
|
}
|
|
|
|
function glCreateTexture(image)
|
|
{
|
|
// build the texture
|
|
const texture = glContext.createTexture();
|
|
glContext.bindTexture(gl_TEXTURE_2D, texture);
|
|
glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, gl_RGBA, gl_UNSIGNED_BYTE, image);
|
|
return texture;
|
|
}
|
|
|
|
function glPreRender(canvasSize)
|
|
{
|
|
// set size of canvas and viewport which also clears it
|
|
glContext.viewport(0, 0, glCanvas.width = canvasSize.x, glCanvas.height = canvasSize.y);
|
|
glDrawCalls = glBatchCount = glBatchCountTotal = 0; // reset draw counts
|
|
//debug && glContext.clearColor(1, 0, 1, 1); // test background color
|
|
//glContext.clear(gl_DEPTH_BUFFER_BIT|gl_COLOR_BUFFER_BIT); // auto cleared
|
|
|
|
// use point filtering for pixelated rendering
|
|
glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, gl_NEAREST);
|
|
glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, gl_NEAREST);
|
|
|
|
// set up the camera transform
|
|
const viewMatrix = buildMatrix(cameraPos, cameraRot).inverse();
|
|
const combinedMatrix = glCreateProjectionMatrix().multiply(viewMatrix);
|
|
glContext.uniformMatrix4fv(glUniform('m'), 0, combinedMatrix.toFloat32Array());
|
|
}
|
|
|
|
function glRender(transform=new DOMMatrix)
|
|
{
|
|
// set up the lights and fog
|
|
const initUniform4f = (name, x, y, z)=> glContext.uniform4f(glUniform(name), x, y, z, 0);
|
|
const lightColor = glEnableLighting ? glLightColor : BLACK;
|
|
const ambientColor = glEnableLighting ? glAmbientColor : WHITE;
|
|
initUniform4f('g', lightColor.r, lightColor.g, lightColor.b);
|
|
initUniform4f('a', ambientColor.r, ambientColor.g, ambientColor.b);
|
|
initUniform4f('f', glFogColor.r, glFogColor.g, glFogColor.b);
|
|
initUniform4f('l', glLightDirection.x, glLightDirection.y, glLightDirection.z);
|
|
|
|
// render the verts
|
|
ASSERT(glBatchCount < gl_MAX_BATCH, 'Too many points!');
|
|
const vertexData = glVertexData.subarray(0, glBatchCount * gl_INDICIES_PER_VERT);
|
|
const m = transform.scaleSelf(glRenderScale, glRenderScale, glRenderScale);
|
|
glContext.uniformMatrix4fv(glUniform('o'), 0, m.toFloat32Array());
|
|
glContext.bufferSubData(gl_ARRAY_BUFFER, 0, vertexData);
|
|
glContext.drawArrays(gl_TRIANGLE_STRIP, 0, glBatchCount);
|
|
glBatchCountTotal += glBatchCount;
|
|
glBatchCount = 0;
|
|
++glDrawCalls;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// webgl helper functions
|
|
|
|
const glUniform = (name) => glContext.getUniformLocation(glShader, name);
|
|
|
|
function glSetCapability(cap, enable=1)
|
|
{ enable ? glContext.enable(cap) : glContext.disable(cap); }
|
|
|
|
function glPolygonOffset(units=0)
|
|
{ glContext.polygonOffset(0, -units); glSetCapability(gl_POLYGON_OFFSET_FILL, !!units); }
|
|
|
|
function glSetDepthTest(depthTest=1, depthWrite=1)
|
|
{ glSetCapability(gl_DEPTH_TEST, !!depthTest); glContext.depthMask(!!depthWrite); }
|
|
|
|
function glCreateProjectionMatrix(fov=.5, near = 1, far = 1e4)
|
|
{
|
|
const aspect = glCanvas.width / glCanvas.height;
|
|
const f = 1 / Math.tan(fov), range = far - near;
|
|
return new DOMMatrix
|
|
([
|
|
f / aspect, 0, 0, 0,
|
|
0, f, 0, 0,
|
|
0, 0, (near + far) / range, 2 * near * far / range,
|
|
0, 0, -1, 0
|
|
]);
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// drawing functions
|
|
|
|
const vectorOne = vec3(1); // no lighting/texture
|
|
|
|
// push a list of colored verts with optonal normals and uvs
|
|
function glPushVerts(points, normals, color, uvs)
|
|
{
|
|
const count = points.length;
|
|
if (!(count < gl_MAX_BATCH - glBatchCount))
|
|
glRender();
|
|
|
|
const na = vectorOne; // no lighting/texture
|
|
for(let i=count; i--;)
|
|
glPushVert(points[i], normals ? normals[i] : na, uvs ? uvs[i] : na, color);
|
|
}
|
|
|
|
// push a list of colored verts with optonal normals and uvs
|
|
// this is also capped with degenerate verts to close the shape
|
|
function glPushVertsCapped(points, normals, color, uvs)
|
|
{
|
|
// push points with extra degenerate verts to cap both sides
|
|
const count = points.length;
|
|
if (!(count+2 < gl_MAX_BATCH - glBatchCount))
|
|
glRender();
|
|
|
|
const na = vectorOne; // no lighting/texture
|
|
glPushVert(points[count-1], na, na, color);
|
|
for(let i=count; i--;)
|
|
glPushVert(points[i], normals ? normals[i] : na, uvs ? uvs[i] : na, color);
|
|
glPushVert(points[0], na, na, color);
|
|
}
|
|
|
|
// push a list of colored verts without normals or uvs
|
|
function glPushColoredVerts(points, colors)
|
|
{
|
|
// push points with a list of vertex colors
|
|
const count = points.length;
|
|
if (!(count+2 < gl_MAX_BATCH - glBatchCount))
|
|
glRender();
|
|
|
|
const na = vectorOne; // no lighting/texture
|
|
glPushVert(points[count-1], na, na, colors[count-1]);
|
|
for(let i=count; i--;)
|
|
glPushVert(points[i], na, na, colors[i]);
|
|
glPushVert(points[0], na, na, colors[0]);
|
|
}
|
|
|
|
// push a single vert to the buffer
|
|
function glPushVert(pos, normal, uv, color)
|
|
{
|
|
let offset = glBatchCount++ * gl_INDICIES_PER_VERT;
|
|
glVertexData[offset++] = pos.x/glRenderScale;
|
|
glVertexData[offset++] = pos.y/glRenderScale;
|
|
glVertexData[offset++] = pos.z/glRenderScale;
|
|
glVertexData[offset++] = 1;
|
|
glVertexData[offset++] = normal.x;
|
|
glVertexData[offset++] = normal.y;
|
|
glVertexData[offset++] = normal.z;
|
|
glVertexData[offset++] = 0;
|
|
glVertexData[offset++] = uv.x;
|
|
glVertexData[offset++] = uv.y;
|
|
glVertexData[offset++] = uv.z; // >0 if untextured
|
|
glVertexData[offset++] = !glEnableFog;
|
|
glVertexData[offset++] = color.r;
|
|
glVertexData[offset++] = color.g;
|
|
glVertexData[offset++] = color.b;
|
|
glVertexData[offset++] = color.a;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// store webgl constants as integers so they can be minifed
|
|
|
|
const
|
|
gl_TRIANGLE_STRIP = 5,
|
|
gl_DEPTH_BUFFER_BIT = 256,
|
|
gl_SRC_ALPHA = 770,
|
|
gl_ONE_MINUS_SRC_ALPHA = 771,
|
|
gl_CULL_FACE = 2884,
|
|
gl_DEPTH_TEST = 2929,
|
|
gl_BLEND = 3042,
|
|
gl_TEXTURE_2D = 3553,
|
|
gl_UNSIGNED_BYTE = 5121,
|
|
gl_FLOAT = 5126,
|
|
gl_RGBA = 6408,
|
|
gl_NEAREST = 9728,
|
|
gl_TEXTURE_MAG_FILTER = 10240,
|
|
gl_TEXTURE_MIN_FILTER = 10241,
|
|
gl_COLOR_BUFFER_BIT = 16384,
|
|
gl_POLYGON_OFFSET_FILL = 32823,
|
|
gl_ARRAY_BUFFER = 34962,
|
|
gl_DYNAMIC_DRAW = 35048,
|
|
gl_FRAGMENT_SHADER = 35632,
|
|
gl_VERTEX_SHADER = 35633,
|
|
gl_COMPILE_STATUS = 35713,
|
|
gl_LINK_STATUS = 35714,
|
|
|
|
// constants for batch rendering
|
|
gl_MAX_BATCH = 2e4, // max verts per batch
|
|
gl_INDICIES_PER_VERT = (1 * 4) * 4, // vec4 * 4
|
|
gl_VERTEX_BYTE_STRIDE = gl_INDICIES_PER_VERT * 4, // 4 bytes per float
|
|
gl_VERTEX_BUFFER_SIZE = gl_MAX_BATCH * gl_VERTEX_BYTE_STRIDE; |