Files
my_site/vue/public/race/webgl.js
yoyuzh d669738967 init
2026-02-27 14:29:05 +08:00

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;