570 lines
18 KiB
JavaScript
570 lines
18 KiB
JavaScript
import {
|
|
multiply,
|
|
perspective,
|
|
viewMatrix,
|
|
key,
|
|
isFullCube,
|
|
unitBox,
|
|
project,
|
|
} from "./math.js";
|
|
const vertex = `#version 300 es
|
|
precision highp float;layout(location=0) in vec3 aPos;layout(location=1) in vec3 aColor;layout(location=2) in vec3 aNormal;layout(location=3) in float aMaterial;layout(location=4) in float aOpacity;uniform mat4 uVP;out vec3 vPos;out vec3 vColor;out vec3 vNormal;flat out float vMaterial;out float vOpacity;void main(){vOpacity=aOpacity;vPos=aPos;vColor=aColor;vNormal=aNormal;vMaterial=aMaterial;gl_Position=uVP*vec4(aPos,1.);}`;
|
|
const fragment = `#version 300 es
|
|
precision highp float;in vec3 vPos;in vec3 vColor;in vec3 vNormal;flat in float vMaterial;in float vOpacity;uniform vec3 uEye;uniform sampler2D uTexture;uniform bool uTextured;uniform sampler2D uEffectTexture;uniform bool uEffectTextured;uniform float uPulse;out vec4 outColor;
|
|
float hash(vec3 p){return fract(sin(dot(p,vec3(127.1,311.7,74.7)))*43758.5453);}
|
|
vec3 shacraftBounceTint(vec3 color,float pulse){return color;}
|
|
void main(){vec3 n=normalize(vNormal);vec2 uv=abs(n.y)>.5?vPos.xz:(abs(n.x)>.5?vPos.zy:vPos.xy);vec3 p=floor((vPos+n*.002)*16.);float noise=hash(p)*.14-.07;float pattern=1.;
|
|
if(vMaterial>1.5&&vMaterial<2.5){pattern=.89+.11*step(.07,fract(uv.y*4.));pattern*=.9+.1*step(.055,fract(uv.x*.5+floor(uv.y*4.)*.5));}
|
|
if(vMaterial>2.5&&vMaterial<3.5){pattern=.87+.13*step(.06,fract(uv.y*4.));pattern*=.84+.16*step(.045,fract(uv.x*2.+floor(uv.y*4.)*.5));}
|
|
if(vMaterial>3.5&&vMaterial<4.5){pattern=.91+.09*sin(floor(uv.x*16.)*1.73+sin(floor(uv.y*16.)*.22));}
|
|
float tex=uTextured?mix(.84,1.12,texture(uTexture,uv).r):1.;if(vMaterial>4.5&&uEffectTextured)tex=mix(.75,1.2,texture(uEffectTexture,uv).g);float diffuse=max(0.,dot(n,normalize(vec3(-.45,.86,.3))));float light=.69+diffuse*.34;vec3 color=clamp((vColor+noise)*pattern*tex*light,0.,1.);if(vMaterial>.5&&vMaterial<1.5&&n.y<.5)color=mix(color,vec3(.38,.29,.17),.65);
|
|
if(vMaterial>4.5)color=shacraftBounceTint(color,uPulse);float fog=1.-exp(-pow(distance(vPos,uEye)/74.,2.));color=mix(color,vec3(.70,.80,.79),fog*.91);outColor=vec4(color,vOpacity);}`;
|
|
const skyVert = `#version 300 es
|
|
precision highp float;out vec2 vUV;void main(){vec2 p=vec2((gl_VertexID<<1)&2,gl_VertexID&2);vUV=p;gl_Position=vec4(p*2.-1.,1.,1.);}`;
|
|
const skyFrag = `#version 300 es
|
|
precision highp float;in vec2 vUV;uniform float uPitch;uniform float uYaw;uniform float uAspect;out vec4 outColor;void main(){float h=vUV.y+uPitch*.72;vec3 color=mix(vec3(.77,.83,.77),vec3(.38,.65,.78),smoothstep(.14,1.2,h));vec2 sun=vec2(.77-uYaw*.3,.82-uPitch*.72);float d=length((vUV-sun)*vec2(uAspect,1.));color=mix(color,vec3(1.,.98,.8),smoothstep(.048,.039,d)*.98);color+=vec3(.045,.025,0.)*exp(-d*7.);outColor=vec4(color,1.);}`;
|
|
const lineVertex = `#version 300 es
|
|
precision highp float;layout(location=0)in vec3 aPos;uniform mat4 uVP;void main(){gl_Position=uVP*vec4(aPos,1.);}`;
|
|
const lineFragment = `#version 300 es
|
|
precision highp float;out vec4 outColor;void main(){outColor=vec4(.13,.21,.12,1.);}`;
|
|
const faces = [
|
|
{
|
|
n: [1, 0, 0],
|
|
v: [
|
|
[1, 0, 1],
|
|
[1, 0, 0],
|
|
[1, 1, 0],
|
|
[1, 1, 1],
|
|
],
|
|
},
|
|
{
|
|
n: [-1, 0, 0],
|
|
v: [
|
|
[0, 0, 0],
|
|
[0, 0, 1],
|
|
[0, 1, 1],
|
|
[0, 1, 0],
|
|
],
|
|
},
|
|
{
|
|
n: [0, 1, 0],
|
|
v: [
|
|
[0, 1, 1],
|
|
[1, 1, 1],
|
|
[1, 1, 0],
|
|
[0, 1, 0],
|
|
],
|
|
},
|
|
{
|
|
n: [0, -1, 0],
|
|
v: [
|
|
[0, 0, 0],
|
|
[1, 0, 0],
|
|
[1, 0, 1],
|
|
[0, 0, 1],
|
|
],
|
|
},
|
|
{
|
|
n: [0, 0, 1],
|
|
v: [
|
|
[0, 0, 1],
|
|
[1, 0, 1],
|
|
[1, 1, 1],
|
|
[0, 1, 1],
|
|
],
|
|
},
|
|
{
|
|
n: [0, 0, -1],
|
|
v: [
|
|
[1, 0, 0],
|
|
[0, 0, 0],
|
|
[0, 1, 0],
|
|
[1, 1, 0],
|
|
],
|
|
},
|
|
];
|
|
function program(gl, vs, fs) {
|
|
const p = gl.createProgram();
|
|
for (const [type, source] of [
|
|
[gl.VERTEX_SHADER, vs],
|
|
[gl.FRAGMENT_SHADER, fs],
|
|
]) {
|
|
const s = gl.createShader(type);
|
|
gl.shaderSource(s, source);
|
|
gl.compileShader(s);
|
|
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS))
|
|
throw Error(gl.getShaderInfoLog(s));
|
|
gl.attachShader(p, s);
|
|
gl.deleteShader(s);
|
|
}
|
|
gl.linkProgram(p);
|
|
if (!gl.getProgramParameter(p, gl.LINK_STATUS))
|
|
throw Error(gl.getProgramInfoLog(p));
|
|
return p;
|
|
}
|
|
function boxVertices(
|
|
out,
|
|
pos,
|
|
box,
|
|
color,
|
|
material = 0,
|
|
skip = () => false,
|
|
yaw = 0,
|
|
opacity = 1,
|
|
) {
|
|
for (let f = 0; f < 6; f++) {
|
|
if (skip(faces[f].n)) continue;
|
|
const face = faces[f],
|
|
c = Math.cos(yaw),
|
|
s = Math.sin(yaw),
|
|
n = [
|
|
face.n[0] * c - face.n[2] * s,
|
|
face.n[1],
|
|
face.n[0] * s + face.n[2] * c,
|
|
];
|
|
for (const i of [0, 1, 2, 0, 2, 3]) {
|
|
const v = face.v[i].map(
|
|
(v, k) => box.min[k] + v * (box.max[k] - box.min[k]),
|
|
);
|
|
out.push(
|
|
pos[0] + v[0] * c - v[2] * s,
|
|
pos[1] + v[1],
|
|
pos[2] + v[0] * s + v[2] * c,
|
|
...color,
|
|
...n,
|
|
material,
|
|
opacity,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
function materialType(state = "", effect) {
|
|
if (effect === "bounce") return 5;
|
|
if (state.includes("grass_block")) return 1;
|
|
if (state.includes("planks")) return 2;
|
|
if (state.includes("brick")) return 3;
|
|
if (/log|stem|wood/.test(state)) return 4;
|
|
return 0;
|
|
}
|
|
const section = (p) => p.map((v) => Math.floor(v / 16)).join(",");
|
|
export class Renderer {
|
|
constructor(canvas) {
|
|
this.canvas = canvas;
|
|
const gl = (this.gl = canvas.getContext("webgl2", {
|
|
antialias: true,
|
|
alpha: false,
|
|
preserveDrawingBuffer: true,
|
|
powerPreference: "high-performance",
|
|
}));
|
|
if (!gl)
|
|
throw Error(
|
|
"WebGL2 недоступен. Включите аппаратное ускорение или откройте клиент в браузере с поддержкой WebGL2.",
|
|
);
|
|
this.program = program(gl, vertex, fragment);
|
|
this.sky = program(gl, skyVert, skyFrag);
|
|
this.line = program(gl, lineVertex, lineFragment);
|
|
this.sections = new Map();
|
|
this.dirty = new Set();
|
|
this.blocks = new Map();
|
|
this.materials = new Map();
|
|
this.texture = gl.createTexture();
|
|
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
|
gl.texImage2D(
|
|
gl.TEXTURE_2D,
|
|
0,
|
|
gl.RGBA,
|
|
1,
|
|
1,
|
|
0,
|
|
gl.RGBA,
|
|
gl.UNSIGNED_BYTE,
|
|
new Uint8Array([180, 180, 180, 255]),
|
|
);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
this.textured = false;
|
|
this.effectTexture = gl.createTexture();
|
|
gl.bindTexture(gl.TEXTURE_2D, this.effectTexture);
|
|
gl.texImage2D(
|
|
gl.TEXTURE_2D,
|
|
0,
|
|
gl.RGBA,
|
|
1,
|
|
1,
|
|
0,
|
|
gl.RGBA,
|
|
gl.UNSIGNED_BYTE,
|
|
new Uint8Array([180, 180, 180, 255]),
|
|
);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
this.effectTextured = false;
|
|
this.bounceAt = -10000;
|
|
this.dynamic = this.mesh([]);
|
|
this.lines = this.mesh([], 3);
|
|
this.triangles = 0;
|
|
this.vp = new Float32Array(16);
|
|
this.lost = false;
|
|
canvas.addEventListener("webglcontextlost", (e) => {
|
|
e.preventDefault();
|
|
this.lost = true;
|
|
});
|
|
canvas.addEventListener("webglcontextrestored", () => location.reload());
|
|
}
|
|
mesh(data, stride = 11) {
|
|
const gl = this.gl,
|
|
vao = gl.createVertexArray(),
|
|
buffer = gl.createBuffer();
|
|
gl.bindVertexArray(vao);
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.DYNAMIC_DRAW);
|
|
for (const [loc, size, offset] of stride === 3
|
|
? [[0, 3, 0]]
|
|
: [
|
|
[0, 3, 0],
|
|
[1, 3, 3],
|
|
[2, 3, 6],
|
|
[3, 1, 9],
|
|
[4, 1, 10],
|
|
]) {
|
|
gl.enableVertexAttribArray(loc);
|
|
gl.vertexAttribPointer(
|
|
loc,
|
|
size,
|
|
gl.FLOAT,
|
|
false,
|
|
stride * 4,
|
|
offset * 4,
|
|
);
|
|
}
|
|
return { vao, buffer, count: data.length / stride };
|
|
}
|
|
disposeMesh(mesh) {
|
|
if (mesh.transparent) this.disposeMesh(mesh.transparent);
|
|
this.gl.deleteBuffer(mesh.buffer);
|
|
this.gl.deleteVertexArray(mesh.vao);
|
|
}
|
|
upload(mesh, data, stride = 11) {
|
|
const gl = this.gl;
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, mesh.buffer);
|
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.DYNAMIC_DRAW);
|
|
mesh.count = data.length / stride;
|
|
}
|
|
setBounceShader(source) {
|
|
if (source.length > 32768) throw Error("Шейдер пакета слишком велик");
|
|
const next = program(
|
|
this.gl,
|
|
vertex,
|
|
fragment.replace(
|
|
"vec3 shacraftBounceTint(vec3 color,float pulse){return color;}",
|
|
source,
|
|
),
|
|
);
|
|
this.gl.deleteProgram(this.program);
|
|
this.program = next;
|
|
}
|
|
async setTexture(blob, effect = false) {
|
|
const bitmap = await createImageBitmap(blob);
|
|
this.gl.bindTexture(
|
|
this.gl.TEXTURE_2D,
|
|
effect ? this.effectTexture : this.texture,
|
|
);
|
|
this.gl.texImage2D(
|
|
this.gl.TEXTURE_2D,
|
|
0,
|
|
this.gl.RGBA,
|
|
this.gl.RGBA,
|
|
this.gl.UNSIGNED_BYTE,
|
|
bitmap,
|
|
);
|
|
this.gl.texParameteri(
|
|
this.gl.TEXTURE_2D,
|
|
this.gl.TEXTURE_WRAP_S,
|
|
this.gl.REPEAT,
|
|
);
|
|
this.gl.texParameteri(
|
|
this.gl.TEXTURE_2D,
|
|
this.gl.TEXTURE_WRAP_T,
|
|
this.gl.REPEAT,
|
|
);
|
|
bitmap.close();
|
|
if (effect) this.effectTextured = true;
|
|
else this.textured = true;
|
|
}
|
|
replace(blocks, materials) {
|
|
for (const m of this.sections.values()) this.disposeMesh(m);
|
|
this.sections.clear();
|
|
this.dirty.clear();
|
|
this.blocks = blocks;
|
|
this.materials = materials;
|
|
for (const k of blocks.keys())
|
|
this.dirty.add(section(k.split(",").map(Number)));
|
|
}
|
|
change(changes) {
|
|
for (const { pos } of changes) {
|
|
this.dirty.add(section(pos));
|
|
for (const f of faces)
|
|
this.dirty.add(section(pos.map((v, i) => v + f.n[i])));
|
|
}
|
|
}
|
|
invalidate() {
|
|
for (const k of this.blocks.keys())
|
|
this.dirty.add(section(k.split(",").map(Number)));
|
|
}
|
|
rebuild(id) {
|
|
const base = id.split(",").map((v) => Number(v) * 16),
|
|
vertices = [],
|
|
transparent = [];
|
|
for (let x = base[0]; x < base[0] + 16; x++)
|
|
for (let y = base[1]; y < base[1] + 16; y++)
|
|
for (let z = base[2]; z < base[2] + 16; z++) {
|
|
const pos = [x, y, z],
|
|
block = this.blocks.get(key(pos));
|
|
if (!block) continue;
|
|
const mat = this.materials.get(block) || {
|
|
color: [150, 152, 142],
|
|
render: [unitBox],
|
|
},
|
|
color = mat.color.map((v) => v / 255),
|
|
full = isFullCube(mat);
|
|
for (const box of mat.render || [unitBox]) {
|
|
boxVertices(
|
|
mat.opacity < 1 ? transparent : vertices,
|
|
pos,
|
|
box,
|
|
color,
|
|
materialType(mat.state, mat.effect),
|
|
(n) =>
|
|
full &&
|
|
isFullCube(
|
|
this.materials.get(
|
|
this.blocks.get(key(pos.map((v, i) => v + n[i]))),
|
|
),
|
|
),
|
|
0,
|
|
mat.opacity ?? 1,
|
|
);
|
|
}
|
|
}
|
|
let mesh = this.sections.get(id);
|
|
if (mesh) this.upload(mesh, vertices);
|
|
else if (vertices.length || transparent.length) {
|
|
mesh = this.mesh(vertices);
|
|
this.sections.set(id, mesh);
|
|
}
|
|
if (mesh) {
|
|
if (mesh.transparent) this.upload(mesh.transparent, transparent);
|
|
else if (transparent.length) mesh.transparent = this.mesh(transparent);
|
|
}
|
|
if (mesh && vertices.length === 0 && transparent.length === 0) {
|
|
this.disposeMesh(mesh);
|
|
this.sections.delete(id);
|
|
}
|
|
}
|
|
updateSelection(hit) {
|
|
const a = [];
|
|
if (hit) {
|
|
const mat = this.materials.get(hit.block),
|
|
boxes = mat?.render?.length ? mat.render : [unitBox];
|
|
for (const box of boxes) {
|
|
const points = [];
|
|
for (let i = 0; i < 8; i++)
|
|
points.push(
|
|
[0, 1, 2].map(
|
|
(k) =>
|
|
hit.pos[k] +
|
|
((i >> k) & 1 ? box.max[k] + 0.003 : box.min[k] - 0.003),
|
|
),
|
|
);
|
|
for (let i = 0; i < 8; i++)
|
|
for (let axis = 0; axis < 3; axis++)
|
|
if (!(i & (1 << axis)))
|
|
a.push(...points[i], ...points[i | (1 << axis)]);
|
|
}
|
|
}
|
|
this.upload(this.lines, a, 3);
|
|
}
|
|
draw(camera, players, entities, entityDefinitions) {
|
|
if (this.lost) return;
|
|
const gl = this.gl,
|
|
ratio = Math.min(devicePixelRatio || 1, 1.75),
|
|
w = Math.floor(this.canvas.clientWidth * ratio),
|
|
h = Math.floor(this.canvas.clientHeight * ratio);
|
|
if (this.canvas.width !== w || this.canvas.height !== h) {
|
|
this.canvas.width = w;
|
|
this.canvas.height = h;
|
|
}
|
|
gl.viewport(0, 0, w, h);
|
|
gl.clearColor(0.65, 0.79, 0.8, 1);
|
|
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
gl.disable(gl.DEPTH_TEST);
|
|
gl.disable(gl.CULL_FACE);
|
|
gl.useProgram(this.sky);
|
|
gl.uniform1f(gl.getUniformLocation(this.sky, "uPitch"), camera.pitch);
|
|
gl.uniform1f(gl.getUniformLocation(this.sky, "uYaw"), camera.yaw);
|
|
gl.uniform1f(gl.getUniformLocation(this.sky, "uAspect"), w / h);
|
|
gl.bindVertexArray(null);
|
|
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
this.vp = multiply(
|
|
perspective(Math.PI / 2.7, w / h, 0.06, 190),
|
|
viewMatrix(camera.eye, camera.yaw, camera.pitch),
|
|
);
|
|
const start = performance.now();
|
|
let rebuilt = 0;
|
|
for (const id of this.dirty) {
|
|
this.rebuild(id);
|
|
this.dirty.delete(id);
|
|
rebuilt++;
|
|
if (rebuilt >= 3 || performance.now() - start > 7) break;
|
|
}
|
|
gl.enable(gl.DEPTH_TEST);
|
|
gl.enable(gl.CULL_FACE);
|
|
gl.cullFace(gl.BACK);
|
|
gl.frontFace(gl.CCW);
|
|
gl.useProgram(this.program);
|
|
gl.uniformMatrix4fv(
|
|
gl.getUniformLocation(this.program, "uVP"),
|
|
false,
|
|
this.vp,
|
|
);
|
|
gl.uniform3fv(gl.getUniformLocation(this.program, "uEye"), camera.eye);
|
|
gl.activeTexture(gl.TEXTURE0);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
|
gl.uniform1i(gl.getUniformLocation(this.program, "uTexture"), 0);
|
|
gl.uniform1i(
|
|
gl.getUniformLocation(this.program, "uTextured"),
|
|
this.textured ? 1 : 0,
|
|
);
|
|
gl.activeTexture(gl.TEXTURE1);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.effectTexture);
|
|
gl.uniform1i(gl.getUniformLocation(this.program, "uEffectTexture"), 1);
|
|
gl.uniform1i(
|
|
gl.getUniformLocation(this.program, "uEffectTextured"),
|
|
this.effectTextured ? 1 : 0,
|
|
);
|
|
gl.uniform1f(
|
|
gl.getUniformLocation(this.program, "uPulse"),
|
|
Math.max(0, 1 - (performance.now() - this.bounceAt) / 600),
|
|
);
|
|
this.triangles = 0;
|
|
for (const [id, m] of this.sections) {
|
|
const center = id.split(",").map((v) => Number(v) * 16 + 8);
|
|
if (
|
|
Math.hypot(center[0] - camera.eye[0], center[2] - camera.eye[2]) > 135
|
|
)
|
|
continue;
|
|
gl.bindVertexArray(m.vao);
|
|
gl.drawArrays(gl.TRIANGLES, 0, m.count);
|
|
this.triangles += m.count / 3;
|
|
}
|
|
const dynamic = [];
|
|
for (const p of players) {
|
|
const c = p.color || [0.29, 0.46, 0.36],
|
|
yaw = p.yaw || 0;
|
|
boxVertices(
|
|
dynamic,
|
|
p.position,
|
|
{ min: [-0.21, 0.63, -0.13], max: [0.21, 1.27, 0.13] },
|
|
c,
|
|
2,
|
|
() => false,
|
|
yaw,
|
|
);
|
|
boxVertices(
|
|
dynamic,
|
|
p.position,
|
|
{ min: [-0.23, 1.27, -0.23], max: [0.23, 1.73, 0.23] },
|
|
[0.77, 0.61, 0.43],
|
|
0,
|
|
() => false,
|
|
yaw,
|
|
);
|
|
for (const side of [-1, 1]) {
|
|
boxVertices(
|
|
dynamic,
|
|
p.position,
|
|
{
|
|
min: [side * 0.13 - 0.085, 0.02, -0.09],
|
|
max: [side * 0.13 + 0.085, 0.63, 0.09],
|
|
},
|
|
[0.25, 0.3, 0.32],
|
|
0,
|
|
() => false,
|
|
yaw,
|
|
);
|
|
boxVertices(
|
|
dynamic,
|
|
p.position,
|
|
{
|
|
min: [side * 0.31 - 0.085, 0.63, -0.1],
|
|
max: [side * 0.31 + 0.085, 1.24, 0.1],
|
|
},
|
|
c,
|
|
0,
|
|
() => false,
|
|
yaw,
|
|
);
|
|
}
|
|
boxVertices(
|
|
dynamic,
|
|
p.position,
|
|
{ min: [-0.18, 1.56, -0.234], max: [0.18, 1.64, -0.226] },
|
|
[0.13, 0.19, 0.15],
|
|
0,
|
|
() => false,
|
|
yaw,
|
|
);
|
|
}
|
|
for (const e of entities) {
|
|
const def = entityDefinitions.get(e.kind || e.name) || {},
|
|
color = (e.color || def.color || [192, 161, 123]).map((v) => v / 255),
|
|
boxes = e.render ||
|
|
def.render || [{ min: [-0.3, 0, -0.3], max: [0.3, 0.8, 0.3] }];
|
|
for (const b of boxes)
|
|
boxVertices(dynamic, e.position, b, color, 0, () => false, e.yaw || 0);
|
|
}
|
|
this.upload(this.dynamic, dynamic);
|
|
gl.bindVertexArray(this.dynamic.vao);
|
|
gl.drawArrays(gl.TRIANGLES, 0, this.dynamic.count);
|
|
this.triangles += this.dynamic.count / 3;
|
|
gl.enable(gl.BLEND);
|
|
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
gl.depthMask(false);
|
|
const translucent = [...this.sections]
|
|
.filter(([, m]) => m.transparent?.count)
|
|
.sort((a, b) => {
|
|
const dist = (id) =>
|
|
Math.hypot(
|
|
...id.split(",").map((v, i) => Number(v) * 16 + 8 - camera.eye[i]),
|
|
);
|
|
return dist(b[0]) - dist(a[0]);
|
|
});
|
|
for (const [, m] of translucent) {
|
|
gl.bindVertexArray(m.transparent.vao);
|
|
gl.drawArrays(gl.TRIANGLES, 0, m.transparent.count);
|
|
this.triangles += m.transparent.count / 3;
|
|
}
|
|
gl.depthMask(true);
|
|
gl.disable(gl.BLEND);
|
|
if (this.lines.count) {
|
|
gl.useProgram(this.line);
|
|
gl.uniformMatrix4fv(
|
|
gl.getUniformLocation(this.line, "uVP"),
|
|
false,
|
|
this.vp,
|
|
);
|
|
gl.bindVertexArray(this.lines.vao);
|
|
gl.drawArrays(gl.LINES, 0, this.lines.count);
|
|
}
|
|
gl.bindVertexArray(null);
|
|
}
|
|
project(p) {
|
|
return project(
|
|
p,
|
|
this.vp,
|
|
this.canvas.clientWidth,
|
|
this.canvas.clientHeight,
|
|
);
|
|
}
|
|
}
|