Files
shacraft-core/client/renderer.js
T

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,
);
}
}