Skip to content

Shock Wave

Demo code
vue
<script setup lang="ts">
import { ContactShadows, Environment, OrbitControls } from '@tresjs/cientos'
import { TresCanvas } from '@tresjs/core'
import { TresLeches, useControls } from '@tresjs/leches'
import { NoToneMapping, Shape, Vector3 } from 'three'
import { computed, onUnmounted, reactive, ref, shallowRef } from 'vue'
import { DepthPickingPassPmndrs, EffectComposerPmndrs, ShockWavePmndrs } from '@tresjs/post-processing'
import { useElementBounding, useMouse, useParentElement } from '@vueuse/core'
import { gsap } from 'gsap'

import '@tresjs/leches/styles'

const gl = {
  clearColor: '#8D404A',
  toneMapping: NoToneMapping,
  multisampling: 8,
}

const shockWaveEffectRef = shallowRef(null)
const elCanvasRef = ref(null)
const depthPickingPassRef = ref(null)
const meshHeartRef = ref(null)
const mousePosition = ref(new Vector3())

function createHeartShape(scale: number) {
  const shape = new Shape()
  const x = 0
  const y = 0

  shape.moveTo(x, y)
  shape.bezierCurveTo(x, y, x - 0.5 * scale, y + 2.5 * scale, x - 2.5 * scale, y + 2.5 * scale)
  shape.bezierCurveTo(x - 6.5 * scale, y + 2.5 * scale, x - 6.5 * scale, y - 1.5 * scale, x - 6.5 * scale, y - 1.5 * scale)
  shape.bezierCurveTo(x - 6.5 * scale, y - 4.5 * scale, x - 3.5 * scale, y - 7 * scale, x, y - 9.5 * scale)
  shape.bezierCurveTo(x + 3.5 * scale, y - 7 * scale, x + 6.5 * scale, y - 4.5 * scale, x + 6.5 * scale, y - 1.5 * scale)
  shape.bezierCurveTo(x + 6.5 * scale, y - 1.5 * scale, x + 6.5 * scale, y + 2.5 * scale, x + 2.5 * scale, y + 2.5 * scale)
  shape.bezierCurveTo(x + 0.5 * scale, y + 2.5 * scale, x, y, x, y)

  return shape
}

const heartShapeFront = createHeartShape(0.35)

const parentEl = useParentElement()
const { x, y } = useMouse({ target: parentEl })
const { width, height, left, top } = useElementBounding(parentEl)

const extrudeSettings = reactive({
  depth: 0.1,
  bevelEnabled: true,
  bevelSegments: 2,
  steps: 2,
  bevelSize: 0.25,
  bevelThickness: 0.25,
})

const materialProps = reactive({
  color: '#FF9999',
  reflectivity: 0.75,
  ior: 1.5,
  roughness: 0.75,
  clearcoat: 0.01,
  clearcoatRoughness: 0.15,
  transmission: 0.7,
})

let tl: gsap.core.Timeline

const ctx = gsap.context(() => { })

const { amplitude, waveSize, speed, maxRadius } = useControls({
  amplitude: { value: 0.4, step: 0.01, max: 1.0 },
  waveSize: { value: 0.5, step: 0.01, max: 1.0 },
  speed: { value: 1.5, step: 0.1, max: 10.0 },
  maxRadius: { value: 0.2, step: 0.01, max: 2 },
})

const cursorX = computed(() => ((x.value - left.value - width.value) / width.value) * 2.0 + 1.0)
const cursorY = computed(() => -((y.value - top.value - height.value) / height.value) * 2.0 - 1.0)

async function updateMousePosition() {
  if (!elCanvasRef.value || !shockWaveEffectRef.value || !depthPickingPassRef.value) { return }

  const ndcPosition = new Vector3(cursorX.value, cursorY.value, 0)

  // Read depth from depth picking pass
  ndcPosition.z = await depthPickingPassRef.value.pass.readDepth(ndcPosition)

  ndcPosition.z = ndcPosition.z * 2.0 - 1.0

  mousePosition.value.copy(ndcPosition.unproject(elCanvasRef.value.context.camera.value))
}

