355 lines
9.6 KiB
JavaScript
Executable File
355 lines
9.6 KiB
JavaScript
Executable File
|
|
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++;
|
|
}
|
|
};
|
|
|