openvibers/openvibe.games Commits
Scraplandia has sun shadows: a shadow box around the player (buildings, trees, props and people cast; the ground and statics receive), on the quality tiers with shadows (#111)
Changes
@@@ -1,6 +1,9 @@import type { AbstractEngine } from '@babylonjs/core/Engines/abstractEngine.js'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 { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh.js'import '@babylonjs/core/LensFlares/lensFlareSystemSceneComponent.js'// LensFlareSystem's occlusion test uses Scene.pick, which requires the Ray// side-effect module in tree-shaken builds.@@@ -42,6 +45,9 @@ const FOGMODE_EXP2 = 2export class Environment {readonly sun: DirectionalLightprivate readonly hemi: HemisphericLight++ /** Sun shadows in a box around the player (quality tiers with shadows; scene.shadowsEnabled gates them). */++ private readonly shadows: ShadowGenerator++ private readonly shadowCenter = new Vector3()private pipeline: DefaultRenderingPipeline | null = nullprivate readonly flareEmitter: TransformNodeprivate readonly flares: LensFlareSystem@@@ -67,6 +73,25 @@ export class Environment {this.sun.renderPriority = 10this.hemi.renderPriority = 10++ // The sun casts shadows within SHADOW_REACH of the player: the world had no shadows at all, so buildings, trees++ // and people floated on flat ground. The shadow box follows the camera (update()), snapped to a grid so the++ // edges do not crawl while walking.++ this.sun.autoUpdateExtends = false++ this.sun.orthoLeft = -SHADOW_REACH++ this.sun.orthoRight = SHADOW_REACH++ this.sun.orthoTop = SHADOW_REACH++ this.sun.orthoBottom = -SHADOW_REACH++ this.sun.shadowMinZ = 1++ this.sun.shadowMaxZ = 420++ this.shadows = new ShadowGenerator(2048, this.sun)++ this.shadows.usePercentageCloserFiltering = true++ this.shadows.filteringQuality = ShadowGenerator.QUALITY_MEDIUM++ this.shadows.bias = 0.0006++ this.shadows.normalBias = 0.02++ this.shadows.setDarkness(0.35)++ const map = this.shadows.getShadowMap()++ if (map) map.renderListPredicate = (m) => castsShadow(m, this.shadowCenter)++// ── Sky dome (analytic daylight model; renders the actual sun disc) ─this.skyMat = new SkyMaterial('sky', scene)this.skyMat.backFaceCulling = false@@@ -196,6 +221,12 @@ export class Environment {const dir = new Vector3(azimuth * 0.62, -Math.max(elevation, -0.6), 0.45)dir.normalize()this.sun.direction = dir++ this.shadowCenter.set(++ Math.round(cameraPos.x / 4) * 4,++ Math.round(cameraPos.y / 4) * 4,++ Math.round(cameraPos.z / 4) * 4,++ )++ this.sun.position.copyFrom(this.shadowCenter).subtractInPlace(dir.scale(200))// Sky dome follows the sun; turbidity rises toward dusk for warm haze.const sunPos = dir.scale(-800)@@@ -259,6 +290,21 @@ export class Environment {}}++/** Half the side of the box around the player in which the sun casts shadows, metres. */++const SHADOW_REACH = 60++/** 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 {++ if (!CASTER.test(mesh.name)) return false++ const sphere = mesh.getBoundingInfo().boundingSphere++ // Small things (pebbles, branches) add nothing a player notices; past the box's edge nothing lands inside it.++ if (sphere.radiusWorld < 0.5) return false++ return (++ Vector3.DistanceSquared(sphere.centerWorld, center) <=++ (SHADOW_REACH * 1.15 + sphere.radiusWorld) ** 2++ )++}++/** Tiny procedural radial-gradient flare sprite (no asset needed). */function flareTexture(): string {const size = 128@@@ -411,7 +411,10 @@ function renderStaticBody(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()++ for (const m of [mesh, ...mesh.getChildMeshes()]) {++ m.freezeWorldMatrix()++ m.receiveShadows = true++ }}/**
@@ -1,6 +1,9 @@
@@ -1,6 +1,9 @@
import type { AbstractEngine } from '@babylonjs/core/Engines/abstractEngine.js'
import type { AbstractEngine } from '@babylonjs/core/Engines/abstractEngine.js'
import { DirectionalLight } from '@babylonjs/core/Lights/directionalLight.js'
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 { ShadowGenerator } from '@babylonjs/core/Lights/Shadows/shadowGenerator.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
// side-effect module in tree-shaken builds.
// side-effect module in tree-shaken builds.
@@ -42,6 +45,9 @@ const FOGMODE_EXP2 = 2
@@ -42,6 +45,9 @@ const FOGMODE_EXP2 = 2
export class Environment {
export class Environment {
readonly sun: DirectionalLight
readonly sun: DirectionalLight
private readonly hemi: HemisphericLight
private readonly hemi: HemisphericLight
+ /** Sun shadows in a box around the player (quality tiers with shadows; scene.shadowsEnabled gates them). */
+ private readonly shadows: ShadowGenerator
+ private readonly shadowCenter = new Vector3()
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
@@ -67,6 +73,25 @@ export class Environment {
@@ -67,6 +73,25 @@ export class Environment {
this.sun.renderPriority = 10
this.sun.renderPriority = 10
this.hemi.renderPriority = 10
this.hemi.renderPriority = 10
+ // The sun casts shadows within SHADOW_REACH of the player: the world had no shadows at all, so buildings, trees
+ // and people floated on flat ground. The shadow box follows the camera (update()), snapped to a grid so the
+ // edges do not crawl while walking.
+ this.sun.autoUpdateExtends = false
+ this.sun.orthoLeft = -SHADOW_REACH
+ this.sun.orthoRight = SHADOW_REACH
+ this.sun.orthoTop = SHADOW_REACH
+ this.sun.orthoBottom = -SHADOW_REACH
+ this.sun.shadowMinZ = 1
+ this.sun.shadowMaxZ = 420
+ this.shadows = new ShadowGenerator(2048, this.sun)
+ this.shadows.usePercentageCloserFiltering = true
+ this.shadows.filteringQuality = ShadowGenerator.QUALITY_MEDIUM
+ this.shadows.bias = 0.0006
+ this.shadows.normalBias = 0.02
+ this.shadows.setDarkness(0.35)
+ const map = this.shadows.getShadowMap()
+ if (map) map.renderListPredicate = (m) => castsShadow(m, this.shadowCenter)
+
// ── 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)
this.skyMat.backFaceCulling = false
this.skyMat.backFaceCulling = false
@@ -196,6 +221,12 @@ export class Environment {
@@ -196,6 +221,12 @@ export class Environment {
const dir = new Vector3(azimuth * 0.62, -Math.max(elevation, -0.6), 0.45)
const dir = new Vector3(azimuth * 0.62, -Math.max(elevation, -0.6), 0.45)
dir.normalize()
dir.normalize()
this.sun.direction = dir
this.sun.direction = dir
+ this.shadowCenter.set(
+ Math.round(cameraPos.x / 4) * 4,
+ Math.round(cameraPos.y / 4) * 4,
+ Math.round(cameraPos.z / 4) * 4,
+ )
+ this.sun.position.copyFrom(this.shadowCenter).subtractInPlace(dir.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.
const sunPos = dir.scale(-800)
const sunPos = dir.scale(-800)
@@ -259,6 +290,21 @@ export class Environment {
@@ -259,6 +290,21 @@ export class Environment {
}
}
}
}
+/** Half the side of the box around the player in which the sun casts shadows, metres. */
+const SHADOW_REACH = 60
+/** 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 {
+ if (!CASTER.test(mesh.name)) return false
+ const sphere = mesh.getBoundingInfo().boundingSphere
+ // Small things (pebbles, branches) add nothing a player notices; past the box's edge nothing lands inside it.
+ if (sphere.radiusWorld < 0.5) return false
+ return (
+ Vector3.DistanceSquared(sphere.centerWorld, center) <=
+ (SHADOW_REACH * 1.15 + sphere.radiusWorld) ** 2
+ )
+}
+
/** Tiny procedural radial-gradient flare sprite (no asset needed). */
/** Tiny procedural radial-gradient flare sprite (no asset needed). */
function flareTexture(): string {
function flareTexture(): string {
const size = 128
const size = 128
@@ -411,7 +411,10 @@ function renderStaticBody(
@@ -411,7 +411,10 @@ function renderStaticBody(
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',
// 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.
// doors' and windows', are computed once instead of every frame.
- for (const m of [mesh, ...mesh.getChildMeshes()]) m.freezeWorldMatrix()
+ for (const m of [mesh, ...mesh.getChildMeshes()]) {
+ m.freezeWorldMatrix()
+ m.receiveShadows = true
+ }
}
}
/**
/**