function triggerShockWave() {
  if (!meshHeartRef.value || !shockWaveEffectRef.value) { return }

  updateMousePosition()

  shockWaveEffectRef.value.effect.explode()

  const duration = getActiveDuration()

  const durationSeconds = duration / 1000

  ctx.add(() => {
    tl?.kill()

    tl = gsap.timeline()

    tl.to(meshHeartRef.value.scale, {
      duration: durationSeconds / 9,
      x: 0.8,
      y: 0.8,
      z: 0.8,
      ease: 'power2.inOut',
    }).to(meshHeartRef.value.scale, {
      duration: durationSeconds / 9,
      x: 1.2,
      y: 1.2,
      z: 1.2,
      ease: 'power2.inOut',
    }).to(meshHeartRef.value.scale, {
      duration: durationSeconds / 9,
      x: 1,
      y: 1,
      z: 1,
      ease: 'power2.inOut',
    })
  })

  // Fallback for onFinish explode Shock Wave
  // setTimeout(() => {
  // console.log('Explode effect animation done')
  // }, duration)
}

function getActiveDuration() {
  // This function retrieves the duration for emitting the shock wave.
  // For more details, see: https://github.com/pmndrs/postprocessing/blob/3d3df0576b6d49aec9e763262d5a1ff7429fd91a/src/effects/ShockWaveEffectRef.js#L258-L301

  // To reduce the duration of the animation, you can decrease the values of maxRadius and waveSize.
  // Note that the speed affects how quickly the shock wave radius increases over time, but not the total duration of the emit explode.

  // Retrieve the values dynamically
  const radiusMax = maxRadius.value.value
  const wave = waveSize.value.value

  // Duration formula: 2 * maxRadius + 3 * waveSize
  const duration = 2 * radiusMax + 3 * wave

  // Convert to milliseconds
  return duration * 1000
}

onUnmounted(() => {
  ctx.revert()
})
</script>

<template>
  <TresLeches style="left: initial;right:10px; top:10px;" />

  <p class="doc-shock-wave-instructions text-xs font-semibold text-zinc-600">Click on the heart to distribute love</p>

  <TresCanvas
    ref="elCanvasRef"
    v-bind="gl"
  >
    <TresPerspectiveCamera
      :position="[0, 0, 10]"
    />

    <OrbitControls make-default auto-rotate />

    <TresMesh ref="meshHeartRef" :position-y="2" @click="triggerShockWave">
      <TresExtrudeGeometry :args="[heartShapeFront, extrudeSettings]" />
      <TresMeshPhysicalMaterial
        v-bind="materialProps"
      />
    </TresMesh>

    <TresDirectionalLight
      :position="[5, 5, 7.5]"
      :intensity="2"
    />

    <ContactShadows
      :opacity="1"
      :position-y="-2.75"
      :blur=".5"
    />

    <Suspense>
      <Environment preset="night" />
    </Suspense>

    <Suspense>
      <EffectComposerPmndrs>
        <DepthPickingPassPmndrs ref="depthPickingPassRef" />
        <ShockWavePmndrs ref="shockWaveEffectRef" :position="mousePosition" :amplitude="amplitude.value" :waveSize="waveSize.value" :speed="speed.value" :maxRadius="maxRadius.value" />
      </EffectComposerPmndrs>
    </Suspense>
  </TresCanvas>
</template>

<style scoped>
.doc-shock-wave-instructions {
  position: absolute;
  bottom: 0;
  left: 0;
  padding: 0.65rem 0.85rem;
  text-align: center;
  color: #fff;
  z-index: 2;
  border-radius: 0px 10px 0px 0px;
  background: linear-gradient(90deg, hsla(24, 100%, 83%, 1) 0%, hsla(341, 91%, 68%, 1) 100%);
  margin: 0;
}
</style>

The ShockWave effect is part of the postprocessing package. It simulates a shockwave effect originating from a center point, creating a ripple-like distortion in the scene. This effect can add dramatic impact to your scene by simulating explosions or other shockwave phenomena.

Usage

The <ShockWavePmndrs> component is easy to use and provides customizable options to suit different visual styles. There are several possible techniques to achieve this. See Events and DepthPickingPass for more details.

The main difference between Events and DepthPickingPass lies in the scope you want. Events is more suited for being used on a specific element, while DepthPickingPass is intended to be used for an entire scene (depth is calculated globally).

Events

To determine the position of the shockwave effect, you can use Tres.js events. Tres.js allows you to handle user interactions directly and find the intersection point with objects in the scene. This technique is useful when you need to interact with specific objects based on user input.

You can use various Tres.js events such as click, pointer-enter, etc., to trigger the shockwave effect. For more details about available events, see the documentation.

Here is an example of how to use events to trigger the shockwave effect:

vue
<script setup lang="ts">
import { EffectComposerPmndrs, ShockWavePmndrs } from '@tresjs/post-processing'
import { useMouse, useWindowSize } from '@vueuse/core'
import { NoToneMapping, Vector3 } from 'three'
import { computed, ref } from 'vue'
import { TresCanvas } from '@tresjs/core'

