perf: faster still

This commit is contained in:
Dan Finch 2026-08-04 19:12:09 +02:00
parent ef5029da1d
commit 67cd54fe33
8 changed files with 280 additions and 203 deletions

View file

@ -1,5 +1,5 @@
import type { Vec3 } from "../math/Vec3"
import type { Mesh } from "./Mesh"
import { STRIDE, type Mesh } from "./Mesh"
const TAU = Math.PI * 2
@ -37,7 +37,7 @@ export namespace Boulder {
const cy = boulder.position.y + sy * 0.55
const jitter = jitterGrid(seg, rings, rand)
const start = mesh.vertices.length
const start = mesh.verts.length / STRIDE
for (let ir = 0; ir <= rings; ir++) {
const phi = (ir / rings) * Math.PI
const cyv = Math.cos(phi)
@ -45,14 +45,13 @@ export namespace Boulder {
for (let is = 0; is <= seg; is++) {
const theta = (is / seg) * TAU
const j = jitter[ir][is]
mesh.vertices.push({
pos: {
x: cx + crv * Math.cos(theta) * sx * j,
y: cy + cyv * sy * j,
z: cz + crv * Math.sin(theta) * sz * j,
},
uv: { x: (is / seg) * 1.5, y: (ir / rings) * 1.5 },
})
mesh.verts.push(
cx + crv * Math.cos(theta) * sx * j,
cy + cyv * sy * j,
cz + crv * Math.sin(theta) * sz * j,
(is / seg) * 1.5,
(ir / rings) * 1.5,
)
}
}
const row = seg + 1

View file

@ -1,11 +1,26 @@
import type { Vec2 } from "../math/Vec2"
import type { Vec3 } from "../math/Vec3"
/** Floats per vertex in `Mesh.verts`: position x, y, z then texture u, v. */
export const STRIDE = 5
/** One mesh vertex: a world-space position and its texture coordinate. uv is in
* tile units, not 0..1, so values >1 repeat the texture (see Texture.sample). */
export type Vertex = { pos: Vec3; uv: Vec2 }
/**
* Indexed triangle mesh, stored flat for speed. `verts` is a packed run of
* `STRIDE` floats per vertex (x, y, z, u, v) instead of an array of nested
* `{pos, uv}` objects, so the transform loop reads contiguous numbers with no
* pointer chasing or per-vertex allocation. `indices` holds three vertex indices
* per triangle (an index `i` addresses `verts[i * STRIDE ..]`); sharing vertices
* keeps seams welded and shrinks the data. uv is in tile units, not 0..1, so
* values >1 repeat the texture (see Texture.sample). Build with `Mesh.push`.
*/
export type Mesh = { verts: number[]; indices: number[] }
/** Indexed triangle mesh in world space. `indices` holds three entries per
* triangle, each indexing into `vertices`; sharing vertices between triangles
* keeps seams welded and shrinks the data. */
export type Mesh = { vertices: Vertex[]; indices: number[] }
export namespace Mesh {
export function create(): Mesh {
return { verts: [], indices: [] }
}
/** Append a vertex, returning its index (for wiring up `indices`). */
export function push(mesh: Mesh, x: number, y: number, z: number, u: number, v: number): number {
const index = mesh.verts.length / STRIDE
mesh.verts.push(x, y, z, u, v)
return index
}
}

View file

@ -34,12 +34,9 @@ export namespace Sprite {
const lz = p.z - rz * hw
const gx = p.x + rx * hw
const gz = p.z + rz * hw
const vertices = [
{ pos: { x: lx, y: y0, z: lz }, uv: { x: 0, y: 1 } },
{ pos: { x: gx, y: y0, z: gz }, uv: { x: 1, y: 1 } },
{ pos: { x: gx, y: y1, z: gz }, uv: { x: 1, y: 0 } },
{ pos: { x: lx, y: y1, z: lz }, uv: { x: 0, y: 0 } },
]
return { vertices, indices: [0, 1, 2, 0, 2, 3] }
// Flat verts (x, y, z, u, v) per corner: bottom-left, bottom-right, top-right,
// top-left.
const verts = [lx, y0, lz, 0, 1, gx, y0, gz, 1, 1, gx, y1, gz, 1, 0, lx, y1, lz, 0, 0]
return { verts, indices: [0, 1, 2, 0, 2, 3] }
}
}

View file

@ -1,4 +1,4 @@
import type { Mesh } from "./Mesh"
import { STRIDE, type Mesh } from "./Mesh"
/** A procedural heightfield surrounding the room. It is the single source of
* ground height: the outdoor mesh is built from it and the player stands on the
@ -59,15 +59,15 @@ export namespace Terrain {
rows: number,
uvScale: number,
): void {
const base = mesh.vertices.length
const base = mesh.verts.length / STRIDE
const dx = (x1 - x0) / cols
const dz = (z1 - z0) / rows
const stride = cols + 1
const rowLen = cols + 1
for (let i = 0; i <= rows; i++) {
const z = z0 + i * dz
for (let j = 0; j <= cols; j++) {
const x = x0 + j * dx
mesh.vertices.push({ pos: { x, y: height(t, x, z), z }, uv: { x: x * uvScale, y: z * uvScale } })
mesh.verts.push(x, height(t, x, z), z, x * uvScale, z * uvScale)
}
}
for (let i = 0; i < rows; i++) {
@ -77,9 +77,9 @@ export namespace Terrain {
if (Math.max(Math.abs(cx), Math.abs(cz)) < t.inner) {
continue
}
const p = base + i * stride + j
const p = base + i * rowLen + j
// Wound so the surface faces up/out, matching the backface-cull sign.
mesh.indices.push(p, p + stride + 1, p + 1, p, p + stride, p + stride + 1)
mesh.indices.push(p, p + rowLen + 1, p + 1, p, p + rowLen, p + rowLen + 1)
}
}
}

View file

@ -1,5 +1,5 @@
import { Vec3 } from "../math/Vec3"
import type { Mesh } from "./Mesh"
import { STRIDE, type Mesh } from "./Mesh"
const TAU = Math.PI * 2
@ -95,13 +95,15 @@ export namespace Tree {
const axis = Vec3.normalize(Vec3.sub(b, a))
const [u, v] = basis(axis)
const len = Vec3.length(Vec3.sub(b, a))
const start = mesh.vertices.length
const start = mesh.verts.length / STRIDE
for (let i = 0; i <= sides; i++) {
const angle = (i / sides) * TAU
const dir = Vec3.add(Vec3.scale(u, Math.cos(angle)), Vec3.scale(v, Math.sin(angle)))
const dx = u.x * Math.cos(angle) + v.x * Math.sin(angle)
const dy = u.y * Math.cos(angle) + v.y * Math.sin(angle)
const dz = u.z * Math.cos(angle) + v.z * Math.sin(angle)
const s = i / sides
mesh.vertices.push({ pos: Vec3.add(a, Vec3.scale(dir, ra)), uv: { x: s * 1.5, y: 0 } })
mesh.vertices.push({ pos: Vec3.add(b, Vec3.scale(dir, rb)), uv: { x: s * 1.5, y: len * 0.5 } })
mesh.verts.push(a.x + dx * ra, a.y + dy * ra, a.z + dz * ra, s * 1.5, 0)
mesh.verts.push(b.x + dx * rb, b.y + dy * rb, b.z + dz * rb, s * 1.5, len * 0.5)
}
for (let i = 0; i < sides; i++) {
const p = start + i * 2
@ -111,17 +113,15 @@ export namespace Tree {
/** A cone standing on a base ring, apex `height` above it (one spruce tier). */
function cone(mesh: Mesh, base: Vec3, height: number, radius: number, sides: number): void {
const start = mesh.vertices.length
mesh.vertices.push({ pos: { x: base.x, y: base.y + height, z: base.z }, uv: { x: 0.5, y: 0 } })
const start = mesh.verts.length / STRIDE
mesh.verts.push(base.x, base.y + height, base.z, 0.5, 0)
for (let i = 0; i <= sides; i++) {
const angle = (i / sides) * TAU
mesh.vertices.push({
pos: { x: base.x + Math.cos(angle) * radius, y: base.y, z: base.z + Math.sin(angle) * radius },
uv: { x: (i / sides) * 2, y: 1 },
})
mesh.verts.push(base.x + Math.cos(angle) * radius, base.y, base.z + Math.sin(angle) * radius, (i / sides) * 2, 1)
}
for (let i = 0; i < sides; i++) {
mesh.indices.push(start, start + 1 + i, start + 2 + i)
// Wound so the outer surface faces out, matching the backface-cull sign.
mesh.indices.push(start, start + 2 + i, start + 1 + i)
}
}
@ -130,7 +130,7 @@ export namespace Tree {
function blob(mesh: Mesh, center: Vec3, radius: number, rand: () => number): void {
const seg = 5
const rings = 3
const start = mesh.vertices.length
const start = mesh.verts.length / STRIDE
for (let r = 0; r <= rings; r++) {
const phi = (r / rings) * Math.PI
const cy = Math.cos(phi)
@ -138,14 +138,13 @@ export namespace Tree {
const scale = radius * (0.85 + rand() * 0.3)
for (let s = 0; s <= seg; s++) {
const theta = (s / seg) * TAU
mesh.vertices.push({
pos: {
x: center.x + cr * Math.cos(theta) * scale,
y: center.y + cy * scale,
z: center.z + cr * Math.sin(theta) * scale,
},
uv: { x: (s / seg) * 2, y: (r / rings) * 2 },
})
mesh.verts.push(
center.x + cr * Math.cos(theta) * scale,
center.y + cy * scale,
center.z + cr * Math.sin(theta) * scale,
(s / seg) * 2,
(r / rings) * 2,
)
}
}
const row = seg + 1