feat: add highly experimental logic to try to self-restore any issues with chunks loading by automating f3+a action. write debug info into chat for now. can be disabled feat: rework chunks debug screen showing actually useful information now
427 lines
14 KiB
TypeScript
427 lines
14 KiB
TypeScript
import * as THREE from 'three'
|
|
|
|
export interface Create3DItemMeshOptions {
|
|
depth: number
|
|
pixelSize?: number
|
|
}
|
|
|
|
export interface Create3DItemMeshResult {
|
|
geometry: THREE.BufferGeometry
|
|
totalVertices: number
|
|
totalTriangles: number
|
|
}
|
|
|
|
/**
|
|
* Creates a 3D item geometry with front/back faces and connecting edges
|
|
* from a canvas containing the item texture
|
|
*/
|
|
export function create3DItemMesh (
|
|
canvas: HTMLCanvasElement,
|
|
options: Create3DItemMeshOptions
|
|
): Create3DItemMeshResult {
|
|
const { depth, pixelSize } = options
|
|
|
|
// Validate canvas dimensions
|
|
if (canvas.width <= 0 || canvas.height <= 0) {
|
|
throw new Error(`Invalid canvas dimensions: ${canvas.width}x${canvas.height}`)
|
|
}
|
|
|
|
const ctx = canvas.getContext('2d')!
|
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
|
|
const { data } = imageData
|
|
|
|
const w = canvas.width
|
|
const h = canvas.height
|
|
const halfDepth = depth / 2
|
|
const actualPixelSize = pixelSize ?? (1 / Math.max(w, h))
|
|
|
|
// Find opaque pixels
|
|
const isOpaque = (x: number, y: number) => {
|
|
if (x < 0 || y < 0 || x >= w || y >= h) return false
|
|
const i = (y * w + x) * 4
|
|
return data[i + 3] > 128 // alpha > 128
|
|
}
|
|
|
|
const vertices: number[] = []
|
|
const indices: number[] = []
|
|
const uvs: number[] = []
|
|
const normals: number[] = []
|
|
|
|
let vertexIndex = 0
|
|
|
|
// Helper to add a vertex
|
|
const addVertex = (x: number, y: number, z: number, u: number, v: number, nx: number, ny: number, nz: number) => {
|
|
vertices.push(x, y, z)
|
|
uvs.push(u, v)
|
|
normals.push(nx, ny, nz)
|
|
return vertexIndex++
|
|
}
|
|
|
|
// Helper to add a quad (two triangles)
|
|
const addQuad = (v0: number, v1: number, v2: number, v3: number) => {
|
|
indices.push(v0, v1, v2, v0, v2, v3)
|
|
}
|
|
|
|
// Convert pixel coordinates to world coordinates
|
|
const pixelToWorld = (px: number, py: number) => {
|
|
const x = (px / w - 0.5) * actualPixelSize * w
|
|
const y = -(py / h - 0.5) * actualPixelSize * h
|
|
return { x, y }
|
|
}
|
|
|
|
// Create a grid of vertices for front and back faces
|
|
const frontVertices: Array<Array<number | null>> = Array.from({ length: h + 1 }, () => Array.from({ length: w + 1 }, () => null))
|
|
const backVertices: Array<Array<number | null>> = Array.from({ length: h + 1 }, () => Array.from({ length: w + 1 }, () => null))
|
|
|
|
// Create vertices at pixel corners
|
|
for (let py = 0; py <= h; py++) {
|
|
for (let px = 0; px <= w; px++) {
|
|
const { x, y } = pixelToWorld(px - 0.5, py - 0.5)
|
|
|
|
// UV coordinates should map to the texture space of the extracted tile
|
|
const u = px / w
|
|
const v = py / h
|
|
|
|
// Check if this vertex is needed for any face or edge
|
|
let needVertex = false
|
|
|
|
// Check all 4 adjacent pixels to see if any are opaque
|
|
const adjacentPixels = [
|
|
[px - 1, py - 1], // top-left pixel
|
|
[px, py - 1], // top-right pixel
|
|
[px - 1, py], // bottom-left pixel
|
|
[px, py] // bottom-right pixel
|
|
]
|
|
|
|
for (const [adjX, adjY] of adjacentPixels) {
|
|
if (isOpaque(adjX, adjY)) {
|
|
needVertex = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if (needVertex) {
|
|
frontVertices[py][px] = addVertex(x, y, halfDepth, u, v, 0, 0, 1)
|
|
backVertices[py][px] = addVertex(x, y, -halfDepth, u, v, 0, 0, -1)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create front and back faces
|
|
for (let py = 0; py < h; py++) {
|
|
for (let px = 0; px < w; px++) {
|
|
if (!isOpaque(px, py)) continue
|
|
|
|
const v00 = frontVertices[py][px]
|
|
const v10 = frontVertices[py][px + 1]
|
|
const v11 = frontVertices[py + 1][px + 1]
|
|
const v01 = frontVertices[py + 1][px]
|
|
|
|
const b00 = backVertices[py][px]
|
|
const b10 = backVertices[py][px + 1]
|
|
const b11 = backVertices[py + 1][px + 1]
|
|
const b01 = backVertices[py + 1][px]
|
|
|
|
if (v00 !== null && v10 !== null && v11 !== null && v01 !== null) {
|
|
// Front face
|
|
addQuad(v00, v10, v11, v01)
|
|
}
|
|
|
|
if (b00 !== null && b10 !== null && b11 !== null && b01 !== null) {
|
|
// Back face (reversed winding)
|
|
addQuad(b10, b00, b01, b11)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create edge faces for each side of the pixel with proper UVs
|
|
for (let py = 0; py < h; py++) {
|
|
for (let px = 0; px < w; px++) {
|
|
if (!isOpaque(px, py)) continue
|
|
|
|
const pixelU = (px + 0.5) / w // Center of current pixel
|
|
const pixelV = (py + 0.5) / h
|
|
|
|
// Left edge (x = px)
|
|
if (!isOpaque(px - 1, py)) {
|
|
const f0 = frontVertices[py][px]
|
|
const f1 = frontVertices[py + 1][px]
|
|
const b0 = backVertices[py][px]
|
|
const b1 = backVertices[py + 1][px]
|
|
|
|
if (f0 !== null && f1 !== null && b0 !== null && b1 !== null) {
|
|
// Create new vertices for edge with current pixel's UV
|
|
const ef0 = addVertex(vertices[f0 * 3], vertices[f0 * 3 + 1], vertices[f0 * 3 + 2], pixelU, pixelV, -1, 0, 0)
|
|
const ef1 = addVertex(vertices[f1 * 3], vertices[f1 * 3 + 1], vertices[f1 * 3 + 2], pixelU, pixelV, -1, 0, 0)
|
|
const eb1 = addVertex(vertices[b1 * 3], vertices[b1 * 3 + 1], vertices[b1 * 3 + 2], pixelU, pixelV, -1, 0, 0)
|
|
const eb0 = addVertex(vertices[b0 * 3], vertices[b0 * 3 + 1], vertices[b0 * 3 + 2], pixelU, pixelV, -1, 0, 0)
|
|
addQuad(ef0, ef1, eb1, eb0)
|
|
}
|
|
}
|
|
|
|
// Right edge (x = px + 1)
|
|
if (!isOpaque(px + 1, py)) {
|
|
const f0 = frontVertices[py + 1][px + 1]
|
|
const f1 = frontVertices[py][px + 1]
|
|
const b0 = backVertices[py + 1][px + 1]
|
|
const b1 = backVertices[py][px + 1]
|
|
|
|
if (f0 !== null && f1 !== null && b0 !== null && b1 !== null) {
|
|
const ef0 = addVertex(vertices[f0 * 3], vertices[f0 * 3 + 1], vertices[f0 * 3 + 2], pixelU, pixelV, 1, 0, 0)
|
|
const ef1 = addVertex(vertices[f1 * 3], vertices[f1 * 3 + 1], vertices[f1 * 3 + 2], pixelU, pixelV, 1, 0, 0)
|
|
const eb1 = addVertex(vertices[b1 * 3], vertices[b1 * 3 + 1], vertices[b1 * 3 + 2], pixelU, pixelV, 1, 0, 0)
|
|
const eb0 = addVertex(vertices[b0 * 3], vertices[b0 * 3 + 1], vertices[b0 * 3 + 2], pixelU, pixelV, 1, 0, 0)
|
|
addQuad(ef0, ef1, eb1, eb0)
|
|
}
|
|
}
|
|
|
|
// Top edge (y = py)
|
|
if (!isOpaque(px, py - 1)) {
|
|
const f0 = frontVertices[py][px]
|
|
const f1 = frontVertices[py][px + 1]
|
|
const b0 = backVertices[py][px]
|
|
const b1 = backVertices[py][px + 1]
|
|
|
|
if (f0 !== null && f1 !== null && b0 !== null && b1 !== null) {
|
|
const ef0 = addVertex(vertices[f0 * 3], vertices[f0 * 3 + 1], vertices[f0 * 3 + 2], pixelU, pixelV, 0, -1, 0)
|
|
const ef1 = addVertex(vertices[f1 * 3], vertices[f1 * 3 + 1], vertices[f1 * 3 + 2], pixelU, pixelV, 0, -1, 0)
|
|
const eb1 = addVertex(vertices[b1 * 3], vertices[b1 * 3 + 1], vertices[b1 * 3 + 2], pixelU, pixelV, 0, -1, 0)
|
|
const eb0 = addVertex(vertices[b0 * 3], vertices[b0 * 3 + 1], vertices[b0 * 3 + 2], pixelU, pixelV, 0, -1, 0)
|
|
addQuad(ef0, ef1, eb1, eb0)
|
|
}
|
|
}
|
|
|
|
// Bottom edge (y = py + 1)
|
|
if (!isOpaque(px, py + 1)) {
|
|
const f0 = frontVertices[py + 1][px + 1]
|
|
const f1 = frontVertices[py + 1][px]
|
|
const b0 = backVertices[py + 1][px + 1]
|
|
const b1 = backVertices[py + 1][px]
|
|
|
|
if (f0 !== null && f1 !== null && b0 !== null && b1 !== null) {
|
|
const ef0 = addVertex(vertices[f0 * 3], vertices[f0 * 3 + 1], vertices[f0 * 3 + 2], pixelU, pixelV, 0, 1, 0)
|
|
const ef1 = addVertex(vertices[f1 * 3], vertices[f1 * 3 + 1], vertices[f1 * 3 + 2], pixelU, pixelV, 0, 1, 0)
|
|
const eb1 = addVertex(vertices[b1 * 3], vertices[b1 * 3 + 1], vertices[b1 * 3 + 2], pixelU, pixelV, 0, 1, 0)
|
|
const eb0 = addVertex(vertices[b0 * 3], vertices[b0 * 3 + 1], vertices[b0 * 3 + 2], pixelU, pixelV, 0, 1, 0)
|
|
addQuad(ef0, ef1, eb1, eb0)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const geometry = new THREE.BufferGeometry()
|
|
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3))
|
|
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2))
|
|
geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3))
|
|
geometry.setIndex(indices)
|
|
|
|
// Compute normals properly
|
|
geometry.computeVertexNormals()
|
|
|
|
return {
|
|
geometry,
|
|
totalVertices: vertexIndex,
|
|
totalTriangles: indices.length / 3
|
|
}
|
|
}
|
|
|
|
export interface ItemTextureInfo {
|
|
u: number
|
|
v: number
|
|
sizeX: number
|
|
sizeY: number
|
|
}
|
|
|
|
export interface ItemMeshResult {
|
|
mesh: THREE.Object3D
|
|
itemsTexture?: THREE.Texture
|
|
itemsTextureFlipped?: THREE.Texture
|
|
cleanup?: () => void
|
|
}
|
|
|
|
/**
|
|
* Extracts item texture region to a canvas
|
|
*/
|
|
export function extractItemTextureToCanvas (
|
|
sourceTexture: THREE.Texture,
|
|
textureInfo: ItemTextureInfo
|
|
): HTMLCanvasElement {
|
|
const { u, v, sizeX, sizeY } = textureInfo
|
|
|
|
// Calculate canvas size - fix the calculation
|
|
const canvasWidth = Math.max(1, Math.floor(sizeX * sourceTexture.image.width))
|
|
const canvasHeight = Math.max(1, Math.floor(sizeY * sourceTexture.image.height))
|
|
|
|
const canvas = document.createElement('canvas')
|
|
canvas.width = canvasWidth
|
|
canvas.height = canvasHeight
|
|
|
|
const ctx = canvas.getContext('2d')!
|
|
ctx.imageSmoothingEnabled = false
|
|
|
|
// Draw the item texture region to canvas
|
|
ctx.drawImage(
|
|
sourceTexture.image,
|
|
u * sourceTexture.image.width,
|
|
v * sourceTexture.image.height,
|
|
sizeX * sourceTexture.image.width,
|
|
sizeY * sourceTexture.image.height,
|
|
0,
|
|
0,
|
|
canvas.width,
|
|
canvas.height
|
|
)
|
|
|
|
return canvas
|
|
}
|
|
|
|
/**
|
|
* Creates either a 2D or 3D item mesh based on parameters
|
|
*/
|
|
export function createItemMesh (
|
|
sourceTexture: THREE.Texture,
|
|
textureInfo: ItemTextureInfo,
|
|
options: {
|
|
faceCamera?: boolean
|
|
use3D?: boolean
|
|
depth?: number
|
|
} = {}
|
|
): ItemMeshResult {
|
|
const { faceCamera = false, use3D = true, depth = 0.04 } = options
|
|
const { u, v, sizeX, sizeY } = textureInfo
|
|
|
|
if (faceCamera) {
|
|
// Create sprite for camera-facing items
|
|
const itemsTexture = sourceTexture.clone()
|
|
itemsTexture.flipY = true
|
|
itemsTexture.offset.set(u, 1 - v - sizeY)
|
|
itemsTexture.repeat.set(sizeX, sizeY)
|
|
itemsTexture.needsUpdate = true
|
|
itemsTexture.magFilter = THREE.NearestFilter
|
|
itemsTexture.minFilter = THREE.NearestFilter
|
|
|
|
const spriteMat = new THREE.SpriteMaterial({
|
|
map: itemsTexture,
|
|
transparent: true,
|
|
alphaTest: 0.1,
|
|
})
|
|
const mesh = new THREE.Sprite(spriteMat)
|
|
|
|
return {
|
|
mesh,
|
|
itemsTexture,
|
|
cleanup () {
|
|
itemsTexture.dispose()
|
|
}
|
|
}
|
|
}
|
|
|
|
if (use3D) {
|
|
// Try to create 3D mesh
|
|
try {
|
|
const canvas = extractItemTextureToCanvas(sourceTexture, textureInfo)
|
|
const { geometry } = create3DItemMesh(canvas, { depth })
|
|
|
|
// Create texture from canvas for the 3D mesh
|
|
const itemsTexture = new THREE.CanvasTexture(canvas)
|
|
itemsTexture.magFilter = THREE.NearestFilter
|
|
itemsTexture.minFilter = THREE.NearestFilter
|
|
itemsTexture.wrapS = itemsTexture.wrapT = THREE.ClampToEdgeWrapping
|
|
itemsTexture.flipY = false
|
|
itemsTexture.needsUpdate = true
|
|
|
|
const material = new THREE.MeshStandardMaterial({
|
|
map: itemsTexture,
|
|
side: THREE.DoubleSide,
|
|
transparent: true,
|
|
alphaTest: 0.1,
|
|
})
|
|
|
|
const mesh = new THREE.Mesh(geometry, material)
|
|
|
|
return {
|
|
mesh,
|
|
itemsTexture,
|
|
cleanup () {
|
|
itemsTexture.dispose()
|
|
geometry.dispose()
|
|
if (material.map) material.map.dispose()
|
|
material.dispose()
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn('Failed to create 3D item mesh, falling back to 2D:', error)
|
|
// Fall through to 2D rendering
|
|
}
|
|
}
|
|
|
|
// Fallback to 2D flat rendering
|
|
const itemsTexture = sourceTexture.clone()
|
|
itemsTexture.flipY = true
|
|
itemsTexture.offset.set(u, 1 - v - sizeY)
|
|
itemsTexture.repeat.set(sizeX, sizeY)
|
|
itemsTexture.needsUpdate = true
|
|
itemsTexture.magFilter = THREE.NearestFilter
|
|
itemsTexture.minFilter = THREE.NearestFilter
|
|
|
|
const itemsTextureFlipped = itemsTexture.clone()
|
|
itemsTextureFlipped.repeat.x *= -1
|
|
itemsTextureFlipped.needsUpdate = true
|
|
itemsTextureFlipped.offset.set(u + sizeX, 1 - v - sizeY)
|
|
|
|
const material = new THREE.MeshStandardMaterial({
|
|
map: itemsTexture,
|
|
transparent: true,
|
|
alphaTest: 0.1,
|
|
})
|
|
const materialFlipped = new THREE.MeshStandardMaterial({
|
|
map: itemsTextureFlipped,
|
|
transparent: true,
|
|
alphaTest: 0.1,
|
|
})
|
|
const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 0), [
|
|
new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), new THREE.MeshBasicMaterial({ color: 0x00_00_00 }),
|
|
new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), new THREE.MeshBasicMaterial({ color: 0x00_00_00 }),
|
|
material, materialFlipped,
|
|
])
|
|
|
|
return {
|
|
mesh,
|
|
itemsTexture,
|
|
itemsTextureFlipped,
|
|
cleanup () {
|
|
itemsTexture.dispose()
|
|
itemsTextureFlipped.dispose()
|
|
material.dispose()
|
|
materialFlipped.dispose()
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a complete 3D item mesh from a canvas texture
|
|
*/
|
|
export function createItemMeshFromCanvas (
|
|
canvas: HTMLCanvasElement,
|
|
options: Create3DItemMeshOptions
|
|
): THREE.Mesh {
|
|
const { geometry } = create3DItemMesh(canvas, options)
|
|
|
|
// Base color texture for the item
|
|
const colorTexture = new THREE.CanvasTexture(canvas)
|
|
colorTexture.magFilter = THREE.NearestFilter
|
|
colorTexture.minFilter = THREE.NearestFilter
|
|
colorTexture.wrapS = colorTexture.wrapT = THREE.ClampToEdgeWrapping
|
|
colorTexture.flipY = false // Important for canvas textures
|
|
colorTexture.needsUpdate = true
|
|
|
|
// Material - no transparency, no alpha test needed for edges
|
|
const material = new THREE.MeshBasicMaterial({
|
|
map: colorTexture,
|
|
side: THREE.DoubleSide,
|
|
transparent: true,
|
|
alphaTest: 0.1
|
|
})
|
|
|
|
return new THREE.Mesh(geometry, material)
|
|
}
|