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