openvibers/openvibe.games Commits
Scraplandia: the shadow map is redrawn every third frame (and at once when its box moves) in place of every frame, which cost about 390 draw calls in Scrap City and halved the frame rate (#118)
Changes
@@@ -8,6 +8,10 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).### Fixed++- **Shadows halved the frame rate in town.** The shadow pass drew every caster in Scrap City again each frame (about++ 390 more draw calls, ~720 a frame), and a test browser dropped from ~50 to ~10 fps. The shadow map is now redrawn++ every third frame, and at once whenever the shadow box moves, so no shadow jumps. That gives ~490 draw calls and++ about twice the frame rate in the same browser.- **Lamps blacked out the world.** A material uses its first 8 lights, and the town's lamp lights (off by day) werecreated before the sun and sky. A map with a dozen lamps rendered every surface black. The sun and sky now have thehigher render priority. Each lamp lights only the geometry in its range, and its light goes with its post, so a live@@@ -3,6 +3,7 @@ import { DirectionalLight } from '@babylonjs/core/Lights/directionalLight.js'import { HemisphericLight } from '@babylonjs/core/Lights/hemisphericLight.js'import '@babylonjs/core/Lights/Shadows/shadowGeneratorSceneComponent.js'import { ShadowGenerator } from '@babylonjs/core/Lights/Shadows/shadowGenerator.js'++import type { RenderTargetTexture } from '@babylonjs/core/Materials/Textures/renderTargetTexture.js'import type { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh.js'import '@babylonjs/core/LensFlares/lensFlareSystemSceneComponent.js'// LensFlareSystem's occlusion test uses Scene.pick, which requires the Ray@@@ -48,6 +49,7 @@ export class Environment {/** Sun shadows in a box around the player (quality tiers with shadows; scene.shadowsEnabled gates them). */private readonly shadows: ShadowGeneratorprivate readonly shadowCenter = new Vector3()++ private shadowMap: RenderTargetTexture | null = nullprivate pipeline: DefaultRenderingPipeline | null = nullprivate readonly flareEmitter: TransformNodeprivate readonly flares: LensFlareSystem@@@ -92,7 +94,14 @@ export class Environment {this.shadows.normalBias = 0.02this.shadows.setDarkness(0.35)const map = this.shadows.getShadowMap()-- if (map) map.renderListPredicate = (m) => castsShadow(m, this.shadowCenter)++ if (map) {++ map.renderListPredicate = (m) => castsShadow(m, this.shadowCenter)++ // The shadow pass draws every caster in the box again, about 390 draw calls in Scrap City, which took the++ // frame rate from ~50 to ~10 in a test browser. The sun moves slowly and the box only moves in 4 m steps (and++ // forces a fresh map when it does, in update()), so the map is redrawn every third frame.++ map.refreshRate = SHADOW_REFRESH_FRAMES++ }++ this.shadowMap = map// ── Sky dome (analytic daylight model; renders the actual sun disc) ─this.skyMat = new SkyMaterial('sky', scene)@@@ -226,11 +235,13 @@ export class Environment {// It used to keep the set sun's direction, pointing up out of the ground, so the ground got only sky light.const lightDir = elevation >= 0 ? dir : dir.negate()this.sun.direction = lightDir-- this.shadowCenter.set(-- Math.round(cameraPos.x / 4) * 4,-- Math.round(cameraPos.y / 4) * 4,-- Math.round(cameraPos.z / 4) * 4,-- )++ const cx = Math.round(cameraPos.x / 4) * 4++ const cy = Math.round(cameraPos.y / 4) * 4++ const cz = Math.round(cameraPos.z / 4) * 4++ // A moved box needs its map at once: a stale one would shift every shadow by 4 m for a frame or two.++ if (cx !== this.shadowCenter.x || cy !== this.shadowCenter.y || cz !== this.shadowCenter.z)++ this.shadowMap?.resetRefreshCounter()++ this.shadowCenter.set(cx, cy, cz)this.sun.position.copyFrom(this.shadowCenter).subtractInPlace(lightDir.scale(200))// Sky dome follows the sun; turbidity rises toward dusk for warm haze.@@@ -298,6 +309,8 @@ export class Environment {/** Half the side of the box around the player in which the sun casts shadows, metres. */const SHADOW_REACH = 60++/** The shadow map is redrawn once every this many frames (and at once when the box moves). */++const SHADOW_REFRESH_FRAMES = 3/** What casts a sun shadow: world geometry, trees and nodes, props and people, near the player. */const CASTER = /^(mapstatic|static|res|tree|prop|npc|player):/function castsShadow(mesh: AbstractMesh, center: Vector3): boolean {
@@ -8,6 +8,10 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
@@ -8,6 +8,10 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### Fixed
### Fixed
+- **Shadows halved the frame rate in town.** The shadow pass drew every caster in Scrap City again each frame (about
+ 390 more draw calls, ~720 a frame), and a test browser dropped from ~50 to ~10 fps. The shadow map is now redrawn
+ every third frame, and at once whenever the shadow box moves, so no shadow jumps. That gives ~490 draw calls and
+ about twice the frame rate in the same browser.
- **Lamps blacked out the world.** A material uses its first 8 lights, and the town's lamp lights (off by day) were
- **Lamps blacked out the world.** A material uses its first 8 lights, and the town's lamp lights (off by day) were
created before the sun and sky. A map with a dozen lamps rendered every surface black. The sun and sky now have the
created before the sun and sky. A map with a dozen lamps rendered every surface black. The sun and sky now have the
higher render priority. Each lamp lights only the geometry in its range, and its light goes with its post, so a live
higher render priority. Each lamp lights only the geometry in its range, and its light goes with its post, so a live
@@ -3,6 +3,7 @@ import { DirectionalLight } from '@babylonjs/core/Lights/directionalLight.js'
@@ -3,6 +3,7 @@ import { DirectionalLight } from '@babylonjs/core/Lights/directionalLight.js'
import { HemisphericLight } from '@babylonjs/core/Lights/hemisphericLight.js'
import { HemisphericLight } from '@babylonjs/core/Lights/hemisphericLight.js'
import '@babylonjs/core/Lights/Shadows/shadowGeneratorSceneComponent.js'
import '@babylonjs/core/Lights/Shadows/shadowGeneratorSceneComponent.js'
import { ShadowGenerator } from '@babylonjs/core/Lights/Shadows/shadowGenerator.js'
import { ShadowGenerator } from '@babylonjs/core/Lights/Shadows/shadowGenerator.js'
+import type { RenderTargetTexture } from '@babylonjs/core/Materials/Textures/renderTargetTexture.js'
import type { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh.js'
import type { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh.js'
import '@babylonjs/core/LensFlares/lensFlareSystemSceneComponent.js'
import '@babylonjs/core/LensFlares/lensFlareSystemSceneComponent.js'
// LensFlareSystem's occlusion test uses Scene.pick, which requires the Ray
// LensFlareSystem's occlusion test uses Scene.pick, which requires the Ray
@@ -48,6 +49,7 @@ export class Environment {
@@ -48,6 +49,7 @@ export class Environment {
/** Sun shadows in a box around the player (quality tiers with shadows; scene.shadowsEnabled gates them). */
/** Sun shadows in a box around the player (quality tiers with shadows; scene.shadowsEnabled gates them). */
private readonly shadows: ShadowGenerator
private readonly shadows: ShadowGenerator
private readonly shadowCenter = new Vector3()
private readonly shadowCenter = new Vector3()
+ private shadowMap: RenderTargetTexture | null = null
private pipeline: DefaultRenderingPipeline | null = null
private pipeline: DefaultRenderingPipeline | null = null
private readonly flareEmitter: TransformNode
private readonly flareEmitter: TransformNode
private readonly flares: LensFlareSystem
private readonly flares: LensFlareSystem
@@ -92,7 +94,14 @@ export class Environment {
@@ -92,7 +94,14 @@ export class Environment {
this.shadows.normalBias = 0.02
this.shadows.normalBias = 0.02
this.shadows.setDarkness(0.35)
this.shadows.setDarkness(0.35)
const map = this.shadows.getShadowMap()
const map = this.shadows.getShadowMap()
- if (map) map.renderListPredicate = (m) => castsShadow(m, this.shadowCenter)
+ if (map) {
+ map.renderListPredicate = (m) => castsShadow(m, this.shadowCenter)
+ // The shadow pass draws every caster in the box again, about 390 draw calls in Scrap City, which took the
+ // frame rate from ~50 to ~10 in a test browser. The sun moves slowly and the box only moves in 4 m steps (and
+ // forces a fresh map when it does, in update()), so the map is redrawn every third frame.
+ map.refreshRate = SHADOW_REFRESH_FRAMES
+ }
+ this.shadowMap = map
// ── Sky dome (analytic daylight model; renders the actual sun disc) ─
// ── Sky dome (analytic daylight model; renders the actual sun disc) ─
this.skyMat = new SkyMaterial('sky', scene)
this.skyMat = new SkyMaterial('sky', scene)
@@ -226,11 +235,13 @@ export class Environment {
@@ -226,11 +235,13 @@ export class Environment {
// It used to keep the set sun's direction, pointing up out of the ground, so the ground got only sky light.
// It used to keep the set sun's direction, pointing up out of the ground, so the ground got only sky light.
const lightDir = elevation >= 0 ? dir : dir.negate()
const lightDir = elevation >= 0 ? dir : dir.negate()
this.sun.direction = lightDir
this.sun.direction = lightDir
- this.shadowCenter.set(
- Math.round(cameraPos.x / 4) * 4,
- Math.round(cameraPos.y / 4) * 4,
- Math.round(cameraPos.z / 4) * 4,
- )
+ const cx = Math.round(cameraPos.x / 4) * 4
+ const cy = Math.round(cameraPos.y / 4) * 4
+ const cz = Math.round(cameraPos.z / 4) * 4
+ // A moved box needs its map at once: a stale one would shift every shadow by 4 m for a frame or two.
+ if (cx !== this.shadowCenter.x || cy !== this.shadowCenter.y || cz !== this.shadowCenter.z)
+ this.shadowMap?.resetRefreshCounter()
+ this.shadowCenter.set(cx, cy, cz)
this.sun.position.copyFrom(this.shadowCenter).subtractInPlace(lightDir.scale(200))
this.sun.position.copyFrom(this.shadowCenter).subtractInPlace(lightDir.scale(200))
// Sky dome follows the sun; turbidity rises toward dusk for warm haze.
// Sky dome follows the sun; turbidity rises toward dusk for warm haze.
@@ -298,6 +309,8 @@ export class Environment {
@@ -298,6 +309,8 @@ export class Environment {
/** Half the side of the box around the player in which the sun casts shadows, metres. */
/** Half the side of the box around the player in which the sun casts shadows, metres. */
const SHADOW_REACH = 60
const SHADOW_REACH = 60
+/** The shadow map is redrawn once every this many frames (and at once when the box moves). */
+const SHADOW_REFRESH_FRAMES = 3
/** What casts a sun shadow: world geometry, trees and nodes, props and people, near the player. */
/** What casts a sun shadow: world geometry, trees and nodes, props and people, near the player. */
const CASTER = /^(mapstatic|static|res|tree|prop|npc|player):/
const CASTER = /^(mapstatic|static|res|tree|prop|npc|player):/
function castsShadow(mesh: AbstractMesh, center: Vector3): boolean {
function castsShadow(mesh: AbstractMesh, center: Vector3): boolean {