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)
Changes
@@@ -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 = falseswitch (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 = trunkcanopy.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 = rootstump.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@@@ -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 / 4roof.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 = buildingdoor.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 = buildingwin.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}/**
@@ -1,3 +1,4 @@
@@ -1,3 +1,4 @@
+import '@babylonjs/core/Meshes/instancedMesh.js' // createInstance() needs its side effect (tree instances)
import '@babylonjs/core/Rendering/outlineRenderer.js'
import '@babylonjs/core/Rendering/outlineRenderer.js'
import { Color3 } from '@babylonjs/core/Maths/math.color.js'
import { Color3 } from '@babylonjs/core/Maths/math.color.js'
import { Quaternion, Vector3 } from '@babylonjs/core/Maths/math.vector.js'
import { Quaternion, Vector3 } from '@babylonjs/core/Maths/math.vector.js'
@@ -573,19 +574,19 @@ function buildNodeVisual(
@@ -573,19 +574,19 @@ function buildNodeVisual(
root.isVisible = false
root.isVisible = false
switch (visual) {
switch (visual) {
case 'tree': {
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.position.y = 1.6
- trunk.material = materialFor(scene, '#6d4c2a')
trunk.parent = root
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.parent = trunk
canopy.position.y = 1.9
canopy.position.y = 1.9
// Chopped-down state: the standing tree is replaced by a stump (the
// Chopped-down state: the standing tree is replaced by a stump (the
// trunk itself falls as a physical prop spawned by the server).
// 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.position.y = 0.22
- stump.material = materialFor(scene, '#5c3f24')
stump.parent = root
stump.parent = root
stump.setEnabled(false)
stump.setEnabled(false)
break
break
@@ -626,6 +627,26 @@ function buildNodeVisual(
@@ -626,6 +627,26 @@ function buildNodeVisual(
return root
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. */
/** Deterministic pseudo-random yaw so identical piles don't look cloned. */
function hashAngle(id: string): number {
function hashAngle(id: string): number {
let h = 0
let h = 0
@@ -23,6 +23,8 @@ import {
@@ -23,6 +23,8 @@ import {
buildPatchGrid,
buildPatchGrid,
buildTerrainGrid,
buildTerrainGrid,
getMapOverride,
getMapOverride,
+ terrainHeight,
+ WATER_LEVEL,
type ContentRegistry,
type ContentRegistry,
type MapTextureEntry,
type MapTextureEntry,
type WorldShape,
type WorldShape,
@@ -301,10 +303,28 @@ export function buildStaticWorld(scene: Scene, content: ContentRegistry): void {
@@ -301,10 +303,28 @@ export function buildStaticWorld(scene: Scene, content: ContentRegistry): void {
const groundMeshes = buildTerrainMesh(scene, content)
const groundMeshes = buildTerrainMesh(scene, content)
for (const m of groundMeshes) water.addToRenderList(m)
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)
for (const [i, s] of world.statics.entries()) renderStaticBody(scene, water, s, `static:${i}`, i)
buildMapStaticVisuals(scene, water)
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.
* 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
* They used to be pushed into `content.world.statics` at boot, which merged
@@ -376,10 +396,13 @@ function renderStaticBody(
@@ -376,10 +396,13 @@ function renderStaticBody(
mesh.rotationQuaternion = s.rot
mesh.rotationQuaternion = s.rot
? Quaternion.FromEulerAngles(s.rot[0], s.rot[1], s.rot[2])
? Quaternion.FromEulerAngles(s.rot[0], s.rot[1], s.rot[2])
: Quaternion.FromEulerAngles(0, s.yaw, 0)
: 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)
if (s.decor === 'building') decorateBuilding(scene, mesh, s)
else if (s.decor === 'fountain' && water) buildFountain(scene, mesh, s, water)
else if (s.decor === 'fountain' && water) buildFountain(scene, mesh, s, water)
else if (s.decor === 'lamp') decorateLamp(scene, mesh, s, decorSeed ?? 0)
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 {
@@ -398,42 +421,74 @@ function decorateBuilding(scene: Scene, building: Mesh, s: StaticBody): void {
roof.rotation.y = Math.PI / 4
roof.rotation.y = Math.PI / 4
roof.scaling.set(w + 0.8, 1.5, d + 0.8)
roof.scaling.set(w + 0.8, 1.5, d + 0.8)
roof.position.y = h / 2 + 0.75
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]) {
for (const side of [1, -1]) {
const door = CreateBox(
const door = CreateBox(
`${building.name}:door${side}`,
`${building.name}:door${side}`,
{ width: 1, height: 1.9, depth: 0.1 },
{ width: 1, height: 1.9, depth: 0.1 },
scene,
scene,
)
)
- door.parent = building
door.position.set(0, -h / 2 + 0.95, side * (d / 2 + 0.03))
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]) {
for (const wx of [-1, 1]) {
const win = CreateBox(
const win = CreateBox(
`${building.name}:win${side}${wx}`,
`${building.name}:win${side}${wx}`,
{ width: 0.8, height: 0.8, depth: 0.08 },
{ width: 0.8, height: 0.8, depth: 0.08 },
scene,
scene,
)
)
- win.parent = building
win.position.set((wx * w) / 3.4, 0.45, side * (d / 2 + 0.03))
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
}
}
/**
/**