import { multiply, perspective, lookAt, modelMatrix } from './math3d.mjs'; import { cubeFaces } from './cube-mesh.mjs'; export function makeScene(canvas) { const gl = canvas.getContext('webgl', { antialias: true, alpha: false }); if (!gl) throw new Error('Trình duyệt này chưa hỗ trợ WebGL. Anh có thể chơi bản 2D hoặc bật hỗ trợ đồ họa phần cứng để thử 3D.'); const vertex = `attribute vec3 aPosition;attribute vec3 aNormal;uniform mat4 uModel;uniform mat4 uVP;uniform vec3 uEye;varying float vLight;varying float vDistance;void main(){vec4 world=uModel*vec4(aPosition,1.0);gl_Position=uVP*world;vec3 normal=normalize(mat3(uModel)*aNormal);vLight=.4+.6*max(dot(normal,normalize(vec3(.5,1.0,.3))),0.0);vDistance=length(world.xyz-uEye);}`; const fragment = `precision mediump float;uniform vec3 uColor;varying float vLight;varying float vDistance;void main(){vec3 color=uColor*vLight;float fog=smoothstep(5.0,18.0,vDistance);gl_FragColor=vec4(mix(color,vec3(.045,.08,.14),fog*.7),1.0);}`; const compile = (type, source) => { const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) throw new Error('Chưa khởi tạo được bộ dựng hình WebGL.'); return shader; }; const program = gl.createProgram(); gl.attachShader(program, compile(gl.VERTEX_SHADER, vertex)); gl.attachShader(program, compile(gl.FRAGMENT_SHADER, fragment)); gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error('Chưa liên kết được chương trình đồ họa 3D.'); gl.useProgram(program); const vertices = []; for (const [normal, corners] of cubeFaces) for (const index of [0, 1, 2, 0, 2, 3]) vertices.push(...corners[index], ...normal); const buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); for (const [name, offset] of [['aPosition', 0], ['aNormal', 12]]) { const location = gl.getAttribLocation(program, name); gl.enableVertexAttribArray(location); gl.vertexAttribPointer(location, 3, gl.FLOAT, false, 24, offset); } const uniforms = Object.fromEntries(['uModel', 'uVP', 'uColor', 'uEye'].map(key => [key, gl.getUniformLocation(program, key)])); gl.enable(gl.DEPTH_TEST); gl.clearColor(.045, .08, .14, 1); function box(x, y, z, sx, sy, sz, color, yaw = 0) { gl.uniformMatrix4fv(uniforms.uModel, false, modelMatrix(x, y, z, sx, sy, sz, yaw)); gl.uniform3fv(uniforms.uColor, color); gl.drawArrays(gl.TRIANGLES, 0, 36); } return { render(game, overview = false) { const ratio = Math.min(window.devicePixelRatio || 1, 1.7), width = Math.round(canvas.clientWidth * ratio), height = Math.round(canvas.clientHeight * ratio); if (canvas.width !== width || canvas.height !== height) { canvas.width = width; canvas.height = height; } gl.viewport(0, 0, width, height); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); const p = game.player, n = game.grid.length, eye = overview ? [n / 2, n * .95, n * 1.05] : [p.x, .66, p.z], target = overview ? [n / 2, 0, n / 2] : [p.x + Math.sin(p.yaw), .66, p.z - Math.cos(p.yaw)]; const vp = multiply(perspective(Math.PI / 2.6, width / height, .05, 60), lookAt(eye, target)); gl.uniformMatrix4fv(uniforms.uVP, false, vp); gl.uniform3fv(uniforms.uEye, eye); box(n / 2, -.08, n / 2, n, .15, n, [.16, .22, .3]); game.grid.forEach((row, z) => row.forEach((wall, x) => { if (wall) box(x + .5, .6, z + .5, 1, 1.2, 1, (x + z) % 3 === 0 ? [.23, .35, .48] : [.18, .28, .4]); else if ((x + z) % 2 === 0) box(x + .5, .006, z + .5, .94, .015, .94, [.2, .27, .34]); })); for (const gem of game.gems) if (!gem.taken) { box(gem.x, .39 + Math.sin(game.time * 2) * .06, gem.z, .23, .38, .23, [.23, .94, .76], game.time); box(gem.x, .025, gem.z, .55, .035, .55, [.08, .36, .33]); } box(game.exit.x, .03, game.exit.z, .72, .06, .72, game.collected === game.gems.length ? [1, .66, .2] : [.45, .32, .24]); for (const x of [-.31, .31]) box(game.exit.x + x, .58, game.exit.z, .08, 1.1, .1, [.95, .63, .25]); box(game.exit.x, 1.12, game.exit.z, .7, .1, .1, [.95, .63, .25]); if (overview) box(p.x, .28, p.z, .3, .55, .3, [.95, .45, .3], p.yaw); } }; }