Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(AccumulativeShadows): add component, demo, docs #558

Merged
merged 6 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,13 +156,15 @@ 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: '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' },
],
},
{
Expand Down
20 changes: 20 additions & 0 deletions docs/.vitepress/theme/components/AccumulativeShadowsDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script setup lang="ts">
import { AccumulativeShadows, OrbitControls } from '@tresjs/cientos'
import { TresCanvas } from '@tresjs/core'
</script>

<template>
<TresCanvas clear-color="#fbb03b" :shadows="true">
<OrbitControls />
<TresMesh :position-y="0.3" :scale="0.4" :cast-shadow="true">
<TresTorusKnotGeometry />
<TresMeshNormalMaterial />
</TresMesh>
<AccumulativeShadows
:blend="100"
color="#fbb03b"
once
:position-y="-0.4"
/>
</TresCanvas>
</template>
2 changes: 2 additions & 0 deletions docs/component-list/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand All @@ -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' },
],
},
{
Expand Down
34 changes: 34 additions & 0 deletions docs/guide/staging/accumulative-shadows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# AccumulativeShadows

<DocsDemo>
<AccumulativeShadowsDemo />
</DocsDemo>

`<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 `<AccumulatedShadows />`, but it's designed to be used with `<RandomizedLights />`.

By default, there's a `<RandomizedLights />` instance provided in `<AccumulatedShadows />`'s `<slot />`. You can replace it with your own `<RandomizedLights />` or an alternative by passing it as a child component.
35 changes: 35 additions & 0 deletions docs/guide/staging/randomized-lights.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# RandomizedLights

`<RandomizedLights />` internally creates multiple lights and jiggles them. You would normally add it as a child of `<AccumulativeShadows />`.

It is based on this [Drei component](http://drei.docs.pmnd.rs/staging/randomized-light).

## Usage

```vue
<RandomizedLights
:ambient="0.25"
:bias="0.001"
:count="8"
:intensity="Math.PI"
:map-size="1024"
:position="[5, 5, -10]"
:radius="2"
/>
```

## 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]` |
90 changes: 90 additions & 0 deletions playground/vue/src/pages/staging/AccumulativeShadowsDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<script setup lang="ts">
import { AccumulativeShadows, Box, Environment, Icosahedron, OrbitControls, Sphere } from '@tresjs/cientos'
import { NoToneMapping } from 'three'
import { TresCanvas } from '@tresjs/core'
import { TresLeches, useControls } from '@tresjs/leches'
import '@tresjs/leches/styles'

const c = useControls({
frames: { value: 100, min: 2, max: 200, step: 1 },
once: true,
accumulate: true,
limit: { value: 1000, min: 0, max: 10000, step: 10 },
isLimitInfinite: false,
blend: { value: 200, min: 2, max: 1000, step: 1 },
alphaTest: { value: 0.75, min: 0, max: 1, step: 0.01 },
colorBlend: { value: 2, min: 0, max: 5, step: 0.1 },
opacity: { value: 2, min: 0, max: 2, step: 0.1 },
resolution: { value: 2048, min: 16, max: 2048, step: 16 },
scale: { value: 12, min: 1, max: 24, step: 1 },
toneMapped: true,
isOrange: true,
enabled: true,
})

const x = shallowRef(0)

let intervalId: ReturnType<typeof setInterval>
let elapsed = 0
onMounted(() => {
intervalId = setInterval(() => {
elapsed += 1000 / 30
x.value = Math.sin(elapsed * 0.001)
}, 1000 / 30)
})

onUnmounted(() => {
clearInterval(intervalId)
})
</script>

<template>
<TresLeches />
<TresCanvas :shadows="true" clear-color="orange" :tone-mapping="NoToneMapping">
<TresPerspectiveCamera :args="[50]" :position="[0, 0.6, 2]" :look-at="[0, 0.5, 0]" name="mainCam" />

<OrbitControls />
<TresDirectionalLight :position="[5, 10, -10]" :intensity="3.14" />
<Box
:args="[0.4, 0.4, 0.4]"
:cast-shadow="true"
:position="[-0.5, 0.2, -0.3]"
>
<TresMeshStandardMaterial color="orange" />
</Box>
<Icosahedron
:args="[0.3]"
:cast-shadow="true"
:position="[x, 0.3, 0.4]"
>
<TresMeshNormalMaterial />
</Icosahedron>

<Sphere
:scale="0.2"
:position="[0.5, 0.4, -0.3]"
:cast-shadow="true"
>
<TresMeshStandardMaterial color="lightblue" />
</Sphere>

<AccumulativeShadows
v-if="c.enabled.value.value"
:accumulate="c.accumulate.value.value"
:alpha-test="c.alphaTest.value.value"
:blend="c.blend.value.value"
:color="c.isOrange.value.value ? 'orange' : 'blue'"
:color-blend="c.colorBlend.value.value"
:frames="c.frames.value.value"
:limit="c.limit.value.value"
:once="c.once.value.value"
:opacity="c.opacity.value.value"
:resolution="c.resolution.value.value"
:tone-mapped="c.toneMapped.value.value"
:scale="c.scale.value.value"
/>
<Suspense>
<Environment preset="city" />
</Suspense>
</TresCanvas>
</template>
5 changes: 5 additions & 0 deletions playground/vue/src/router/routes/staging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
},
]
141 changes: 141 additions & 0 deletions src/core/staging/AccumulativeShadows/ProgressiveLightMap.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading
Loading