Files

355 lines
9.6 KiB
JavaScript
Raw Permalink Normal View History

2021-09-13 09:34:08 +02:00
let
options = {antialias: false},
gl = (c.getContext('webgl', options) || c.getContext('experimental-webgl', options)),
R_MAX_VERTS = 1024 * 64, // allow 512k verts max
R_MAX_LIGHT_V3 = 64,
// Vertex shader source. This translates the model position & rotation and also
// mixes positions of two buffers for animations.
R_SOURCE_VS =
'precision highp float;' +
// Vertex positions, normals and uv coords for the fragment shader
'varying vec3 vp,vn;' +
'varying vec2 vt;' +
// Input vertex positions & normals and blend vertex positions & normals
'attribute vec3 p,n,p2,n2;' +
// Input UV coords
'attribute vec2 t;' +
// Camera position (x, y, z) and aspect ratio (w)
'uniform vec4 c;' +
// Model position (x, y, z)
'uniform vec3 mp;' +
// Model rotation (yaw, pitch)
'uniform vec2 mr;' +
// Mouse rotation yaw (x), pitch (y)
'uniform vec2 m;' +
// Blend factor between the two vertex positions
'uniform float f;' +
// Generate a rotation Matrix around the x,y,z axis;
// Used for model rotation and camera yaw
'mat4 rx(float r){' +
'return mat4(' +
'1,0,0,0,' +
'0,cos(r),sin(r),0,' +
'0,-sin(r),cos(r),0,' +
'0,0,0,1' +
');' +
'}' +
'mat4 ry(float r){' +
'return mat4(' +
'cos(r),0,-sin(r),0,' +
'0,1,0,0,' +
'sin(r),0,cos(r),0,' +
'0,0,0,1' +
');' +
'}' +
'mat4 rz(float r){' +
'return mat4(' +
'cos(r),sin(r),0,0,' +
'-sin(r),cos(r),0,0,' +
'0,0,1,0,' +
'0,0,0,1' +
');' +
'}' +
'void main(void){' +
// Rotation Matrixes for model rotation
'mat4 '+
'mry=ry(mr.x),' +
'mrz=rz(mr.y);' +
// Mix vertex positions, rotate and add the model position
'vp=(mry*mrz*vec4(mix(p,p2,f),1.)).xyz+mp;' +
// Mix normals
'vn=(mry*mrz*vec4(mix(n,n2,f),1.)).xyz;' +
// UV coords are handed over to the fragment shader as is
'vt=t;' +
// Final vertex position is transformed by the projection matrix,
// rotated around mouse yaw/pitch and offset by the camera position
// We use a FOV of 90, so the matrix[0] and [5] are conveniently 1.
// (1 / Math.tan((90/180) * Math.PI / 2) === 1)
'gl_Position=' +
'mat4(' +
'1,0,0,0,' +
'0,c.w,0,0,' +
'0,0,1,1,' +
'0,0,-2,0' +
')*' + // projection
'rx(-m.y)*ry(-m.x)*' +
'vec4(vp-c.xyz,1.);' +
'}',
// Fragment shader source. Calculates the lighting, does some cheesy gamma
// correction and reduces the colors of the final output.
R_SOURCE_FS =
'precision highp float;' +
// Vertex positions, normals and uv coords
'varying vec3 vp,vn;' +
'varying vec2 vt;' +
'uniform sampler2D s;' +
// Lights [(x,y,z), [r,g,b], ...]
'uniform vec3 l['+R_MAX_LIGHT_V3+'];' +
'void main(void){' +
'gl_FragColor=texture2D(s,vt);' +
// Debug: no textures
// 'gl_FragColor=vec4(1.0,1.0,1.0,1.0);' +
// Calculate all lights
'vec3 vl;' +
'for(int i=0;i<'+R_MAX_LIGHT_V3+';i+=2) {' +
'vl+=' +
// Angle to normal
'max('+
'dot('+
'vn, normalize(l[i]-vp)' +
')' +
',0.)*' +
'(1./pow(length(l[i]-vp),2.))' + // Inverse distance squared
'*l[i+1];' + // Light color/intensity
'}' +
// Debug: full bright lights
// 'vl = vec3(2,2,2);' +
'gl_FragColor.rgb=floor('+
'gl_FragColor.rgb*pow(vl,vec3(0.75))'+ // Light, Gamma
'*16.0+0.5'+
')/16.0;' + // Reduce final output color for some extra dirty looks
'}',
// 8 properties per vert [x,y,z, u,v, nx,ny,nz]
r_buffer = new Float32Array(R_MAX_VERTS*8),
r_num_verts = 0,
// 2 vec3 per light [(x,y,z), [r,g,b], ...]
r_light_buffer = new Float32Array(R_MAX_LIGHT_V3*3),
r_num_lights = 0,
// Uniform locations
r_u_camera,
r_u_lights,
r_u_mouse,
r_u_pos,
r_u_rotation,
r_u_frame_mix,
// Vertex attribute location for mixing
r_va_p2, r_va_n2,
// Texture handles
r_textures = [],
// Camera position
r_camera = vec3(0, 0,-50),
r_camera_pitch = 0.2,
r_camera_yaw = 0,
// We collect all draw calls in an array and draw them all at once at the end
// the frame. This way the lights buffer will be completely filled and we
// only need to set it once for all geometry
r_draw_calls = [],
r_init = () => {
// Create shorthand WebGL function names
// let webglShortFunctionNames = {};
for (let name in gl) {
if (gl[name].length != undefined) {
gl[name.match(/(^..|[A-Z]|\d.|v$)/g).join('')] = gl[name];
// webglShortFunctionNames[name] = 'gl.' +name.match(/(^..|[A-Z]|\d.|v$)/g).join('');
}
}
// console.log(JSON.stringify(webglShortFunctionNames, null, '\t'));
let shader_program = gl.createProgram();
gl.attachShader(shader_program, r_compile_shader(gl.VERTEX_SHADER, R_SOURCE_VS));
gl.attachShader(shader_program, r_compile_shader(gl.FRAGMENT_SHADER, R_SOURCE_FS));
gl.linkProgram(shader_program);
gl.useProgram(shader_program);
r_u_camera = gl.getUniformLocation(shader_program, 'c');
r_u_lights = gl.getUniformLocation(shader_program, 'l');
r_u_mouse = gl.getUniformLocation(shader_program, 'm');
r_u_pos = gl.getUniformLocation(shader_program, 'mp');
r_u_rotation = gl.getUniformLocation(shader_program, 'mr');
r_u_frame_mix = gl.getUniformLocation(shader_program, 'f');
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
r_vertex_attrib(shader_program, 'p', 3, 8, 0); // position
r_vertex_attrib(shader_program, 't', 2, 8, 3); // texture coord
r_vertex_attrib(shader_program, 'n', 3, 8, 5); // normals
r_va_p2 = r_vertex_attrib(shader_program, 'p2', 3, 8, 0); // mix position
r_va_n2 = r_vertex_attrib(shader_program, 'n2', 3, 8, 5); // mix normals
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.BLEND);
gl.enable(gl.CULL_FACE);
gl.viewport(0,0,c.width,c.height);
},
r_compile_shader = (shader_type, shader_source) => {
let shader = gl.createShader(shader_type);
gl.shaderSource(shader, shader_source);
gl.compileShader(shader);
// console.log(gl.getShaderInfoLog(shader));
return shader;
},
r_vertex_attrib = (shader_program, attrib_name, count, vertex_size, offset) => {
let location = gl.getAttribLocation(shader_program, attrib_name);
gl.enableVertexAttribArray(location);
gl.vertexAttribPointer(location, count, gl.FLOAT, false, vertex_size * 4, offset * 4);
return location;
},
r_create_texture = (c) => {
let t = {t:gl.createTexture(), c};
gl.bindTexture(gl.TEXTURE_2D, t.t);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, c);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
gl.generateMipmap(gl.TEXTURE_2D);
r_textures.push(t);
},
r_prepare_frame = (r,g,b) => {
gl.clearColor(r,g,b,1);
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
r_num_lights = 0;
r_light_buffer.fill(0);
},
r_end_frame = () => {
gl.uniform4f(r_u_camera, r_camera.x, r_camera.y, r_camera.z, 16/9);
gl.uniform2f(r_u_mouse, r_camera_yaw, r_camera_pitch);
gl.uniform3fv(r_u_lights, r_light_buffer);
let vo = 0,
last_texture = -1;
for (let c of r_draw_calls) {
// c = [x, y, z, yaw, pitch, texture, offset1, offset2, mix, length]
// Bind new texture only if it changed from the previous one. The map
// is sorted by texture indices, so this helps.
if (last_texture != c[5]) {
last_texture = c[5];
gl.bindTexture(gl.TEXTURE_2D, r_textures[last_texture].t);
}
gl.uniform3f(r_u_pos, c[0], c[1], c[2]);
gl.uniform2f(r_u_rotation, c[3], c[4]);
gl.uniform1f(r_u_frame_mix, c[8]);
// If we have two different frames, calculate the offset from the
// drawArrays call to the mix frame.
// Setting the vertexAttribPointer is quite expensive, so we only
// do this if we have to; i.e. for animated models.
if (vo != (c[7]-c[6])) {
vo = (c[7]-c[6]);
gl.vertexAttribPointer(r_va_p2, 3, gl.FLOAT, false, 8 * 4, vo*8*4);
gl.vertexAttribPointer(r_va_n2, 3, gl.FLOAT, false, 8 * 4, (vo*8+5)*4);
}
gl.drawArrays(gl.TRIANGLES, c[6], c[9]);
}
// Reset draw calls
r_draw_calls = [];
},
r_draw = (pos, yaw, pitch, texture, f1, f2, mix, num_verts) => {
r_draw_calls.push([
pos.x, pos.y, pos.z, yaw, pitch,
texture, f1, f2, mix, num_verts
]);
},
r_submit_buffer = () => {
gl.bufferData(gl.ARRAY_BUFFER, r_buffer.subarray(0, r_num_verts*8), gl.STATIC_DRAW);
},
r_push_vert = (pos, normal, u, v) => {
r_buffer.set([pos.x, pos.y, pos.z, u, v, normal.x, normal.y, normal.z], r_num_verts * 8);
r_num_verts++;
},
r_push_quad = (v0, v1, v2, v3, u, v) => {
let n = vec3_face_normal(v0, v1, v2);
r_push_vert(v0, n, u, 0);
r_push_vert(v1, n, 0, 0);
r_push_vert(v2, n, u, v);
r_push_vert(v3, n, 0, v);
r_push_vert(v2, n, u, v);
r_push_vert(v1, n, 0, 0);
},
r_push_block = (x, y, z, sx, sy, sz, texture) => {
let canvas = r_textures[texture].c,
index = r_num_verts,
tx = sx/canvas.width,
ty = sy/canvas.height,
tz = sz/canvas.width,
// top
v0 = vec3(x, y + sy, z),
v1 = vec3(x + sx, y + sy, z),
v2 = vec3(x, y + sy, z + sz),
v3 = vec3(x + sx, y + sy, z + sz),
// bottom
v4 = vec3(x, y, z + sz),
v5 = vec3(x + sx, y, z + sz),
v6 = vec3(x, y, z),
v7 = vec3(x + sx, y, z);
r_push_quad(v0, v1, v2, v3, tx, tz); // top
r_push_quad(v4, v5, v6, v7, tx, tz); // bottom
r_push_quad(v2, v3, v4, v5, tx, ty); // front
r_push_quad(v1, v0, v7, v6, tx, ty); // back
r_push_quad(v3, v1, v5, v7, tz, ty); // right
r_push_quad(v0, v2, v6, v4, tz, ty); // left
return index;
},
r_push_light = (pos, intensity, r, g, b) => {
// Calculate the distance to the light, fade it out between 768--1024
let fade = clamp(
scale(
vec3_dist(pos, r_camera),
768, 1024, 1, 0
),
0, 1
) * intensity * 10;
if (fade && r_num_lights < R_MAX_LIGHT_V3/2) {
r_light_buffer.set([pos.x, pos.y, pos.z, r*fade, g*fade, b*fade], r_num_lights*6);
r_num_lights++;
}
};