diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index af6d9b50..ee07c9ef 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -156,6 +156,7 @@ export default defineConfig({
{ text: 'Sky', link: '/guide/staging/sky' },
{ text: 'Stars', link: '/guide/staging/stars' },
{ text: 'Smoke', link: '/guide/staging/smoke' },
+ { text: 'AccumulativeShadows', link: '/guide/staging/accumulative-shadows' },
{ text: 'ContactShadows', link: '/guide/staging/contact-shadows' },
{ text: 'Precipitation', link: '/guide/staging/precipitation' },
{ text: 'Sparkles', link: '/guide/staging/sparkles' },
@@ -163,6 +164,7 @@ export default defineConfig({
{ text: 'Align', link: '/guide/staging/align' },
{ text: 'SoftShadows', link: '/guide/staging/soft-shadows' },
{ text: 'Grid', link: '/guide/staging/grid' },
+ { text: 'RandomizedLights', link: '/guide/staging/randomized-lights' },
],
},
{
diff --git a/docs/.vitepress/theme/components/AccumulativeShadowsDemo.vue b/docs/.vitepress/theme/components/AccumulativeShadowsDemo.vue
new file mode 100644
index 00000000..1cbc6d22
--- /dev/null
+++ b/docs/.vitepress/theme/components/AccumulativeShadowsDemo.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/component-list/components.ts b/docs/component-list/components.ts
index 02f72ca9..8f158c18 100644
--- a/docs/component-list/components.ts
+++ b/docs/component-list/components.ts
@@ -105,6 +105,7 @@ export default [
{ text: 'Sky', link: '/guide/staging/sky' },
{ text: 'Stars', link: '/guide/staging/stars' },
{ text: 'Smoke', link: '/guide/staging/smoke' },
+ { text: 'AccumulativeShadows', link: '/guide/staging/accumulative-shadows' },
{ text: 'ContactShadows', link: '/guide/staging/contact-shadows' },
{ text: 'Precipitation', link: '/guide/staging/precipitation' },
{ text: 'Sparkles', link: '/guide/staging/sparkles' },
@@ -113,6 +114,7 @@ export default [
{ text: 'Align', link: '/guide/staging/align' },
{ text: 'SoftShadows', link: '/guide/staging/soft-shadows' },
{ text: 'Grid', link: '/guide/staging/grid' },
+ { text: 'RandomizedLights', link: '/guide/staging/randomized-lights' },
],
},
{
diff --git a/docs/guide/staging/accumulative-shadows.md b/docs/guide/staging/accumulative-shadows.md
new file mode 100644
index 00000000..f481f42c
--- /dev/null
+++ b/docs/guide/staging/accumulative-shadows.md
@@ -0,0 +1,34 @@
+# AccumulativeShadows
+
+
+
+
+
+`` is a `THREE.DirectionalLight`-based shadow component. It displays shadows on a single shadow catcher plane, included in the component. It is based on [Drei component of the same name](http://drei.docs.pmnd.rs/staging/accumulative-shadows).
+
+## Usage
+
+<<< @/.vitepress/theme/components/AccumulativeShadowsDemo.vue
+
+## Props
+
+| Prop | Description | Default |
+| - | - | - |
+| `once` | Whether shadow creation only happens once (resets after props change) | `false` |
+| `accumulate` | Whether shadows accumulate progressively over several frames | `true` |
+| `frames` | Number of frames to render. More yields cleaner results but takes more time. If `accumulate && once`, 1 frame will be consumed every update for `frames` updates. Otherwise, `frames` frames are consumed for every update. | `40` |
+| `blend` | If `accumulate`, controls the refresh ratio | `100` |
+| `limit` | If less than `Infinity`, limits the amount of frames rendered. Use this to increase performance once a movable scene has settled | `Infinity` |
+| `scale` | Scale of the plane | `10` |
+| `opacity` | Opacity of the plane | `1` |
+| `alphaTest` | Discards alpha pixels | `0.65` |
+| `color` | Shadow color | `'black'` |
+| `colorBlend` | If less than `Infinity`, limits the amount of frames rendered. Use this to increase performance once a movable scene has settled | `Infinity` |
+| `resolution` | Buffer resolution | `1024` |
+| `toneMapped` | Texture tonemapping | `true` |
+
+## Slot
+
+You can bring your own lights to ``, but it's designed to be used with ``.
+
+By default, there's a `` instance provided in ``'s ``. You can replace it with your own `` or an alternative by passing it as a child component.
diff --git a/docs/guide/staging/randomized-lights.md b/docs/guide/staging/randomized-lights.md
new file mode 100644
index 00000000..7cf5fe43
--- /dev/null
+++ b/docs/guide/staging/randomized-lights.md
@@ -0,0 +1,35 @@
+# RandomizedLights
+
+`` internally creates multiple lights and jiggles them. You would normally add it as a child of ``.
+
+It is based on this [Drei component](http://drei.docs.pmnd.rs/staging/randomized-light).
+
+## Usage
+
+```vue
+
+```
+
+## Props
+
+| Prop | Description | Default |
+| - | - | - |
+| `count` | Number of lights | `8`|
+| `radius` | Radius of the jiggle, higher values make softer light | `1` |
+| `intensity` | Light intensity | `Math.PI` |
+| `ambient` | "Ambient occlusion" to directional light ratio, lower values mean less AO | `0.5` |
+| `castShadow` | If the lights cast shadows | `true` |
+| `bias` | Default shadow bias | `0` |
+| `mapSize` | Size of the lights' shadow map | `512` |
+| `size` | Size of the lights' shadow camera frustum | `10` |
+| `near` | Lights' shadow camera near value | `0.5` |
+| `far` | Lights' shadow camera far value | `500` |
+| `position` | Position | `[5, 5, -10]` |
diff --git a/playground/vue/src/pages/staging/AccumulativeShadowsDemo.vue b/playground/vue/src/pages/staging/AccumulativeShadowsDemo.vue
new file mode 100644
index 00000000..c2a5ee48
--- /dev/null
+++ b/playground/vue/src/pages/staging/AccumulativeShadowsDemo.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/playground/vue/src/router/routes/staging.ts b/playground/vue/src/router/routes/staging.ts
index de21d597..7efd3145 100644
--- a/playground/vue/src/router/routes/staging.ts
+++ b/playground/vue/src/router/routes/staging.ts
@@ -64,4 +64,9 @@ export const stagingRoutes = [
name: 'Grid',
component: () => import('../../pages/staging/GridDemo.vue'),
},
+ {
+ path: '/staging/accumulative-shadows',
+ name: 'Accumulative Shadows',
+ component: () => import('../../pages/staging/AccumulativeShadowsDemo.vue'),
+ },
]
diff --git a/src/core/staging/AccumulativeShadows/ProgressiveLightMap.ts b/src/core/staging/AccumulativeShadows/ProgressiveLightMap.ts
new file mode 100644
index 00000000..6f380bb6
--- /dev/null
+++ b/src/core/staging/AccumulativeShadows/ProgressiveLightMap.ts
@@ -0,0 +1,141 @@
+import type { Camera, Group, Light, Material, Mesh, Scene, ShaderMaterial, Texture, WebGLRenderer } from 'three'
+import { Color, HalfFloatType, MeshLambertMaterial, NearestFilter, WebGLRenderTarget } from 'three'
+import { MeshDiscardMaterial as DiscardMaterial } from '../../materials/meshDiscardMaterial/material'
+
+function isLight(object: any): object is Light {
+ return object.isLight
+}
+
+function isGeometry(object: any): object is Mesh {
+ return !!object.geometry
+}
+
+// NOTE: Based on "Progressive Light Map Accumulator", by [zalo](https://github.com/zalo/)
+export class ProgressiveLightMap {
+ renderer: WebGLRenderer
+ res: number
+ scene: Scene
+ object: Mesh | null
+ lightsGroup: Group | null = null
+ buffer1Active: boolean
+ progressiveLightMap1: WebGLRenderTarget
+ progressiveLightMap2: WebGLRenderTarget
+ discardMat: ShaderMaterial
+ targetMat: MeshLambertMaterial
+ previousShadowMap: { value: Texture }
+ averagingWindow: { value: number }
+ clearColor: Color
+ clearAlpha: number
+ lights: { object: Light, intensity: number }[]
+ meshes: { object: Mesh, material: Material | Material[] }[]
+
+ constructor(renderer: WebGLRenderer, scene: Scene, res = 1024) {
+ this.renderer = renderer
+ this.res = res
+ this.scene = scene
+ this.buffer1Active = false
+ this.lights = []
+ this.meshes = []
+ this.object = null
+ this.clearColor = new Color()
+ this.clearAlpha = 0
+
+ // NOTE: Create the Progressive LightMap Texture
+ const textureParams = {
+ type: HalfFloatType,
+ magFilter: NearestFilter,
+ minFilter: NearestFilter,
+ }
+ this.progressiveLightMap1 = new WebGLRenderTarget(this.res, this.res, textureParams)
+ this.progressiveLightMap2 = new WebGLRenderTarget(this.res, this.res, textureParams)
+
+ // NOTE: Inject some spicy new logic into a standard phong material
+ this.discardMat = new DiscardMaterial()
+ this.targetMat = new MeshLambertMaterial({ fog: false })
+ this.previousShadowMap = { value: this.progressiveLightMap1.texture }
+ this.averagingWindow = { value: 100 }
+ this.targetMat.onBeforeCompile = (shader) => {
+ // NOTE: Vertex Shader: Set Vertex Positions to the Unwrapped UV Positions
+ shader.vertexShader
+ = `varying vec2 vUv;
+ ${shader.vertexShader.slice(0, -1)}
+ vUv = uv;
+ gl_Position = vec4((uv - 0.5) * 2.0, 1.0, 1.0); }`
+
+ // NOTE: Fragment Shader: Set Pixels to average in the Previous frame's Shadows
+ const bodyStart = shader.fragmentShader.indexOf('void main() {')
+ shader.fragmentShader
+ = `
+ varying vec2 vUv;
+ ${shader.fragmentShader.slice(0, bodyStart)}
+ uniform sampler2D previousShadowMap;
+ uniform float averagingWindow;
+ ${shader.fragmentShader.slice(bodyStart - 1, -1)}
+ vec3 texelOld = texture2D(previousShadowMap, vUv).rgb;
+ gl_FragColor.rgb = mix(texelOld, gl_FragColor.rgb, 1.0 / averagingWindow);
+ }`
+
+ // NOTE: Set the Previous Frame's Texture Buffer and Averaging Window
+ shader.uniforms.previousShadowMap = this.previousShadowMap
+ shader.uniforms.averagingWindow = this.averagingWindow
+ }
+ }
+
+ clear() {
+ this.renderer.getClearColor(this.clearColor)
+ this.clearAlpha = this.renderer.getClearAlpha()
+ this.renderer.setClearColor('black', 1)
+ this.renderer.setRenderTarget(this.progressiveLightMap1)
+ this.renderer.clear()
+ this.renderer.setRenderTarget(this.progressiveLightMap2)
+ this.renderer.clear()
+ this.renderer.setRenderTarget(null)
+ this.renderer.setClearColor(this.clearColor, this.clearAlpha)
+
+ this.lights = []
+ this.meshes = []
+ this.scene.traverse((object) => {
+ if (object === this.lightsGroup) { return false }
+ if (isGeometry(object)) {
+ this.meshes.push({ object, material: object.material })
+ }
+ else if (isLight(object)) {
+ this.lights.push({ object, intensity: object.intensity })
+ }
+ })
+ }
+
+ prepare() {
+ this.lights.forEach(light => (light.object.intensity = 0))
+ this.meshes.forEach(mesh => (mesh.object.material = this.discardMat))
+ }
+
+ finish() {
+ this.lights.forEach(light => (light.object.intensity = light.intensity))
+ this.meshes.forEach(mesh => (mesh.object.material = mesh.material))
+ }
+
+ configure(object: Mesh, lightsGroup: Group) {
+ this.object = object
+ this.lightsGroup = lightsGroup
+ }
+
+ update(camera: Camera, blendWindow = 100) {
+ if (!this.object) { return }
+ // NOTE: Set each object's material to the UV Unwrapped Surface Mapping Version
+ this.averagingWindow.value = blendWindow
+ this.object.material = this.targetMat
+ // NOTE: Ping-pong two surface buffers for reading/writing
+ const activeMap = this.buffer1Active ? this.progressiveLightMap1 : this.progressiveLightMap2
+ const inactiveMap = this.buffer1Active ? this.progressiveLightMap2 : this.progressiveLightMap1
+ // NOTE: Render the object's surface maps
+ const oldBg = this.scene.background
+ this.scene.background = null
+ this.renderer.setRenderTarget(activeMap)
+ this.previousShadowMap.value = inactiveMap.texture
+ this.buffer1Active = !this.buffer1Active
+ this.renderer.render(this.scene, camera)
+ this.renderer.setRenderTarget(null)
+ this.scene.background = oldBg
+ }
+}
diff --git a/src/core/staging/AccumulativeShadows/SoftShadowMaterial.ts b/src/core/staging/AccumulativeShadows/SoftShadowMaterial.ts
new file mode 100644
index 00000000..de769adb
--- /dev/null
+++ b/src/core/staging/AccumulativeShadows/SoftShadowMaterial.ts
@@ -0,0 +1,37 @@
+import { shaderMaterial } from '../../../utils/shaderMaterial'
+import type { ColorRepresentation, Texture } from 'three'
+import { Color } from 'three'
+
+export interface SoftShadowMaterialProps {
+ map: Texture
+ color?: ColorRepresentation
+ alphaTest?: number
+ blend?: number
+}
+
+export const SoftShadowMaterial = /* @__PURE__ */ shaderMaterial(
+ {
+ color: new Color(),
+ blend: 2.0,
+ alphaTest: 0.75,
+ opacity: 0,
+ map: null,
+ },
+ `varying vec2 vUv;
+ void main() {
+ gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.);
+ vUv = uv;
+ }`,
+ `varying vec2 vUv;
+ uniform sampler2D map;
+ uniform vec3 color;
+ uniform float opacity;
+ uniform float alphaTest;
+ uniform float blend;
+ void main() {
+ vec4 sampledDiffuseColor = texture2D(map, vUv);
+ gl_FragColor = vec4(color * sampledDiffuseColor.r * blend, max(0.0, (1.0 - (sampledDiffuseColor.r + sampledDiffuseColor.g + sampledDiffuseColor.b) / alphaTest)) * opacity);
+ #include
+ #include
+ }`,
+)
diff --git a/src/core/staging/AccumulativeShadows/component.vue b/src/core/staging/AccumulativeShadows/component.vue
new file mode 100644
index 00000000..66691aba
--- /dev/null
+++ b/src/core/staging/AccumulativeShadows/component.vue
@@ -0,0 +1,193 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/core/staging/RandomizedLights/RandomizedLights.ts b/src/core/staging/RandomizedLights/RandomizedLights.ts
new file mode 100644
index 00000000..a1cf42f6
--- /dev/null
+++ b/src/core/staging/RandomizedLights/RandomizedLights.ts
@@ -0,0 +1,131 @@
+import { DirectionalLight, Group, MathUtils, Vector3 } from 'three'
+
+export default class RandomizedLights extends Group {
+ /** Light position */
+ position: Vector3 = new Vector3(0, 0, 0)
+ /** Radius of the jiggle, higher values make softer light */
+ radius = 1
+ /** Light intensity */
+ intensity = Math.PI
+ /** Ambient occlusion, lower values mean less AO, hight more, you can mix AO and directional light */
+ ambient = 0.5
+ /** If the lights cast shadows */
+ castShadow = true
+ /** Default shadow bias */
+ bias = 0
+
+ constructor(config: Partial = {}) {
+ super()
+ Object.assign(this, config)
+ if (this.count === 0) { this.count = 8 }
+ if (!config.mapSize) {
+ this.mapSize = 512
+ }
+ if (!config.size) {
+ this.size = 10
+ }
+ if (!config.near) {
+ this.near = 0.5
+ }
+ if (!config.far) {
+ this.far = 500
+ }
+ }
+
+ get length() {
+ return this.position.length()
+ }
+
+ set count(n: number) {
+ this.clear()
+ for (let i = 0; i < n; i++) {
+ this.add(new DirectionalLight('white', this.intensity))
+ }
+ }
+
+ get count() {
+ return this.children.filter(c => 'isDirectionalLight' in c).length
+ }
+
+ get mapSize() {
+ return this.lights[0].shadow.mapSize.width
+ }
+
+ set mapSize(n: number) {
+ for (const light of this.lights) {
+ // NOTE: Changing the map size requires 2 modifications.
+ // https://discourse.threejs.org/t/change-resolution-of-shadows-dinamically/50744/6
+ light.shadow.mapSize.set(n, n)
+ light.shadow.map?.setSize(n, n)
+ }
+ }
+
+ get size() {
+ return this.lights[0].shadow.camera.right
+ }
+
+ set size(n: number) {
+ for (const light of this.lights) {
+ light.shadow.camera.left = -n
+ light.shadow.camera.right = n
+ light.shadow.camera.top = n
+ light.shadow.camera.bottom = -n
+ }
+ }
+
+ get near() {
+ return this.lights[0].shadow.camera.near
+ }
+
+ set near(n: number) {
+ for (const light of this.lights) {
+ light.shadow.camera.near = this.near
+ }
+ }
+
+ get far() {
+ return this.lights[0].shadow.camera.far
+ }
+
+ set far(n: number) {
+ for (const light of this.lights) {
+ light.shadow.camera.far = this.far
+ }
+ }
+
+ get lights(): DirectionalLight[] {
+ return this.children.filter(c => 'isDirectionalLight' in c) as DirectionalLight[]
+ }
+
+ update() {
+ const lights = this.lights
+ const lightIntensity = this.intensity / lights.length
+ let ambientCount = Math.floor(this.ambient * lights.length)
+
+ for (const light of lights) {
+ light.castShadow = this.castShadow
+ light.shadow.bias = this.bias
+
+ light.intensity = lightIntensity
+
+ if (ambientCount-- > 0) {
+ const lambda = Math.acos(2 * Math.random() - 1) - Math.PI / 2.0
+ const phi = 2 * Math.PI * Math.random()
+ light.position.set(
+ Math.cos(lambda) * Math.cos(phi) * this.length,
+ Math.abs(Math.cos(lambda) * Math.sin(phi) * this.length),
+ Math.sin(lambda) * this.length,
+ )
+ }
+ else {
+ if (Math.random() > this.ambient) {
+ light.position.set(
+ this.position.x + MathUtils.randFloatSpread(this.radius),
+ this.position.y + MathUtils.randFloatSpread(this.radius),
+ this.position.z + MathUtils.randFloatSpread(this.radius),
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/src/core/staging/RandomizedLights/component.vue b/src/core/staging/RandomizedLights/component.vue
new file mode 100644
index 00000000..bd2936e7
--- /dev/null
+++ b/src/core/staging/RandomizedLights/component.vue
@@ -0,0 +1,70 @@
+
+
+
+
+
diff --git a/src/core/staging/index.ts b/src/core/staging/index.ts
index fc6ea028..f1828ab9 100644
--- a/src/core/staging/index.ts
+++ b/src/core/staging/index.ts
@@ -1,3 +1,4 @@
+import AccumulativeShadows from './AccumulativeShadows/component.vue'
import Align from './Align.vue'
import Backdrop from './Backdrop.vue'
import ContactShadows from './ContactShadows.vue'
@@ -5,6 +6,7 @@ import Fit from './Fit.vue'
import Grid from './Grid.vue'
import Ocean from './Ocean.vue'
import Precipitation from './Precipitation.vue'
+import RandomizedLights from './RandomizedLights/component.vue'
import Sky from './Sky.vue'
import Smoke from './Smoke.vue'
import SoftShadows from './SoftShadows.vue'
@@ -14,6 +16,7 @@ import Environment from './useEnvironment/component.vue'
import Lightformer from './useEnvironment/lightformer/index.vue'
export {
+ AccumulativeShadows,
Align,
Backdrop,
ContactShadows,
@@ -23,6 +26,7 @@ export {
Lightformer,
Ocean,
Precipitation,
+ RandomizedLights,
Sky,
Smoke,
SoftShadows,