Skip to content

Commit 3d21171

Browse files
authored
feat: add CameraShake class for procedural camera shake effect (#88)
* feat: add CameraShake class for procedural camera shake effect
1 parent 9b140fd commit 3d21171

File tree

4 files changed

+265
-0
lines changed

4 files changed

+265
-0
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import * as THREE from 'three'
2+
import { Setup } from '../Setup'
3+
import GUI from 'lil-gui'
4+
import { Meta } from '@storybook/html'
5+
import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js'
6+
import { GroundedSkybox } from 'three/examples/jsm/objects/GroundedSkybox.js'
7+
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
8+
9+
import { CameraShake } from '../../src/core/CameraShake.ts'
10+
11+
export default {
12+
title: 'Staging/CameraShake',
13+
} as Meta
14+
15+
let gui: GUI
16+
let scene: THREE.Scene, camera: THREE.Camera, renderer: THREE.WebGLRenderer, controls: OrbitControls, animateLoop
17+
18+
let cameraShake: CameraShake, clock: THREE.Clock
19+
20+
export const CsStory = async () => {
21+
const setupResult = Setup()
22+
scene = setupResult.scene
23+
camera = setupResult.camera
24+
renderer = setupResult.renderer
25+
animateLoop = setupResult.render
26+
27+
gui = new GUI({ title: CsStory.storyName })
28+
renderer.shadowMap.enabled = true
29+
renderer.toneMapping = THREE.ACESFilmicToneMapping
30+
camera.position.set(12, 12, 12)
31+
32+
controls = new OrbitControls(camera, renderer.domElement)
33+
controls.target.set(0, 6, 0)
34+
controls.update()
35+
36+
const floor = new THREE.Mesh(
37+
new THREE.PlaneGeometry(60, 60).rotateX(-Math.PI / 2),
38+
new THREE.ShadowMaterial({ opacity: 0.3 })
39+
)
40+
floor.receiveShadow = true
41+
scene.add(floor)
42+
43+
const dirLight = new THREE.DirectionalLight(0xabcdef, 10)
44+
dirLight.position.set(1, 20, 1)
45+
dirLight.castShadow = true
46+
dirLight.shadow.mapSize.width = 1024
47+
dirLight.shadow.mapSize.height = 1024
48+
scene.add(dirLight)
49+
50+
clock = new THREE.Clock()
51+
52+
const geometry = new THREE.TorusKnotGeometry(3, 1, 100, 32)
53+
const mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ color: 0xffffff * Math.random() }))
54+
mesh.castShadow = true
55+
mesh.receiveShadow = true
56+
mesh.position.set(0, 6, 0)
57+
scene.add(mesh)
58+
59+
setupEnvironment()
60+
61+
setupCameraShake()
62+
}
63+
64+
/**
65+
* Add scene.environment and groundProjected skybox
66+
*/
67+
const setupEnvironment = () => {
68+
const exrLoader = new EXRLoader()
69+
70+
// exr from polyhaven.com
71+
exrLoader.load('round_platform_1k.exr', (exrTex) => {
72+
exrTex.mapping = THREE.EquirectangularReflectionMapping
73+
scene.environment = exrTex
74+
scene.background = exrTex
75+
76+
const groundProjection = new GroundedSkybox(exrTex, 10, 50)
77+
groundProjection.position.set(0, 10, 0)
78+
scene.add(groundProjection)
79+
})
80+
}
81+
82+
const setupCameraShake = () => {
83+
cameraShake = new CameraShake(camera)
84+
85+
// on orbit controls change event , update the initial values of camera shake
86+
controls.addEventListener('change', () => {
87+
cameraShake.updateInitialRotation()
88+
})
89+
90+
addGui(gui)
91+
92+
// Add camera shake to the animate loop
93+
animateLoop(() => {
94+
const delta = clock.getDelta()
95+
const elapsedTime = clock.getElapsedTime()
96+
cameraShake.update(delta, elapsedTime)
97+
})
98+
}
99+
100+
/**
101+
* Add gui
102+
* @param gui gui instance
103+
*/
104+
function addGui(gui: GUI) {
105+
const folder = gui.addFolder('Camera Shake')
106+
folder.add(cameraShake, 'intensity', 0, 1, 0.01).listen()
107+
folder.add(cameraShake, 'decay')
108+
folder.add(cameraShake, 'decayRate', 0, 1, 0.01)
109+
110+
folder.add(cameraShake, 'maxYaw', 0.01, Math.PI / 4, 0.01).name('Max Yaw')
111+
folder.add(cameraShake, 'maxPitch', 0.01, Math.PI / 4, 0.01).name('Max Pitch')
112+
folder.add(cameraShake, 'maxRoll', 0.01, Math.PI / 4, 0.01).name('Max Roll')
113+
114+
folder.add(cameraShake, 'yawFrequency', 0.1, 5, 0.1).name('Yaw Frequency').listen()
115+
folder.add(cameraShake, 'pitchFrequency', 0.1, 5, 0.1).name('Pitch Frequency').listen()
116+
folder.add(cameraShake, 'rollFrequency', 0.1, 5, 0.1).name('Roll Frequency').listen()
117+
}
118+
119+
CsStory.storyName = 'Default'

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import { pcss, ... } from '@pmndrs/vanilla'
5252
<li><a href="#accumulativeshadows">AccumulativeShadows</a></li>
5353
<li><a href="#caustics">Caustics</a></li>
5454
<li><a href="#cloud">Cloud</a></li>
55+
<li><a href="#camerashake">Camera Shake</a></li>
5556
</ul>
5657
<li><a href="#staging">Abstractions</a></li>
5758
<ul>
@@ -479,6 +480,46 @@ clouds.add(cloud_0)
479480
clouds.update(camera, clock.getElapsedTime(), clock.getDelta())
480481
```
481482

483+
#### CameraShake
484+
485+
[![storybook](https://img.shields.io/badge/-storybook-%23ff69b4)](https://pmndrs.github.io/drei-vanilla/?path=/story/staging-camerashake--cs-story)
486+
487+
[drei counterpart](https://drei.docs.pmnd.rs/staging/camera-shake)
488+
489+
A class for applying a configurable camera shake effect. Currently only supports rotational camera shake. Pass a camera object in the constructor.
490+
491+
If you use camera controls (like OrbitControls), call `.updateInitialRotation()` after the camera is moved to keep the shake relative to the new orientation.
492+
493+
Usage:
494+
495+
```ts
496+
const shake = new CameraShake(camera)
497+
498+
// If using OrbitControls:
499+
orbitControls.addEventListener('change', () => {
500+
shake.updateInitialRotation()
501+
})
502+
503+
// call in animate loop:
504+
function animate() {
505+
shake.update(delta, elapsedTime)
506+
}
507+
```
508+
509+
Shake Options:
510+
511+
```js
512+
shake.maxYaw= 0.1, // Max amount camera can yaw in either direction
513+
shake.maxPitch= 0.1, // Max amount camera can pitch in either direction
514+
shake.maxRoll= 0.1, // Max amount camera can roll in either direction
515+
shake.yawFrequency= 0.1, // Frequency of the yaw rotation
516+
shake.pitchFrequency= 0.1, // Frequency of the pitch rotation
517+
shake.rollFrequency= 0.1, // Frequency of the roll rotation
518+
shake.intensity= 1, // initial intensity of the shake
519+
shake.decay= false, // should the intensity decay over time
520+
shake.decayRate= 0.65, // if decay = true this is the rate at which intensity will reduce at
521+
```
522+
482523
#### Grid
483524
484525
[![storybook](https://img.shields.io/badge/-storybook-%23ff69b4)](https://pmndrs.github.io/drei-vanilla/?path=/story/gizmos-grid--grid-story)

src/core/CameraShake.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import * as THREE from 'three'
2+
import { SimplexNoise } from 'three/examples/jsm/Addons.js'
3+
4+
/**
5+
* Applies a procedural camera shake effect using simplex noise.
6+
* Call `update()` each frame to animate the shake.
7+
*/
8+
export class CameraShake {
9+
/** The object (camera or any Object3D) to apply shake to. */
10+
object: THREE.Object3D
11+
12+
/** The initial rotation of the object before shake is applied. */
13+
initialRotation: THREE.Euler
14+
15+
/** Shake intensity (0 = no shake, 1 = full shake). */
16+
intensity: number
17+
18+
/** If true, shake intensity decays over time. */
19+
decay: boolean
20+
21+
/** Rate at which intensity decays per second. */
22+
decayRate: number
23+
24+
/** Maximum yaw shake in radians. */
25+
maxYaw: number
26+
27+
/** Maximum pitch shake in radians. */
28+
maxPitch: number
29+
30+
/** Maximum roll shake in radians. */
31+
maxRoll: number
32+
33+
/** Frequency of yaw shake. */
34+
yawFrequency: number
35+
36+
/** Frequency of pitch shake. */
37+
pitchFrequency: number
38+
39+
/** Frequency of roll shake. */
40+
rollFrequency: number
41+
42+
/** Internal noise generator for yaw. */
43+
private yawNoise: SimplexNoise
44+
45+
/** Internal noise generator for pitch. */
46+
private pitchNoise: SimplexNoise
47+
48+
/** Internal noise generator for roll. */
49+
private rollNoise: SimplexNoise
50+
51+
/**
52+
* @param objectToShake The Object3D (usually a Camera) to apply shake to.
53+
*/
54+
constructor(objectToShake: THREE.Object3D) {
55+
this.object = objectToShake
56+
57+
this.initialRotation = new THREE.Euler().copy(this.object.rotation)
58+
59+
this.intensity = 1
60+
this.decay = false
61+
this.decayRate = 0.65
62+
this.maxYaw = 0.1
63+
this.maxPitch = 0.1
64+
this.maxRoll = 0.1
65+
this.yawFrequency = 0.1
66+
this.pitchFrequency = 0.1
67+
this.rollFrequency = 0.1
68+
69+
this.yawNoise = new SimplexNoise()
70+
this.pitchNoise = new SimplexNoise()
71+
this.rollNoise = new SimplexNoise()
72+
}
73+
74+
/**
75+
* Updates the stored initial rotation to match the object's current rotation.
76+
* Call this if you manually rotate the object and want shake to be relative to the new orientation.
77+
*/
78+
updateInitialRotation() {
79+
this.initialRotation.copy(this.object.rotation)
80+
}
81+
82+
/**
83+
* Updates the camera shake effect. Call once per frame.
84+
* @param delta Time since last frame.
85+
* @param elapsedTime Total elapsed time.
86+
*/
87+
update(delta: number, elapsedTime: number) {
88+
const shake = Math.pow(this.intensity, 2)
89+
const yaw = this.maxYaw * shake * this.yawNoise.noise(elapsedTime * this.yawFrequency, 1)
90+
const pitch = this.maxPitch * shake * this.pitchNoise.noise(elapsedTime * this.pitchFrequency, 1)
91+
const roll = this.maxRoll * shake * this.rollNoise.noise(elapsedTime * this.rollFrequency, 1)
92+
93+
this.object.rotation.set(
94+
this.initialRotation.x + pitch,
95+
this.initialRotation.y + yaw,
96+
this.initialRotation.z + roll
97+
)
98+
99+
if (this.decay && this.intensity > 0) {
100+
this.intensity -= this.decayRate * delta
101+
this.intensity = THREE.MathUtils.clamp(this.intensity, 0, 1)
102+
}
103+
}
104+
}

src/core/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export * from './shaderMaterial'
66
// Staging/Prototyping
77
export * from './AccumulativeShadows'
88
export * from './Cloud'
9+
export * from './CameraShake'
910

1011
// Misc
1112
export * from './useFBO'

0 commit comments

Comments
 (0)