const gl = {
  toneMapping: NoToneMapping,
  multisampling: 8,
}

const effectProps = {
  speed: 0.2,
}

const position = ref(new Vector3(0, 0, 0))
const shockWaveEffectRef = ref(null)

const { x, y } = useMouse()
const { width, height } = useWindowSize()

const cursorX = computed(() => (x.value / width.value) * 2.0 - 1.0)
const cursorY = computed(() => -(y.value / height.value) * 2.0 + 1.0)

function updateMousePosition({ point }) {
  mousePosition.value.copy(point)
}

function triggerShockWave() {
  if (!shockWaveEffectRef.value) { return }

  updateMousePosition()

  shockWaveEffectRef.value.effect.explode()
}
</script>

<template>
  <TresCanvas v-bind="gl">
    <TresPerspectiveCamera
      :position="[5, 5, 5]"
      :look-at="[0, 0, 0]"
    />
    <TresMesh @click="triggerShockWave">
      <TresBoxGeometry />
      <TresMeshStandardMaterial color="#1C1C1E" />
    </TresMesh>

    <Suspense>
      <EffectComposerPmndrs>
        <ShockWavePmndrs
          ref="shockWaveEffectRef"
          :position="position"
          v-bind="effectProps"
        />
      </EffectComposerPmndrs>
    </Suspense>
  </TresCanvas>
</template>

DepthPickingPass

The DepthPickingPassPmndrs component reads depth information from the scene. This is particularly useful for interacting with 3D objects based on their depth, such as triggering effects at specific points in 3D space.

In the example above, DepthPickingPassPmndrs determines the depth of the point where the shockwave effect should originate, allowing accurate interaction with 3D objects.

vue
<script setup lang="ts">
import { DepthPickingPassPmndrs, EffectComposerPmndrs, ShockWavePmndrs } from '@tresjs/post-processing'
import { useMouse, useWindowSize } from '@vueuse/core'
import { NoToneMapping, Vector3 } from 'three'
import { computed, ref } from 'vue'
import { TresCanvas } from '@tresjs/core'

const gl = {
  toneMapping: NoToneMapping,
  multisampling: 8,
}

const effectProps = {
  speed: 0.2,
}

const position = ref(new Vector3(0, 0, 0))
const depthPickingPassRef = ref(null)
const shockWaveEffectRef = ref(null)
const elCanvasRef = ref(null)

const { x, y } = useMouse()
const { width, height } = useWindowSize()

const cursorX = computed(() => (x.value / width.value) * 2.0 - 1.0)
const cursorY = computed(() => -(y.value / height.value) * 2.0 + 1.0)

async function updateMousePosition() {
  if (!elCanvasRef.value || !depthPickingPassRef.value) { return }

  const ndcPosition = new Vector3(cursorX.value, cursorY.value, 0)

  ndcPosition.z = await depthPickingPassRef.value.pass.readDepth(ndcPosition)
  ndcPosition.z = ndcPosition.z * 2.0 - 1.0

  position.value.copy(ndcPosition.unproject(elCanvasRef.value.context.camera.value))
}

function triggerShockWave() {
  if (!shockWaveEffectRef.value) { return }

  updateMousePosition()

  shockWaveEffectRef.value.effect.explode()
}
</script>

<template>
  <TresCanvas
    v-bind="gl"
    ref="elCanvasRef"
  >
    <TresPerspectiveCamera
      :position="[5, 5, 5]"
      :look-at="[0, 0, 0]"
    />

    <TresMesh @click="triggerShockWave">
      <TresBoxGeometry />
      <TresMeshStandardMaterial color="#1C1C1E" />
    </TresMesh>

    <Suspense>
      <EffectComposerPmndrs>
        <DepthPickingPassPmndrs ref="depthPickingPassRef" />
        <ShockWavePmndrs
          ref="shockWaveEffectRef"
          :position="position"
          v-bind="effectProps"
        />
      </EffectComposerPmndrs>
    </Suspense>
  </TresCanvas>
</template>

For more details about DepthPickingPass, see the documentation.

Props

PropDescriptionDefault
positionThe position of the shockwave.Vector3(0, 0, 0)
amplitudeThe amplitude of the shockwave.0.05
waveSizeThe wave size of the shockwave.0.2
speedThe speed of the shockwave.2.0
maxRadiusThe max radius of the shockwave.1.0

Further Reading

see postprocessing docs