openvibers/openvibe.games Commits

Scraplandia renders a quarter of the draw calls (1,115 a frame became ~270, ~20 fps became 37-50): the water re-renders only what is near water, buildings share their materials and merge their doors and windows, trees are instances, and statics freeze their world matrices (#108)

OP OpenVibers committed 25 days ago · 5f60d8d4fc · 2 files, +102 −26

Changes

ddiff --git a/apps/client/src/render/entityView.ts b/apps/client/src/render/entityView.ts
iindex ac176ee..4603bd7 100644
---- a/apps/client/src/render/entityView.ts
++++ b/apps/client/src/render/entityView.ts
@@@ -1,3 +1,4 @@
++import '@babylonjs/core/Meshes/instancedMesh.js' // createInstance() needs its side effect (tree instances)
import '@babylonjs/core/Rendering/outlineRenderer.js'
import { Color3 } from '@babylonjs/core/Maths/math.color.js'
import { Quaternion, Vector3 } from '@babylonjs/core/Maths/math.vector.js'
@@@ -573,19 +574,19 @@ function buildNodeVisual(
root.isVisible = false
switch (visual) {
case 'tree': {
-- const trunk = CreateCylinder(`res:${id}`, { diameter: 0.6, height: 3.2 }, scene)
++ // Instances of one trunk, canopy and stump per scene: the island's ~200 trees draw in one call per part,
++ // where every tree used to be its own two draw calls.
++ const parts = treeParts(scene)
++ const trunk = parts.trunk.createInstance(`res:${id}`)
trunk.position.y = 1.6
-- trunk.material = materialFor(scene, '#6d4c2a')
trunk.parent = root
-- const canopy = CreateSphere(`res:${id}:canopy`, { diameter: 2.8, segments: 8 }, scene)
-- canopy.material = materialFor(scene, '#3e6b34')
++ const canopy = parts.canopy.createInstance(`res:${id}:canopy`)
canopy.parent = trunk
canopy.position.y = 1.9
// Chopped-down state: the standing tree is replaced by a stump (the
// trunk itself falls as a physical prop spawned by the server).
-- const stump = CreateCylinder(`res:${id}:stump`, { diameter: 0.7, height: 0.45 }, scene)
++ const stump = parts.stump.createInstance(`res:${id}:stump`)
stump.position.y = 0.22
-- stump.material = materialFor(scene, '#5c3f24')
stump.parent = root
stump.setEnabled(false)
break
@@@ -626,6 +627,26 @@ function buildNodeVisual(
return root
}
++/** The source meshes every tree instances (hidden themselves; one set per scene). */
++const treePartSets = new WeakMap<Scene, { trunk: Mesh; canopy: Mesh; stump: Mesh }>()
++function treeParts(scene: Scene): { trunk: Mesh; canopy: Mesh; stump: Mesh } {
++ let set = treePartSets.get(scene)
++ if (set) return set
++ const trunk = CreateCylinder('tree:trunk', { diameter: 0.6, height: 3.2 }, scene)
++ trunk.material = materialFor(scene, '#6d4c2a')
++ const canopy = CreateSphere('tree:canopy', { diameter: 2.8, segments: 8 }, scene)
++ canopy.material = materialFor(scene, '#3e6b34')
++ const stump = CreateCylinder('tree:stump', { diameter: 0.7, height: 0.45 }, scene)
++ stump.material = materialFor(scene, '#5c3f24')
++ for (const m of [trunk, canopy, stump]) {
++ m.isVisible = false // a source draws nothing itself; its instances do
++ m.isPickable = false
++ }
++ set = { trunk, canopy, stump }
++ treePartSets.set(scene, set)
++ return set
++}
++
/** Deterministic pseudo-random yaw so identical piles don't look cloned. */
function hashAngle(id: string): number {
let h = 0
ddiff --git a/apps/client/src/render/sceneSetup.ts b/apps/client/src/render/sceneSetup.ts
iindex e1a152e..ebfd586 100644
---- a/apps/client/src/render/sceneSetup.ts
++++ b/apps/client/src/render/sceneSetup.ts
@@@ -23,6 +23,8 @@ import {
buildPatchGrid,
buildTerrainGrid,
getMapOverride,
++ terrainHeight,
++ WATER_LEVEL,
type ContentRegistry,
type MapTextureEntry,
type WorldShape,
@@@ -301,10 +303,28 @@ export function buildStaticWorld(scene: Scene, content: ContentRegistry): void {
const groundMeshes = buildTerrainMesh(scene, content)
for (const m of groundMeshes) water.addToRenderList(m)
++ // The water re-renders its render list twice a frame (reflection and refraction), so only geometry near open
++ // water belongs on it: on Scrap Island every one of 422 statics, a whole town inland, was drawn three times a frame.
++ nearWater = (x, z) =>
++ NEAR_WATER_PROBES.some(([dx, dz]) => terrainHeight(world, x + dx, z + dz) < WATER_LEVEL + 0.5)
for (const [i, s] of world.statics.entries()) renderStaticBody(scene, water, s, `static:${i}`, i)
buildMapStaticVisuals(scene, water)
}
++/** Ground samples around a static: water within ~12 m of it means it can show in the water. */
++const NEAR_WATER_PROBES: [number, number][] = [
++ [0, 0],
++ [12, 0],
++ [-12, 0],
++ [0, 12],
++ [0, -12],
++ [8, 8],
++ [-8, -8],
++ [8, -8],
++ [-8, 8],
++]
++let nearWater: (x: number, z: number) => boolean = () => true
++
/**
* The MAP's statics, rendered under their stable ids in their own layer.
* They used to be pushed into `content.world.statics` at boot, which merged
@@@ -376,10 +396,13 @@ function renderStaticBody(
mesh.rotationQuaternion = s.rot
? Quaternion.FromEulerAngles(s.rot[0], s.rot[1], s.rot[2])
: Quaternion.FromEulerAngles(0, s.yaw, 0)
-- water?.addToRenderList(mesh)
++ if (water && nearWater(s.pos[0], s.pos[2])) water.addToRenderList(mesh)
if (s.decor === 'building') decorateBuilding(scene, mesh, s)
else if (s.decor === 'fountain' && water) buildFountain(scene, mesh, s, water)
else if (s.decor === 'lamp') decorateLamp(scene, mesh, s, decorSeed ?? 0)
++ // Statics never move (a live map save disposes and rebuilds them): their world matrices, and their roofs',
++ // doors' and windows', are computed once instead of every frame.
++ for (const m of [mesh, ...mesh.getChildMeshes()]) m.freezeWorldMatrix()
}
/**
@@@ -398,42 +421,74 @@ function decorateBuilding(scene: Scene, building: Mesh, s: StaticBody): void {
roof.rotation.y = Math.PI / 4
roof.scaling.set(w + 0.8, 1.5, d + 0.8)
roof.position.y = h / 2 + 0.75
-- const roofMat = new StandardMaterial(`${building.name}:roofmat`, scene)
-- const roofTex = loadTexture(builtinTexSource('clay_roof_tiles'), scene)
-- roofTex.uScale = 3
-- roofTex.vScale = 2
-- roofMat.diffuseTexture = roofTex
-- roofMat.specularColor = new Color3(0.03, 0.03, 0.03)
-- roof.material = roofMat
--
-- const doorMat = new StandardMaterial(`${building.name}:door`, scene)
-- doorMat.diffuseColor = Color3.FromHexString('#3a2a1c')
-- doorMat.specularColor = Color3.Black()
-- const winMat = new StandardMaterial(`${building.name}:win`, scene)
-- winMat.diffuseColor = Color3.FromHexString('#2a3540')
-- winMat.emissiveColor = new Color3(0.45, 0.34, 0.14) // warm glow at night
-- winMat.specularColor = Color3.Black()
++ const mats = buildingMaterials(scene)
++ roof.material = mats.roof
++ // Doors and windows: built in the building's local space, merged into one mesh each and parented to it. Every
++ // building used to add 7 meshes and 3 materials of its own; a town of 50 was ~500 extra draw calls a frame.
++ const doors: Mesh[] = []
++ const windows: Mesh[] = []
for (const side of [1, -1]) {
const door = CreateBox(
`${building.name}:door${side}`,
{ width: 1, height: 1.9, depth: 0.1 },
scene,
)
-- door.parent = building
door.position.set(0, -h / 2 + 0.95, side * (d / 2 + 0.03))
-- door.material = doorMat
++ doors.push(door)
for (const wx of [-1, 1]) {
const win = CreateBox(
`${building.name}:win${side}${wx}`,
{ width: 0.8, height: 0.8, depth: 0.08 },
scene,
)
-- win.parent = building
win.position.set((wx * w) / 3.4, 0.45, side * (d / 2 + 0.03))
-- win.material = winMat
++ windows.push(win)
}
}
++ for (const [parts, mat, name] of [
++ [doors, mats.door, 'doors'],
++ [windows, mats.window, 'windows'],
++ ] as const) {
++ const merged = Mesh.MergeMeshes(parts, true)
++ if (!merged) continue
++ merged.name = `${building.name}:${name}`
++ merged.material = mat
++ merged.parent = building
++ }
++}
++
++/** The roof, door and window materials every building shares (one set per scene). */
++const buildingMaterialSets = new WeakMap<
++ Scene,
++ { roof: StandardMaterial; door: StandardMaterial; window: StandardMaterial }
++>()
++function buildingMaterials(scene: Scene): {
++ roof: StandardMaterial
++ door: StandardMaterial
++ window: StandardMaterial
++} {
++ let set = buildingMaterialSets.get(scene)
++ if (set) return set
++ const roof = new StandardMaterial('building:roof', scene)
++ const roofTex = loadTexture(builtinTexSource('clay_roof_tiles'), scene)
++ roofTex.uScale = 3
++ roofTex.vScale = 2
++ roof.diffuseTexture = roofTex
++ roof.specularColor = new Color3(0.03, 0.03, 0.03)
++ roof.maxSimultaneousLights = 8
++ const door = new StandardMaterial('building:door', scene)
++ door.diffuseColor = Color3.FromHexString('#3a2a1c')
++ door.specularColor = Color3.Black()
++ door.maxSimultaneousLights = 8
++ const window = new StandardMaterial('building:window', scene)
++ window.diffuseColor = Color3.FromHexString('#2a3540')
++ window.emissiveColor = new Color3(0.45, 0.34, 0.14) // warm glow at night
++ window.specularColor = Color3.Black()
++ window.maxSimultaneousLights = 8
++ set = { roof, door, window }
++ buildingMaterialSets.set(scene, set)
++ return set
}
/**