diff --git a/assets.js b/assets.js index 3c23b957..6cc41e7e 100644 --- a/assets.js +++ b/assets.js @@ -35,9 +35,9 @@ export async function loadModel(id) { // Parents sometimes use block/ or item/ prefixes directly const parentId = model.parent.includes(':') ? model.parent : `minecraft:${model.parent}`; const parent = await loadModel(parentId); - + // Merge textures - model.textures = { ...parent.textures, ...model.textures }; + model.textures = {...parent.textures, ...model.textures}; // Inherit elements if child doesn't override them if (!model.elements) model.elements = parent.elements; } @@ -49,12 +49,8 @@ export class TextureAtlas { this.canvas = document.createElement('canvas'); this.canvas.width = size; this.canvas.height = size; - this.ctx = this.canvas.getContext('2d', { willReadFrequently: true }); + this.ctx = this.canvas.getContext('2d', {willReadFrequently: true}); this.ctx.imageSmoothingEnabled = false; - - // Magenta background for missing textures - this.ctx.fillStyle = '#ff00ff'; - this.ctx.fillRect(0, 0, size, size); this.map = new Map(); this.cellSize = 16; @@ -67,7 +63,17 @@ export class TextureAtlas { if (this.map.has(id)) return this.map.get(id); const url = resolveResourceLocation(id, 'textures', 'png'); - + + // Calculate position before trying to load, so we can draw a fallback if it fails + if (this.x + this.cellSize > this.canvas.width) { + this.x = 0; + this.y += this.rowHeight; + this.rowHeight = 0; + } + + const currentX = this.x; + const currentY = this.y; + try { const img = await new Promise((resolve, reject) => { const i = new Image(); @@ -77,29 +83,32 @@ export class TextureAtlas { i.src = url; }); - if (this.x + this.cellSize > this.canvas.width) { - this.x = 0; - this.y += this.rowHeight; - this.rowHeight = 0; - } - - this.ctx.drawImage(img, this.x, this.y, this.cellSize, this.cellSize); - - const uvData = { - u: this.x / this.canvas.width, - v: this.y / this.canvas.height, - du: this.cellSize / this.canvas.width, - dv: this.cellSize / this.canvas.height - }; - - this.map.set(id, uvData); - this.x += this.cellSize; - this.rowHeight = Math.max(this.rowHeight, this.cellSize); - - return uvData; + this.ctx.drawImage(img, currentX, currentY, this.cellSize, this.cellSize); } catch (e) { console.warn(`Texture missing: ${id}`); - return null; + // Draw magenta square ONLY for missing textures + this.ctx.fillStyle = '#ff00ff'; + this.ctx.fillRect(currentX, currentY, this.cellSize, this.cellSize); } + + const epsU = 0.1 / this.canvas.width; + const epsV = 0.1 / this.canvas.height; + + const uvData = { + // Push the start coordinate slightly inward + u: (currentX / this.canvas.width) + epsU, + v: (currentY / this.canvas.height) + epsV, + // Shrink the total width/height to account for the inset on both sides + du: (this.cellSize / this.canvas.width) - (epsU * 2), + dv: (this.cellSize / this.canvas.height) - (epsV * 2) + }; + + this.map.set(id, uvData); + this.map.set(id, uvData); + + this.x += this.cellSize; + this.rowHeight = Math.max(this.rowHeight, this.cellSize); + + return uvData; } } \ No newline at end of file diff --git a/blockstate.js b/blockstate.js new file mode 100644 index 00000000..e7c799ae --- /dev/null +++ b/blockstate.js @@ -0,0 +1,46 @@ +function evaluateWhen(properties, condition) { + if (!condition) return true; + if (condition.OR) return condition.OR.some(sub => evaluateWhen(properties, sub)); + if (condition.AND) return condition.AND.every(sub => evaluateWhen(properties, sub)); + + for (const [key, val] of Object.entries(condition)) { + if (key === 'OR' || key === 'AND') continue; + const prop = properties[key]; + if (typeof val === 'string' && val.includes('|')) { + if (!val.split('|').includes(prop)) return false; + } else if (prop !== val) { + return false; + } + } + return true; +} + +export function resolveBlock(state, properties) { + const parts = []; + + if (state.variants) { + // Handle standard variants + const propStr = Object.keys(properties).sort().map(k => `${k}=${properties[k]}`).join(','); + let variantDef = state.variants[propStr] || state.variants[""] || state.variants["normal"]; + if (Array.isArray(variantDef)) variantDef = variantDef[0]; // Take first random model + + if (variantDef) parts.push(variantDef); + } else if (state.multipart) { + // Handle multipart + for (const part of state.multipart) { + if (evaluateWhen(properties, part.when)) { + let applyDef = part.apply; + if (Array.isArray(applyDef)) applyDef = applyDef[0]; + parts.push(applyDef); + } + } + } + + // Returns an array of definitions: [{ model: "block/wall_post", y: 90, uvlock: true }, ...] + return parts; +} + +export function getVariantHash(part) { + // Uniquely identifies a baked geometry variant so we can pool instances + return `${part.model}#y=${part.y || 0},x=${part.x || 0},uvlock=${!!part.uvlock}`; +} \ No newline at end of file diff --git a/geometry.js b/geometry.js index 20648499..9a8737b7 100644 --- a/geometry.js +++ b/geometry.js @@ -1,88 +1,134 @@ // geometry.js +import {mat4Identity, mat4RotateY, mat4Translate} from './math.js'; // Assuming you have mat4RotateX too + const FACES = { - down: { n: [0, -1, 0], c: [[0, 0, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1]] }, - up: { n: [0, 1, 0], c: [[0, 1, 1], [1, 1, 1], [1, 1, 0], [0, 1, 0]] }, - north: { n: [0, 0, -1], c: [[1, 0, 0], [0, 0, 0], [0, 1, 0], [1, 1, 0]] }, - south: { n: [0, 0, 1], c: [[0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]] }, - west: { n: [-1, 0, 0], c: [[0, 0, 0], [0, 0, 1], [0, 1, 1], [0, 1, 0]] }, - east: { n: [1, 0, 0], c: [[1, 0, 1], [1, 0, 0], [1, 1, 0], [1, 1, 1]] } + down: {n: [0, -1, 0], c: [[0, 0, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1]]}, + up: {n: [0, 1, 0], c: [[0, 1, 1], [1, 1, 1], [1, 1, 0], [0, 1, 0]]}, + north: {n: [0, 0, -1], c: [[1, 0, 0], [0, 0, 0], [0, 1, 0], [1, 1, 0]]}, + south: {n: [0, 0, 1], c: [[0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]]}, + west: {n: [-1, 0, 0], c: [[0, 0, 0], [0, 0, 1], [0, 1, 1], [0, 1, 0]]}, + east: {n: [1, 0, 0], c: [[1, 0, 1], [1, 0, 0], [1, 1, 0], [1, 1, 1]]} }; +// Calculates missing UVs by projecting the element bounds onto the face plane +function calculateDefaultUV(faceName, from, to) { + switch (faceName) { + case 'up': + return [from[0], from[2], to[0], to[2]]; + case 'down': + return [from[0], 16 - to[2], to[0], 16 - from[2]]; + case 'north': + return [16 - to[0], 16 - to[1], 16 - from[0], 16 - from[1]]; + case 'south': + return [from[0], 16 - to[1], to[0], 16 - from[1]]; + case 'west': + return [from[2], 16 - to[1], to[2], 16 - from[1]]; + case 'east': + return [16 - to[2], 16 - to[1], 16 - from[2], 16 - from[1]]; + default: + return [0, 0, 16, 16]; + } +} + export function resolveTexture(model, ref) { if (!ref) return null; if (ref[0] !== '#') return ref; - let key = ref.slice(1); let val = model.textures?.[key]; - - // Follow variable chain while (typeof val === 'string' && val[0] === '#') { key = val.slice(1); val = model.textures?.[key]; } - - // Handle modern object-style textures - if (val && typeof val === 'object' && val.sprite) { - val = val.sprite; - } - + if (val && typeof val === 'object' && val.sprite) val = val.sprite; return val || null; } -export function buildGeometry(model, atlas) { - const pos = []; - const norm = []; - const uv = []; - const tint = []; - const idx = []; - +// Takes the blockstate variant object to apply y/x rotations and uvlock +export function buildGeometry(model, atlas, variant = {}) { + const pos = [], norm = [], uv = [], tint = [], shade = [], idx = []; let vOffset = 0; + // 1. Pre-calculate the blockstate rotation matrix (around block center) + const blockMatrix = mat4Identity(new Float32Array(16)); + mat4Translate(blockMatrix, blockMatrix, [0.5, 0.5, 0.5]); + // NOTE: Vanilla blockstates also support `x` rotation. You'd add mat4RotateX here if needed. + if (variant.y) mat4RotateY(blockMatrix, blockMatrix, -variant.y * Math.PI / 180); + mat4Translate(blockMatrix, blockMatrix, [-0.5, -0.5, -0.5]); + for (const el of model.elements || []) { - // Map 0-16 voxel scale to 0.0-1.0 world scale const [fx, fy, fz] = el.from.map(x => x / 16); const [tx, ty, tz] = el.to.map(x => x / 16); const size = [tx - fx, ty - fy, tz - fz]; + // Element-level shading flag. Used to make repeater torches/redstone dust emissive. + const elementShade = el.shade === false ? 0.0 : 1.0; + for (const [name, face] of Object.entries(el.faces || {})) { const tmpl = FACES[name]; const texPath = resolveTexture(model, face.texture); const a = texPath ? atlas.map.get(texPath) : null; - - // Skip faces with missing textures in the atlas if (!a) continue; - // Basic UV fallback if omitted: this is a placeholder. - // Real implementation needs mapping based on face direction and element bounds. - const u1 = (face.uv?.[0] ?? 0) / 16; - const v1 = (face.uv?.[1] ?? 0) / 16; - const u2 = (face.uv?.[2] ?? 16) / 16; - const v2 = (face.uv?.[3] ?? 16) / 16; + // DYNAMIC UV PROJECTION (remains the same) + const rawUV = face.uv || calculateDefaultUV(name, el.from, el.to); + let u1 = rawUV[0] / 16, v1 = rawUV[1] / 16; + let u2 = rawUV[2] / 16, v2 = rawUV[3] / 16; - const au1 = a.u + u1 * a.du; - const au2 = a.u + u2 * a.du; - // WebGL wants V flipped natively compared to Three.js canvas - const av1 = a.v + v1 * a.dv; - const av2 = a.v + v2 * a.dv; + let texRot = face.rotation || 0; + + // uvlock compensation + if (variant.uvlock && (name === 'up' || name === 'down') && variant.y) { + texRot = (texRot - variant.y + 360) % 360; + } + + const au1 = a.u + u1 * a.du, au2 = a.u + u2 * a.du; + const av1 = a.v + v1 * a.dv, av2 = a.v + v2 * a.dv; + + // CORRECTED UV ARRAY MAPPING + // Corner Order: 0:Bottom-Left, 1:Bottom-Right, 2:Top-Right, 3:Top-Left + let faceUVs; + if (texRot === 90) { + // Shift UVs clockwise by 1 corner + faceUVs = [au2, av2, au2, av1, au1, av1, au1, av2]; + } else if (texRot === 180) { + // Shift UVs by 2 corners + faceUVs = [au2, av1, au1, av1, au1, av2, au2, av2]; + } else if (texRot === 270) { + // Shift UVs by 3 corners + faceUVs = [au1, av1, au1, av2, au2, av2, au2, av1]; + } else { + // 0 degrees: av2 (bottom) goes to corners 0 and 1. av1 (top) goes to corners 2 and 3. + faceUVs = [au1, av2, au2, av2, au2, av1, au1, av1]; + } const tintable = face.tintindex !== undefined ? 1.0 : 0.0; for (let i = 0; i < 4; i++) { const [cx, cy, cz] = tmpl.c[i]; - - // TODO: Apply el.rotation matrix here later - pos.push(fx + cx * size[0], fy + cy * size[1], fz + cz * size[2]); - norm.push(...tmpl.n); + let vx = fx + cx * size[0]; + let vy = fy + cy * size[1]; + let vz = fz + cz * size[2]; + + // Apply Blockstate Rotation + let wx = blockMatrix[0] * vx + blockMatrix[4] * vy + blockMatrix[8] * vz + blockMatrix[12]; + let wy = blockMatrix[1] * vx + blockMatrix[5] * vy + blockMatrix[9] * vz + blockMatrix[13]; + let wz = blockMatrix[2] * vx + blockMatrix[6] * vy + blockMatrix[10] * vz + blockMatrix[14]; + + pos.push(wx, wy, wz); + + // Note: Normal rotation should technically use the inverse-transpose of the matrix, + // but since we only have pure rotations, direct multiplication works fine here. + let nx = blockMatrix[0] * tmpl.n[0] + blockMatrix[4] * tmpl.n[1] + blockMatrix[8] * tmpl.n[2]; + let ny = blockMatrix[1] * tmpl.n[0] + blockMatrix[5] * tmpl.n[1] + blockMatrix[9] * tmpl.n[2]; + let nz = blockMatrix[2] * tmpl.n[0] + blockMatrix[6] * tmpl.n[1] + blockMatrix[10] * tmpl.n[2]; + + norm.push(nx, ny, nz); tint.push(tintable); + shade.push(elementShade); } - uv.push(au1, av1, au2, av1, au2, av2, au1, av2); - - // Push 2 triangles per quad - idx.push( - vOffset, vOffset + 1, vOffset + 2, - vOffset, vOffset + 2, vOffset + 3 - ); + uv.push(...faceUVs); + idx.push(vOffset, vOffset + 1, vOffset + 2, vOffset, vOffset + 2, vOffset + 3); vOffset += 4; } } @@ -92,6 +138,7 @@ export function buildGeometry(model, atlas) { normals: new Float32Array(norm), uvs: new Float32Array(uv), tints: new Float32Array(tint), + shades: new Float32Array(shade), indices: new Uint16Array(idx) }; } \ No newline at end of file diff --git a/index.html b/index.html index b99f8108..bf134aee 100644 --- a/index.html +++ b/index.html @@ -40,20 +40,13 @@ import {loadBlockstate, loadModel, TextureAtlas} from './assets.js'; import {resolveTexture, buildGeometry} from './geometry.js'; import {Renderer} from './renderer.js'; - import { - mat4Identity, - mat4Ortho, - mat4LookAt, - mat4Multiply, - mat4Translate, - mat4RotateY - } from './math.js'; + import {mat4Identity, mat4Ortho, mat4LookAt, mat4Multiply, mat4Translate} from './math.js'; + import {World} from './world.js'; + import {resolveBlock} from './blockstate.js'; - // Matches block properties against vanilla variant strings - function resolveVariant(blockstate, properties) { - const propStr = Object.keys(properties).sort().map(k => `${k}=${properties[k]}`).join(','); - let variantDef = blockstate.variants[propStr] || blockstate.variants[""]; - return Array.isArray(variantDef) ? variantDef[0] : variantDef; + // Helper to uniquely identify a baked model + function getVariantHash(part) { + return `${part.model}#y=${part.y || 0},x=${part.x || 0},uvlock=${!!part.uvlock}`; } async function testPipeline() { @@ -61,70 +54,103 @@ const atlas = new TextureAtlas(512); document.getElementById('atlas-container').appendChild(atlas.canvas); - const stateId = 'minecraft:repeater'; - const state = await loadBlockstate(stateId); + // 1. Manually Populate World + const world = new World(); - const directions = ['north', 'east', 'south', 'west']; - const instancesToDraw = []; + // Draw a redstone line into a repeater, next to a cobblestone wall + world.setBlock('minecraft:redstone_wire', 0, 0, 0, { + east: 'side', + west: 'up', + north: 'none', + south: 'none', + power: '15' + }); + world.setBlock('minecraft:redstone_wire', 1, 0, 0, { + east: 'side', + west: 'side', + north: 'none', + south: 'none', + power: '3' + }); + world.setBlock('minecraft:repeater', 2, 0, 0, { + facing: 'east', + delay: '1', + locked: 'false', + powered: 'true' + }); + world.setBlock('minecraft:cobblestone_wall', 2, 0, -1, { + up: 'true', + north: 'tall', + south: 'low' + }); - // Generate a 4x4 grid covering all repeater states - for (let x = 0; x < 4; x++) { - for (let z = 0; z < 4; z++) { - const props = { - facing: directions[x], - delay: (z + 1).toString(), - locked: (x % 2 === 0).toString(), - powered: (z % 2 === 0).toString() - }; + // 2. Build Scene Pools + const instancePool = new Map(); - const variant = resolveVariant(state, props); - const model = await loadModel(variant.model); + for (const block of world.blocks.values()) { + const stateJSON = await loadBlockstate(block.id); + const parts = resolveBlock(stateJSON, block.props); - // Load textures - for (const el of model.elements || []) { - for (const face of Object.values(el.faces || {})) { - const texPath = resolveTexture(model, face.texture); - if (texPath) await atlas.load(texPath); - } + for (const part of parts) { + const hash = getVariantHash(part); + if (!instancePool.has(hash)) { + instancePool.set(hash, {partDef: part, matrices: [], colors: []}); } - // For now, we rebuild geometry per block. - // Later, we will cache buffers by model ID. - const geometry = buildGeometry(model, atlas); - - // Calculate instance matrix const matrix = mat4Identity(new Float32Array(16)); + mat4Translate(matrix, matrix, [block.x, block.y, block.z]); - // 1. Move to grid slot - mat4Translate(matrix, matrix, [x * 1.5, 0, z * 1.5]); + const pool = instancePool.get(hash); + pool.matrices.push(...matrix); - // 2. Minecraft rotates around block center (0.5, 0.5, 0.5) - mat4Translate(matrix, matrix, [0.5, 0.5, 0.5]); - if (variant.y) mat4RotateY(matrix, matrix, -variant.y * Math.PI / 180); - mat4Translate(matrix, matrix, [-0.5, -0.5, -0.5]); - - instancesToDraw.push({geometry, matrix}); + // Simple redstone tint calculation (if power exists) + if (block.props.power) { + const p = parseInt(block.props.power, 10); + const red = (0x4B + (p * 12)) / 255; + pool.colors.push(red, 0.0, 0.0); + } else { + pool.colors.push(1.0, 1.0, 1.0); // Default white + } } } + // 3. Bake Geometries & Create WebGL Buffers const webglCanvas = document.getElementById('webgl-canvas'); const renderer = new Renderer(webglCanvas); + const renderableMeshes = []; + + for (const [hash, pool] of instancePool.entries()) { + const modelJSON = await loadModel(pool.partDef.model); + + // Load textures + for (const el of modelJSON.elements || []) { + for (const face of Object.values(el.faces || {})) { + const texPath = resolveTexture(modelJSON, face.texture); + if (texPath) await atlas.load(texPath); + } + } + + // Bake geometry with local rotations + const geometry = buildGeometry(modelJSON, atlas, pool.partDef); + + // Pack instances + const matrixArray = new Float32Array(pool.matrices); + const colorArray = new Float32Array(pool.colors); + const instanceCount = pool.matrices.length / 16; + + const mesh = renderer.createInstancedMesh(geometry, instanceCount, matrixArray, colorArray); + renderableMeshes.push(mesh); + } + renderer.updateAtlas(atlas.canvas); - // Convert raw geometries into WebGL VAOs - const buffers = instancesToDraw.map(inst => renderer.createModelBuffer(inst.geometry)); - - // Camera setup to fit the grid + // 4. Render Setup const proj = new Float32Array(16); const view = new Float32Array(16); const viewProj = new Float32Array(16); - mat4Ortho(proj, -4, 4, -4, 4, -20, 20); - mat4LookAt(view, - 8, 6, 8, // Eye - 2.25, 0, 2.25, // Look at center of grid - 0, 1, 0 - ); + mat4Ortho(proj, -3, 3, -3, 3, -20, 20); + mat4LookAt(view, 6, 5, 6, 1, 0, 0, 0, 1, 0); mat4Multiply(viewProj, proj, view); function render() { @@ -133,20 +159,16 @@ gl.clearColor(0.1, 0.1, 0.12, 1.0); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - for (let i = 0; i < instancesToDraw.length; i++) { - renderer.drawSingleInstance(buffers[i], viewProj, instancesToDraw[i].matrix); + for (const mesh of renderableMeshes) { + renderer.drawInstanced(mesh, viewProj); } - requestAnimationFrame(render); } render(); - document.getElementById('output').innerHTML = `Repeater Stress Test Active`; - } catch (err) { console.error(err); - document.getElementById('output').textContent = 'Error: ' + err.message; } } diff --git a/renderer.js b/renderer.js index 5e913bee..c81d4c30 100644 --- a/renderer.js +++ b/renderer.js @@ -1,4 +1,3 @@ -// renderer.js import {mat4Identity, mat4Multiply, mat4Ortho, mat4LookAt} from './math.js'; const VS_SRC = `#version 300 es @@ -6,10 +5,11 @@ layout(location=0) in vec3 a_position; layout(location=1) in vec3 a_normal; layout(location=2) in vec2 a_uv; layout(location=3) in float a_tint; +layout(location=4) in float a_shade; // 0.0 = emissive, 1.0 = shaded -// Instanced attributes -layout(location=4) in mat4 i_matrix; -layout(location=8) in vec3 i_color; +// Instanced attributes shifted to account for a_shade +layout(location=5) in mat4 i_matrix; +layout(location=9) in vec3 i_color; uniform mat4 u_viewProj; uniform vec3 u_lightDir; @@ -22,30 +22,28 @@ void main() { gl_Position = u_viewProj * i_matrix * vec4(a_position, 1.0); v_uv = a_uv; - // Apply geometry transform to normals for accurate lighting vec3 normal = normalize(mat3(i_matrix) * a_normal); - // Simple Lambertian lighting with ambient base - v_light = max(dot(normal, normalize(u_lightDir)), 0.0) * 0.6 + 0.4; - v_color = mix(vec3(1.0), i_color, a_tint); // Blend tint if face is tintable + // Lambertian lighting + float baseLight = max(dot(normal, normalize(u_lightDir)), 0.0) * 0.6 + 0.4; + + // Mix between full brightness (1.0) and shaded based on the element's shade flag + v_light = mix(1.0, baseLight, a_shade); + + v_color = mix(vec3(1.0), i_color, a_tint); } `; const FS_SRC = `#version 300 es precision highp float; - in vec2 v_uv; in float v_light; in vec3 v_color; - uniform sampler2D u_texture; - out vec4 fragColor; - void main() { vec4 texColor = texture(u_texture, v_uv); - if (texColor.a < 0.1) discard; // Alpha test for foliage/glass - + if (texColor.a < 0.1) discard; fragColor = vec4(texColor.rgb * v_color * v_light, texColor.a); } `; @@ -97,12 +95,13 @@ export class Renderer { gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); } - createModelBuffer(geometry) { + createInstancedMesh(geometry, instanceCount, matrices, colors) { const gl = this.gl; const vao = gl.createVertexArray(); gl.bindVertexArray(vao); - const bindAttr = (loc, data, size) => { + // 1. Bind standard geometry (divisor = 0) + const bindGeomAttr = (loc, data, size) => { const buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW); @@ -110,20 +109,43 @@ export class Renderer { gl.vertexAttribPointer(loc, size, gl.FLOAT, false, 0, 0); }; - bindAttr(0, geometry.positions, 3); - bindAttr(1, geometry.normals, 3); - bindAttr(2, geometry.uvs, 2); - bindAttr(3, geometry.tints, 1); + bindGeomAttr(0, geometry.positions, 3); + bindGeomAttr(1, geometry.normals, 3); + bindGeomAttr(2, geometry.uvs, 2); + bindGeomAttr(3, geometry.tints, 1); + bindGeomAttr(4, geometry.shades, 1); + // 2. Bind Index Buffer const ebo = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ebo); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, geometry.indices, gl.STATIC_DRAW); + // 3. Bind Instanced Attributes (divisor = 1) + const matrixBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, matrixBuffer); + gl.bufferData(gl.ARRAY_BUFFER, matrices, gl.STATIC_DRAW); + + // Mat4 requires 4 separate vec4 attributes in WebGL + for (let i = 0; i < 4; i++) { + const loc = 5 + i; + gl.enableVertexAttribArray(loc); + gl.vertexAttribPointer(loc, 4, gl.FLOAT, false, 64, i * 16); + gl.vertexAttribDivisor(loc, 1); + } + + const colorBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); + gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW); + gl.enableVertexAttribArray(9); + gl.vertexAttribPointer(9, 3, gl.FLOAT, false, 0, 0); + gl.vertexAttribDivisor(9, 1); + gl.bindVertexArray(null); - return {vao, indexCount: geometry.indices.length}; + + return {vao, indexCount: geometry.indices.length, instanceCount}; } - drawSingleInstance(modelBuffer, viewProjMatrix, modelMatrix) { + drawInstanced(mesh, viewProjMatrix) { const gl = this.gl; gl.useProgram(this.program); @@ -134,22 +156,7 @@ export class Renderer { gl.bindTexture(gl.TEXTURE_2D, this.atlasTexture); gl.uniform1i(this.uniforms.texture, 0); - gl.bindVertexArray(modelBuffer.vao); - - gl.disableVertexAttribArray(4); - gl.disableVertexAttribArray(5); - gl.disableVertexAttribArray(6); - gl.disableVertexAttribArray(7); - gl.disableVertexAttribArray(8); - - // Push the calculated model matrix into the instanced attributes manually - gl.vertexAttrib4f(4, modelMatrix[0], modelMatrix[1], modelMatrix[2], modelMatrix[3]); - gl.vertexAttrib4f(5, modelMatrix[4], modelMatrix[5], modelMatrix[6], modelMatrix[7]); - gl.vertexAttrib4f(6, modelMatrix[8], modelMatrix[9], modelMatrix[10], modelMatrix[11]); - gl.vertexAttrib4f(7, modelMatrix[12], modelMatrix[13], modelMatrix[14], modelMatrix[15]); - - gl.vertexAttrib3f(8, 1, 1, 1); // White tint - - gl.drawElements(gl.TRIANGLES, modelBuffer.indexCount, gl.UNSIGNED_SHORT, 0); + gl.bindVertexArray(mesh.vao); + gl.drawElementsInstanced(gl.TRIANGLES, mesh.indexCount, gl.UNSIGNED_SHORT, 0, mesh.instanceCount); } } \ No newline at end of file diff --git a/scene.js b/scene.js new file mode 100644 index 00000000..264aaa03 --- /dev/null +++ b/scene.js @@ -0,0 +1,48 @@ +// scene.js +import { resolveBlock } from './blockstate.js'; +import { mat4Identity, mat4Translate } from './math.js'; +// ... imports for loading assets ... + +export class SceneBuilder { + constructor(renderer, atlas) { + this.renderer = renderer; + this.atlas = atlas; + + // Cache parsed geometries by model ID so we only build them once + this.geometryCache = new Map(); + } + + async buildFromWorld(world) { + // 1. Group all instances by their resolved Model ID + const instancePool = new Map(); // "block/stone" -> [ matrix1, matrix2, ... ] + + for (const block of world.blocks.values()) { + const stateJSON = await loadBlockstate(block.id); + const parts = resolveBlock(stateJSON, block.props); + + for (const part of parts) { + if (!instancePool.has(part.model)) { + instancePool.set(part.model, { + parts: [], + modelId: part.model + }); + } + + // Calculate the world position matrix + const matrix = mat4Identity(new Float32Array(16)); + mat4Translate(matrix, matrix, [block.x, block.y, block.z]); + + instancePool.get(part.model).parts.push({ + matrix: matrix, + variant: part // Pass variant so geometry builder handles local rotation + }); + } + } + + // 2. We now have a list of models and every place they appear. + // The next step is to ensure geometry is built, and pack those matrices + // into Float32Arrays for the WebGL instanced buffers. + + return instancePool; + } +} \ No newline at end of file diff --git a/world.js b/world.js new file mode 100644 index 00000000..ba41709e --- /dev/null +++ b/world.js @@ -0,0 +1,23 @@ +// world.js +export class World { + constructor() { + this.blocks = new Map(); // Keyed by "x,y,z" + } + + setBlock(id, x, y, z, props = {}) { + const key = `${x},${y},${z}`; + this.blocks.set(key, { id, x, y, z, props }); + } + + updateBlock(x, y, z, newProps) { + const key = `${x},${y},${z}`; + const block = this.blocks.get(key); + if (block) { + block.props = { ...block.props, ...newProps }; + } + } + + getBlock(x, y, z) { + return this.blocks.get(`${x},${y},${z}`); + } +} \ No newline at end of file