From 2fb2962de37a3f292aa0ab000d588169196b68c9 Mon Sep 17 00:00:00 2001 From: Peter Date: Sun, 8 Dec 2024 09:10:23 +0100 Subject: [PATCH 1/4] feat(AccumulativeShadows): add component, demo, docs --- docs/.vitepress/config.ts | 2 + .../components/AccumulativeShadowsDemo.vue | 20 ++ docs/component-list/components.ts | 2 + docs/guide/staging/accumulative-shadows.md | 34 +++ docs/guide/staging/randomized-lights.md | 35 ++++ .../pages/staging/AccumulativeShadowsDemo.vue | 90 ++++++++ playground/vue/src/router/routes/staging.ts | 5 + .../ProgressiveLightMap.ts | 141 +++++++++++++ .../AccumulativeShadows/SoftShadowMaterial.ts | 37 ++++ .../staging/AccumulativeShadows/component.vue | 193 ++++++++++++++++++ .../RandomizedLights/RandomizedLights.ts | 131 ++++++++++++ .../staging/RandomizedLights/component.vue | 70 +++++++ src/core/staging/index.ts | 4 + 13 files changed, 764 insertions(+) create mode 100644 docs/.vitepress/theme/components/AccumulativeShadowsDemo.vue create mode 100644 docs/guide/staging/accumulative-shadows.md create mode 100644 docs/guide/staging/randomized-lights.md create mode 100644 playground/vue/src/pages/staging/AccumulativeShadowsDemo.vue create mode 100644 src/core/staging/AccumulativeShadows/ProgressiveLightMap.ts create mode 100644 src/core/staging/AccumulativeShadows/SoftShadowMaterial.ts create mode 100644 src/core/staging/AccumulativeShadows/component.vue create mode 100644 src/core/staging/RandomizedLights/RandomizedLights.ts create mode 100644 src/core/staging/RandomizedLights/component.vue diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 41eb2b4fe..07aac06d8 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -153,10 +153,12 @@ 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' }, { text: 'Ocean', link: '/guide/staging/ocean' }, + { 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 000000000..1cbc6d22d --- /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 f8a8e8f32..bb281b4e9 100644 --- a/docs/component-list/components.ts +++ b/docs/component-list/components.ts @@ -102,11 +102,13 @@ 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' }, { text: 'Ocean', link: '/guide/staging/ocean' }, { text: 'Fit', link: '/guide/staging/fit' }, + { 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 000000000..f481f42c7 --- /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 000000000..7cf5fe430 --- /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 000000000..c2a5ee48b --- /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 e6141523e..de3820dac 100644 --- a/playground/vue/src/router/routes/staging.ts +++ b/playground/vue/src/router/routes/staging.ts @@ -49,4 +49,9 @@ export const stagingRoutes = [ name: 'Fit', component: () => import('../../pages/staging/fit/index.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 000000000..6f380bb6a --- /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 000000000..de769adb5 --- /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 000000000..3d400ddd9 --- /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 000000000..a1cf42f6c --- /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 000000000..dcc73735b --- /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 69f98edfa..bcf6c6922 100644 --- a/src/core/staging/index.ts +++ b/src/core/staging/index.ts @@ -1,8 +1,10 @@ +import AccumulativeShadows from './AccumulativeShadows/component.vue' import Backdrop from './Backdrop.vue' import ContactShadows from './ContactShadows.vue' import Fit from './Fit.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 Sparkles from './Sparkles/component.vue' @@ -11,6 +13,7 @@ import Environment from './useEnvironment/component.vue' import Lightformer from './useEnvironment/lightformer/index.vue' export { + AccumulativeShadows, Backdrop, ContactShadows, Environment, @@ -18,6 +21,7 @@ export { Lightformer, Ocean, Precipitation, + RandomizedLights, Sky, Smoke, Sparkles, From f9281a59f10ac17a87af54c9537bfc558370c91e Mon Sep 17 00:00:00 2001 From: Peter Date: Fri, 20 Dec 2024 23:28:30 +0100 Subject: [PATCH 2/4] refactor(RandomizedLights): rename interface --- src/core/staging/RandomizedLights/component.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/staging/RandomizedLights/component.vue b/src/core/staging/RandomizedLights/component.vue index dcc73735b..bd2936e78 100644 --- a/src/core/staging/RandomizedLights/component.vue +++ b/src/core/staging/RandomizedLights/component.vue @@ -4,7 +4,7 @@ import type { VectorFlexibleParams } from '@tresjs/core' import { extend, normalizeVectorFlexibleParam } from '@tresjs/core' import RandomizedLights from './RandomizedLights' -interface RandomizedLightProps { +export interface RandomizedLightsProps { /** Number of lights, 8 */ count?: number /** Radius of the jiggle, higher values make softer light, 1 */ @@ -29,7 +29,7 @@ interface RandomizedLightProps { position?: VectorFlexibleParams } -const props = withDefaults(defineProps(), { +const props = withDefaults(defineProps(), { count: 8, radius: 1, intensity: Math.PI, From c62d0cd83520ef280c9238579fa87f570713bdbe Mon Sep 17 00:00:00 2001 From: Peter Date: Fri, 20 Dec 2024 23:33:35 +0100 Subject: [PATCH 3/4] refactor(AccumulativeShadows): change default alphaTest value --- src/core/staging/AccumulativeShadows/component.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/staging/AccumulativeShadows/component.vue b/src/core/staging/AccumulativeShadows/component.vue index 3d400ddd9..6b756931f 100644 --- a/src/core/staging/AccumulativeShadows/component.vue +++ b/src/core/staging/AccumulativeShadows/component.vue @@ -54,7 +54,7 @@ const props = withDefaults(defineProps(), { blend: 20, scale: 10, opacity: 1, - alphaTest: 0.75, + alphaTest: 0.65, color: 'black', colorBlend: 2, resolution: 1024, From 0e415826c4a3a0997d4b75b97558535a70a94c55 Mon Sep 17 00:00:00 2001 From: Peter Date: Fri, 20 Dec 2024 23:34:24 +0100 Subject: [PATCH 4/4] refactor(AccumulativeShadows): expose update function --- src/core/staging/AccumulativeShadows/component.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/staging/AccumulativeShadows/component.vue b/src/core/staging/AccumulativeShadows/component.vue index 6b756931f..66691aba4 100644 --- a/src/core/staging/AccumulativeShadows/component.vue +++ b/src/core/staging/AccumulativeShadows/component.vue @@ -157,7 +157,7 @@ useLoop().onBeforeRender(() => { watchEffect(() => gLights.value.traverse = () => null) -defineExpose({ instance: gOuter }) +defineExpose({ instance: gOuter, update: () => { reset(); update() } })