TECH3

VOICE API + PHYSICS​​

05                                        2025-2026

In dit onderdeel leren we hoe we de voice API kunnen integreren met 3D en zien we hoe we interactieve en leuke scenes maken. Physics is na dit onderdeel geen geheim meer.

VOICE API + PHYSICS

VOICE API

01

01

VOICE API

SPEECH RECOGNITION

Sommige browsers hebben standaard een Web Speech API ingebouwd. Daarmee kun je via JavaScript spraak herkennen en meteen omzetten naar tekst. Het werkt alleen niet overal even goed; Chrome ondersteunt het het best.

Hoe werkt het?

  • Toegang tot de microfoon van de gebruiker

  • Audio wordt doorgestuurd naar de speech-engine

  • API stuurt transcripties terug via events

  • Zowel live (interim) als definitieve resultaten

01

VOICE API

PROPERTIES & EVENTS

Belangrijke properties

  • continuous: blijft continu luisteren

  • interimResults: toont tussentijdse resultaten

  • lang: taalinstelling (bv. "nl-NL", "en-US")

  • maxAlternatives: aantal mogelijke transcripties

Belangrijke events

  • onstart: begint met luisteren

  • onresult: nieuwe transcriptie beschikbaar

  • onerror: foutmelding (bv. geen microfoon, API niet beschikbaar)

  • onend: stoppen met luisteren

01

VOICE API

SCENARIO

Je kunt met deze API van alles doen: tekst laten inspreken, apps met je stem bedienen (dat gaan wij doen!), websites handsfree gebruiken en mensen met een beperking beter ondersteunen...

Limitaties

  • Geen support in Firefox, Safari en diverse mobiele browsers

  • Internetverbinding verplicht

  • Privacy- en dataverwerkingsrisico’s (GDPR!)

  • Minder nauwkeurig bij veel achtergrondgeluid

Er zijn andere API's en Speech-to-text libraries die mogelijks wel op Firefox en Safari werken. 

01

VOICE API

LET'S START

We gaan eerst een custom hook maken. Die hook zorgt ervoor dat we heel makkelijk kunnen starten en stoppen met luisteren, en dat we het stukje herkende tekst kunnen ophalen.

Door dit in een custom hook te stoppen, kunnen we het makkelijk gebruiken in andere components. Alles zit netjes op één plek.

Maak een folder hooks aan en plaats daarin useSpeechToText.jsx. Vervolgens plaats je de scafold voor je hook. 

import { useEffect, useRef, useState } from 'react'

const useSpeechToText = (options = {}) => {
  
}

export default useSpeechToText

01

VOICE API

LET'S START

We beginnen bij de basis: in onze hook maken we eerst de states en refs aan die we straks nodig hebben.

import { useEffect, useRef, useState } from 'react'

const useSpeechToText = (options = {}) => {
   const [isListening, setIsListening] = useState(false);
   const [transcript, setTranscript] = useState("");
   const recognitionRef = useRef(null);
}

export default useSpeechToText
  • isListening → toont of microfoon actief is

  • transcript → herkende tekst

  • recognitionRef → houdt de SpeechRecognition-instance persistent vast

01

VOICE API

API CHECK

We moeten eerst checken of de voice API wel werkt. Als dat niet zo is, pakken we dat netjes op. Omdat we dit meteen bij het laden van de pagina willen doen, gebruiken we een useEffect zonder dependencies.

useEffect(() => {
         if (!('webkitSpeechRecognition' in window)) {
             console.error("Speech Recognition API not supported in this browser.");
             return;
         }
     }, []);
  • WebkitSpeechRecognition werkt alleen in Chrome

  • We vermijden crashes door een check uit te voeren

  • Ideaal punt om later een polyfill of fallback in te pluggen

Een polyfill is een stukje JavaScript-code dat nieuwe browserfunctionaliteit emuleert in oudere of niet-ondersteunde browsers.

01

VOICE API

MAKE IT

Nu we zeker weten dat de API werkt, kunnen we de spraakherkenning opstarten. We maken een SpeechRecognition aan en geven de opties
door die we willen gebruiken, zoals taal en live tussentijdse resultaten.

useEffect(() => {
      //...
         recognitionRef.current = new window.webkitSpeechRecognition();
         const recognition = recognitionRef.current;

         recognition.interimResults = options.interimResults ?? true;
         recognition.lang = options.lang ?? 'en-US';
         recognition.continuous = options.continuous ?? false;
     }, []);
  • Maakt de native spraakherkenning aan

  • We bewaren deze in een ref, zodat hij niet herbouwd wordt bij elke render

01

VOICE API

OPTIONAL GRAMMAR

Je kunt een grammar toevoegen, zodat de API beter herkent wat je verwacht. Maar tegenwoordig merken de meeste browsers daar nauwelijks iets van.

useEffect(() => {
      //...
      
        if ("webkitSpeechGrammarList" in window) {
             const grammar = '#JSGF V1.0;';
             const speechRecognitionList = new window.webkitSpeechGrammarList();
             speechRecognitionList.addFromString(grammar, 1);
             recognition.grammars = speechRecognitionList;
         }
     }, []);

01

VOICE API

RESULTS

Om te weten wat er gezegd wordt, gebruiken we het onresult event. We gaan door de resultaten heen en voegen elke nieuwe tekst toe aan onze transcript state. Zo zie je live wat er herkend wordt.

useEffect(() => {
      //...
      
      recognition.onresult = (event) => {
   	  let text = "";
      for (let i = 0; i < event.results.length; i++) {
          text += event.results[i][0].transcript;
      }
      setTranscript(text);
      };
}, []);
  • Event bevat een lijst met (interim + final) resultaten

  • We plakken alle resultaten samen

  • Transcript wordt rechtstreeks in React state geplaatst

01

VOICE API

ERROR / STOP

Wat we zeker niet mogen vergeten is het afhandelen van de errors en wanneer onze recognition api stopt. Dit doen we als volgt.

useEffect(() => {
      //...
      
      recognition.onerror = (event) => {
         console.error("Speech recognition error", event);
	  };
      
      recognition.onend = () => {
         setIsListening(false);
         setTranscript("");
      };
}, []);
  • Logt foutmeldingen zoals microfoonweigering

  • Hier kan je UI-feedback of retries toevoegen

  • Wordt getriggerd wanneer de API vanzelf stopt

  • Reset states zodat UI weer synchroon is

01

VOICE API

CLEANUP

Ook in de cleanup function van onze useEffect moeten we zien dat onze API stopt.

useEffect(() => {
      //...
      
      return () => {
         if (recognition && typeof recognition.stop === 'function') {
            try { recognition.stop(); } catch (e) {}
         }
      };
}, []);
  • Voorkomt memory leaks

  • Voorkomt dat de microfoon blijft luisteren na unmount

  • Verwijdert native listeners

01

VOICE API

START / STOP

Onze API is nu opgezet, het enige wat ons nog resteert is het aanmaken van een startfunctie, stopfunctie en deze te returnen.

const startListening = () => {
    if (recognitionRef.current && !isListening) {
        recognitionRef.current.start();
        setIsListening(true);
    }
};

const stopListening = () => {
    if (recognitionRef.current && isListening) {
        recognitionRef.current.stop();
        setIsListening(false);
    }
};

return { isListening, transcript, startListening, stopListening };

Met alles samen heb je nu een mooie API hook die onze de woorden terug zal geven.

VOICE API + PHYSICS

MAKE IT REACT

02

02

MAKE IT REACT

REACTIVE SCENE

We maken nu een kleine demo: een box die van kleur verandert als je "Change" zegt. Een button start de voice API en de functie die de kleur verandert wordt gedeeld via een context. Zo zie je hoe alles samenwerkt.

Maak een component VoiceButton.jsx aan waarin we onze knop teruggeven.

import React, { useContext, useEffect } from 'react'
import useSpeechToText from '../../hooks/useSpeechToText';

const Voice = () => {

    const { isListening, transcript, startListening, stopListening } = useSpeechToText();

    const listenToggle = () => {
        isListening ? stopVoiceInput() : startListening();
        
    }

    const stopVoiceInput = () => {
        console.log(transcript);
        stopListening()
    }

  return (
    <div className="voice-ui">
        <button className="voice-button" onClick={listenToggle}>{isListening ? "Stop Listening" : "Speak"}</button>
    </div>
  )
}

export default Voice

02

MAKE IT REACT

CONTEXT

We willen onze state doorgeven aan onze experience dus maken we een contextProvider aan. 

Maak een colorContext aan in /context. Maak je provider en plaats deze in App.

import React, { createContext, useCallback, useState } from 'react'

const ColorContext = createContext({
  triggerColorChange: () => {},
  color: "#ffffff",
})

export const ColorProvider = ({ children }) => {
  const [color, setColor] = useState("#ffffff")

  const triggerColorChange = useCallback(() => {
    const randomColor = "#" + Math.floor(Math.random() * 16777215).toString(16)
    setColor(randomColor)
  }, [])

  return (
    <ColorContext.Provider value={{ triggerColorChange, color }}>
      {children}
    </ColorContext.Provider>
  )
}

export default ColorContext

02

MAKE IT REACT

BACK TO VOICE

Nu we onze context hebben kunnen we onze voice button afwerken.
We zullen checken of het woord 'change' is gezegd.

Maak een useEffect aan en plaats daarin de logica voor het woord.

const { triggerColorChange } = useContext(ColorContext);
  
 useEffect(() => {
        if (!transcript || !transcript.length) return;

        const words = transcript.trim().split(/\s+/).filter(x => x);
        words.forEach(w => console.log("Word:", w));

        const normalized = words.map(w => w.toLowerCase().replace(/[^\w]/g, ""));
        if (normalized.includes("change")) {
            try { triggerColorChange(); } catch (e) { /* ignore */ }
            console.log("triggerColorChange called")
        }

        if (words.length > 0 && isListening) {
            try { stopListening(); } catch (e) { /* ignore */ }
        }
    }, [transcript, isListening, stopListening, triggerColorChange])

02

MAKE IT REACT

EXPERIENCE

Nu onze functionaliteiten werken en de color variable aangepast zal worden als we 'change' zeggen kunnen we nu onze cube veranderen van kleur door de color op de material te plaatsen.

Plaats een ref op de cube en verander de color naar die uit de context.

import { OrbitControls } from "@react-three/drei";
import Box from "./components/Box/Box";
import Ground from "./components/Ground/Ground";
import { useContext, useEffect, useRef } from "react";
import * as THREE from "three";
import  ColorContext  from "./context/ColorContext";

const Experience = () => {

    const cubeRef = useRef(null)

   const { color } = useContext(ColorContext)

  // Update mesh color whenever the color in context changes
    useEffect(() => {
        if (cubeRef.current) {
        cubeRef.current.material.color.set(color)
        }
    }, [color])

    return (
        <>
            {/* Controls */}
            <OrbitControls makeDefault />

            {/* Lights */}
            <directionalLight position={[1, 2, 3]} intensity={4.5} />
            <ambientLight intensity={1} />

            {/* Models */}
            <Box ref={cubeRef} rotation-y={Math.PI * 0.25} scale={1.5}/> 

            {/* Floor */}
            <Ground position-y={- 1} rotation-x={- Math.PI * 0.5} scale={10}/>
        </>
    )
}

export default Experience

02

MAKE IT REACT

CSS

Hier is de Css file om het iets aangenamer te maken.

html, body, #root {
  height: 100%;
  width: 100%;
  margin: 0;
}

body {
  overflow: hidden;
}

.full-screen-canvas {
  position: fixed;
  inset: 0; 
  width: 100vw;
  height: 100vh;
  z-index: 0;
  touch-action: none;
}

.voice-ui {
  position: fixed;
  top: 16px;
  right: 16px;
  z-index: 10; 
  display: flex;
  gap: 8px;
  align-items: center;
}

.voice-button {
  padding: 8px 12px;
  border-radius: 6px;
  border: none;
  background: rgba(0,0,0,0.6);
  color: #fff;
  cursor: pointer;
  backdrop-filter: blur(4px);
}

VOICE API + PHYSICS

PHYSICS (RAPIER)

03

03

PHYSICS (RAPIER)

WHAT?

React Three Rapier is de officiële physics integratie voor React Three Fiber.
Het maakt het mogelijk om declaratief echte 3D physics toe te voegen in R3F scenes, zonder WebGL boilerplate.

  • ondersteunt rigid bodies, colliders, constraints, joints, sensors, impulses

  • extreem snel dankzij native Rust physics (sneller dan Cannon / Ammo engines)

  • volledig React gedreven: hooks, components, lifecycle events

  • ideaal voor product configurators, games, AR commerce en simulatie

  • stabiele collisions, stacking en sleeping (geen jank / drift)

  • werkt soepel samen met controls en interactieve UI flows, blijft volledig in sync met React state

03

PHYSICS (RAPIER)

STARTER

We starten met een basis scene zonder bijzondere eigenschappen. De gepresenteerde concepten zijn echter van toepassing op alle 3D-modellen en -omgevingen.

npm install @react-three/rapier

Eens je de starter file gestart hebt voer je volgende uit in de terminal.

Rapier is succesvol geïnstalleerd, waardoor we nu physics-functionaliteit kunnen toepassen.

03

PHYSICS (RAPIER)

PHYSICS

De Physics-component identificeert objecten die actief deelnemen aan de physics-simulatie. Objecten buiten deze component blijven statisch en hebben geen invloed op de simulatie.

import { Physics } from '@react-three/rapier'

<Physics>
     <group>
         <Sphere position-x={-2} position-y={2} scale={1} />     
         <Box ref={cubeRef} rotation-y={Math.PI * 0.25} position-x={2} scale={1.5}/> 
     </group> 

     {/* Floor */}
     <Ground position-y={- 1} rotation-x={- Math.PI * 0.5} scale={10}/>      
</Physics>

Importeer de Physics-component uit Rapier en omsluit onze drie meshes hiermee.

03

PHYSICS (RAPIER)

RIGIDBODY

Zodra de Physics-“World” rond de meshes is opgezet, kunnen we specificeren welke objecten door de physics beïnvloed worden door een RigidBody rondom elk object te plaatsen.

import { RigidBody, Physics } from '@react-three/rapier'

<Physics>
   <RigidBody>
      <Sphere position-x={-2} position-y={2} scale={1} />     
   </RigidBody>

    {/* ... */}
</Physics>

Importeer de RigidBody-component en omsluit de geselecteerde mesh hiermee.

De sphere valt door de grond... waarom is dit het geval?

03

PHYSICS (RAPIER)

RIGIDBODY

Het is noodzakelijk een RigidBody rond de plane te plaatsen om interactie tussen de objecten mogelijk te maken.

<RigidBody>
     <Sphere position-x={-2} position-y={2} scale={1} />     
</RigidBody>

Zet de Ground in een RigidBody en zie wat er gebeurt in de simulatie.

Zie je dat de vloer meebeweegt met de sphere? Dat willen we niet. Geef de RigidBody van de vloer het type ‘fixed’ om dit te fixen.

<RigidBody type="fixed">
      <Ground position-y={- 1} rotation-x={- Math.PI * 0.5} scale={10}/>
</RigidBody>

03

PHYSICS (RAPIER)

WHAT HAPPENS?

Het integreren van physics via React Three Rapier is eenvoudig. Houd rekening met de volgende aandachtspunten:

  • Het is niet nodig de physics bij elk frame te updaten.

  • Objecten zijn automatisch geassocieerd met hun respectieve RigidBody.

  • Rapier genereert physics-shapes die overeenkomen met de Three.js-objecten.

  • Specificatie van eigenschappen zoals oppervlak, massa of zwaartekracht is niet vereist.

Let op: HMR pakt niet altijd alle wijzigingen. Ververs de pagina soms, want complexe physics kan kleine glitches veroorzaken.

03

PHYSICS (RAPIER)

DEBUGGING

Wil je de physics beter begrijpen? Zet de debug-property op Physics en zie welke objecten botsen en op welke manier. Let op: gebruik debug alleen tijdelijk, het kan de performance drukken.

<Physics debug>

    {/* ... */}

</Physics>

Activeer debug op het Physics component

03

PHYSICS (RAPIER)

DEBUGGING

Wil je de physics beter begrijpen? Zet de debug-property op Physics en zie welke objecten botsen en op welke manier. Let op: gebruik debug alleen tijdelijk, het kan de performance drukken.

<Physics debug>

    {/* ... */}

</Physics>

Activeer debug op het Physics component

VOICE API + PHYSICS

COLLIDERS

04

04

COLLIDERS

COLLIDER

Colliders definiëren de geometrische vorm van een RigidBody. Voor de sphere wordt een cube gebruikt, aangeduid als een ‘cuboid’ collider, wat de standaard is voor alle RigidBodies in Rapier.

Plaats een cuboidCollider rond de box door hem in de RigidBody te zetten.

<RigidBody>
    <Box ref={cubeRef} rotation-y={Math.PI * 0.25} position-x={2} position-y={2} scale={1.5}/> 
</RigidBody>

Verander de schaal van de mesh en kijk of de collider correct meebeweegt.

<RigidBody>
    <Box ref={cubeRef} rotation-y={Math.PI * 0.25} position-x={2} position-y={2} scale={3}/> 
</RigidBody>

04

COLLIDERS

COLLIDER

Wanneer de cube wordt aangepast naar een rechthoek, zal de standaard cuboidCollider zich automatisch optimaliseren om de mesh-vorm te volgen.

Pas de args van je boxGeometry aan om de box in een rechthoek te veranderen.

<RigidBody>
    <mesh castShadow position={ [ 2, 2, 0 ] }>
        <boxGeometry args={ [ 3, 2, 1 ] } />
        <meshStandardMaterial color="mediumpurple" />
    </mesh>
</RigidBody>

04

COLLIDERS

COMPOSED

Een RigidBody kan meerdere colliders bevatten. Bij toevoeging van een tweede mesh genereert de RigidBody automatisch een collider die optimaal overeenkomt met de nieuwe mesh.

Voeg een mesh toe aan de Box component.

<RigidBody>
   <mesh {...props}>
        <boxGeometry />
        <meshStandardMaterial color={color || "royalblue"}/>
    </mesh>
    <mesh>
        <boxGeometry args={[3,2,1]}/>
        <meshStandardMaterial color={color || "royalblue"}/>
    </mesh>
</RigidBody>

04

COLLIDERS

COMPOSED

Wanneer de tweede mesh wordt verplaatst, beïnvloedt dit het zwaartepunt van het object. De RigidBody verwerkt automatisch de invloed van beide colliders.

Verander de positie van de tweede box.

<RigidBody>
    <mesh {...props}>
        <boxGeometry />
        <meshStandardMaterial color={color || "royalblue"}/>
    </mesh>
    <mesh position-z={-2}>
        <boxGeometry  args={[3,2,1]}/>
        <meshStandardMaterial color={color || "royalblue"}/>
    </mesh>
</RigidBody>

04

COLLIDERS

BALL COLLIDER

De huidige collider van de sphere is niet optimaal. In veel gevallen beïnvloedt dit de simulatie niet, tenzij nauwkeurigheid vereist is of het visueel relevant is. Cuboid-colliders zijn bovendien performanter. Je kan wel de collider van de RigidBody aanpassen.

Verander de collider op de RigidBody naar 'ball'

<RigidBody colliders="ball">
    <Sphere position-x={-2} position-y={4} scale={1} />     
</RigidBody>

04

COLLIDERS

HULL COLLIDER

Bij een Torus zijn cuboid- of ball-colliders niet geschikt. Een hull-collider volgt de contouren van het object zo nauwkeurig mogelijk, waarbij interne gaten worden genegeerd.

Verander onze box naar een Torus met een Hull Collider

<RigidBody colliders="hull">
    <mesh castShadow position={ [ 0, 1, - 0.25 ] } rotation={ [ Math.PI * 0.1, 0, 0 ] }>
        <torusGeometry args={ [ 1, 0.5, 16, 32 ] } />
        <meshStandardMaterial color="mediumpurple" />
    </mesh>
</RigidBody>

04

COLLIDERS

TRIMESH

Wil je perfecte matching met je model? Gebruik dan een Trimesh-collider. Deze volgt alle contouren en maakt een collider die precies past.

Verander de collider naar trimesh

<RigidBody colliders="trimesh">
    <mesh castShadow position={ [ -2, 1, - 0.25 ] } rotation={ [ Math.PI * 0.1, 0, 0 ] }>
        <torusGeometry args={ [ 1, 0.5, 16, 32 ] } />
        <meshStandardMaterial color="mediumpurple" />
    </mesh>
</RigidBody>

Let op: hull- en trimesh-colliders zijn zwaar en vragen veel berekeningen. Gebruik ze alleen als het echt moet. Ze zijn ook gevoeliger voor glitches bij snelle bewegingen.

04

COLLIDERS

TRIMESH

Wil je perfecte matching met je model? Gebruik dan een Trimesh-collider. Deze volgt alle contouren en maakt een collider die precies past.

Verander de collider naar trimesh

<RigidBody colliders="trimesh">
    <mesh castShadow position={ [ -2, 1, - 0.25 ] } rotation={ [ Math.PI * 0.1, 0, 0 ] }>
        <torusGeometry args={ [ 1, 0.5, 16, 32 ] } />
        <meshStandardMaterial color="mediumpurple" />
    </mesh>
</RigidBody>

Let op: hull- en trimesh-colliders zijn zwaar en vragen veel berekeningen. Gebruik ze alleen als het echt moet. Ze zijn ook gevoeliger voor glitches bij snelle bewegingen.

VOICE API + PHYSICS

CUSTOM COLLIDERS

05

05

CUSTOM COLLIDERS

CUSTOM

Rapier biedt meerdere standaard-colliders, maar je kunt ook zelf custom colliders bouwen.

05

CUSTOM COLLIDERS

CUBOID

We zullen nu custom colliders creëren en testen. Schakel de standaard-colliders op de RigidBody uit door deze op false te zetten en importeer de benodigde collider vanuit Rapier.

Plaats de collider op false en voeg je eigen CuboidCollider toe.

import { CuboidCollider, RigidBody, Physics } from '@react-three/rapier'

<RigidBody colliders={ false }>
    <CuboidCollider args={ [ 1, 1, 1 ] } />
    {/* ... */}
</RigidBody>

Onze kubus is een beetje off. Geen probleem: verander gewoon de grootte, positie en rotatie van de RigidBody.

<RigidBody colliders={false}  position-x={2} position-y={2} scale={1.5}>
     <CuboidCollider args={[.75,.75,.75]} />
     <Box ref={cubeRef}  scale={1.5}/> 
</RigidBody>

05

CUSTOM COLLIDERS

MULTIPLE

Het is mogelijk om meerdere colliders in één RigidBody te definiëren zonder extra meshes. Dit wordt bereikt door een aanvullende CuboidCollider toe te voegen.

Voeg een tweede CuboidCollider toe aan de RigidBody

<RigidBody colliders={ false } position={ [ 0, 1, - 0.25 ] } rotation={ [ Math.PI * 0.1, 0, 0 ] }>
    <CuboidCollider args={ [ 1.5, 1.5, 0.5 ] } />
    <CuboidCollider args={ [ 1, 1, 1 ] } />
    {/* ... */}
</RigidBody>

05

CUSTOM COLLIDERS

BALL

Je kunt in plaats van een Cuboid een Ball Collider gebruiken. Dit creëert een sphere rond het object en functioneert correct binnen de RigidBody.

Importeer de BallCollider en vervang de 2 CuboidColliders door 1 Ball Collider

<RigidBody colliders={false} position-x={2} position-y={2} scale={1.5}>
    <BallCollider args={ [ 1.5 ] } />
    {/* ... */}
</RigidBody>

Naast de standaard-colliders zijn er nog andere types die je zelf kunt toevoegen. Je kunt kiezen tussen handmatig plaatsen of automatisch laten genereren.
Meer info? Bekijk de docs

VOICE API + PHYSICS

FORCES

06

06

FORCES

FORCES

Onze RigidBodies vallen nu vanzelf, maar we kunnen ze ook beïnvloeden met ‘forces’. In dit voorbeeld laten we een kubus springen als we erop klikken.

We plaatsen onze scene terug naar het begin.

<RigidBody colliders="ball">
     <Sphere position-x={-2} position-y={2} scale={1} />     
</RigidBody>
<RigidBody>
     <Box ref={cubeRef} position-x={2} position-y={2} scale={1.5}/> 
</RigidBody>

06

FORCES

FORCES

Om krachten op de kubus te zetten, maken we eerst een reference. Voeg daarnaast een onClick-functie toe die de kubus activeert bij klikken.

Maak een cubeRef aan en plaats deze op de kubus.

import { useRef } from 'react'

export default function Experience()
{
    const cubeRef = useRef()
}

Voeg de reference op de RigidBody

<RigidBody ref={ cube } position={ [ 1.5, 2, 0 ] }>
    {/* ... */}
</RigidBody>

06

FORCES

FORCES

Maak een cubeJump function aan en zend deze naar de cube.

const cube = useRef()
const cubeJump = () =>
{
    console.log('jump!')
}

Voeg de reference op de RigidBody

<RigidBody ref={ cube } position={ [ 1.5, 2, 0 ] }>
    <mesh castShadow onClick={ cubeJump }>
        {/* ... */}
    </mesh>
</RigidBody>

Kijk nu eens wat er in de current reference zit in de console.

const cubeJump = () =>
{
    console.log(cube.current)
}

06

FORCES

IMPULSE

Plaats een applyImpulse op de kubus-reference in de cubeJump-functie om de sprong te simuleren.

const cubeJump = () =>
{
    cube.current.applyImpulse({ x: 0, y: 10, z: 0 })
}

Je kan forces toepassen via addForce of applyImpulse. addForce werkt langzaam, zoals wind. applyImpulse is een korte stoot, perfect voor een sprongetje.

06

FORCES

TORQUE

Voeg een applyTorqueImpulse op de reference in onze cubeJump function.

const cubeJump = () =>
{
    cubeRef.current.applyImpulse({ x: 0, y: 10, z: 0 })
    cubeRef.current.applyTorqueImpulse({ x: 0, y: 1, z: 0 })
}

Onze kubus beweegt nu alleen omhoog. Wil je dat hij ook draait bij een tik? Gebruik addTorque voor continue draaiing of applyTorqueImpulse voor een korte draai-impuls.

Gooi die torque lekker random over alle assen, dan ziet het er realistischer uit.

const cubeJump = () =>
{
   cubeRef.current.applyImpulse({ x: 0, y: 15, z: 0 })
   cubeRef.current.applyTorqueImpulse({ x: (Math.random() * 5) - 0.5, y: (Math.random() * 5) - 0.5, z: (Math.random() * 5) - 0.5 })
}

06

FORCES

USE THE FORCE

Nu kan je met forces spelen in React Three Rapier. Met deze trucs maak je je scene meteen een stuk leuker en interactiever. May the force be with you!

VOICE API + PHYSICS

OBJECT SETTINGS

07

SETTINGS

Er bestaan diverse manieren om het gedrag van objecten en hun onderlinge interacties te beheersen, zoals wrijving, restitutie, massa, zwaartekracht, posities, bewegingsrichtingen, en andere parameters.

Laten we eens kijken naar een paar belangrijke eigenschappen die je kunt aanpassen:

07

OBJECT SETTINGS

  • Gravity
  • Restitution
  • Friction
  • Mass
  • Position & rotation

07

OBJECT SETTINGS

GRAVITY

De standaardinstelling voor zwaartekracht is -9.81 om aardse omstandigheden te simuleren. Deze waarde kan worden aangepast via een Vector3-array toegewezen aan de gravity-eigenschap.

Verander de gravity naar die van de maan (-1.62).

<Physics debug gravity={ [ 0, - 1.62, 0 ] }>

Klik op de cube en je ziet dat hij hoger springt en trager valt. Experimenteer ook met het omkeren van de zwaartekracht of het toepassen op verschillende assen.

<Physics debug gravity={ [ 0, 1.62, 0 ] }>
<Physics debug gravity={ [ - 1.62, 0, 0 ] }>

07

OBJECT SETTINGS

GRAVITY SCALE

De gravityScale-eigenschap geeft aan in welke mate de zwaartekracht van toepassing is op een RigidBody. De standaardwaarde is 1.

Verander de gravityScale van de cube naar 0.2

<RigidBody ref={ cube } position={ [ 1.5, 2, 0 ] } gravityScale={ 0.2 }>
    {/* ... */}
</RigidBody>

Gooi er een negatieve waarde in en je object gaat omhoog in plaats van omlaag.

<RigidBody ref={ cube } position={ [ 1.5, 2, 0 ] } gravityScale={ - 0.2 }>
    {/* ... */}
</RigidBody>

Deze aanpassing is praktisch in scenario’s waarin verschillende gebieden in de scene verschillende zwaartekrachteffecten vereisen.

07

OBJECT SETTINGS

RESTITUTION

De objecten volgen de zwaartekracht, maar vertonen geen stuiterend gedrag. De restitution-eigenschap regelt de veerkracht; de standaardwaarde is 0.

Zet een restitution van 1 op de cube en zie wat gebeurd.

<RigidBody
    ref={ cube }
    position={ [ 1.5, 2, 0 ] }
    gravityScale={ 1 }
    restitution={ 1 }
>
    {/* ... */}
</RigidBody>

07

OBJECT SETTINGS

RESTITUTION

De kubus stuitert, maar bereikt niet de startpositie. Voeg een restitution-waarde toe aan de vloer toe om dit te corrigeren. Hard oppervlak vs. zacht oppervlak beïnvloedt de bounce.

Pas de restitution van de floor aan naar 1 en observeer het effect. (Bouncy Castle)

<RigidBody
    ref={ cube }
    position={ [ 1.5, 2, 0 ] }
    gravityScale={ 1 }
    restitution={ 1 }
>
    {/* ... */}
</RigidBody>

<RigidBody type="fixed" restitution={ 1 }>
    {/* ... */}
</RigidBody>

07

OBJECT SETTINGS

FRICTION

De friction-eigenschap bepaalt de weerstand tussen oppervlakken. Een hogere waarde veroorzaakt snellere afremming, terwijl een lagere waarde langere bewegingen mogelijk maakt. Standaard: 0.7.

Vooraleer we dit doen zullen we eerst de resitution terug op 0 plaatsen van de floor.

<RigidBody
    ref={ cube }
    position={ [ 1.5, 2, 0 ] }
    gravityScale={ 1 }
    restitution={ 0 }
>
    {/* ... */}
</RigidBody>

<RigidBody type="fixed" restitution={ 0 }>
    {/* ... */}
</RigidBody>

07

OBJECT SETTINGS

FRICTION

Verander de friction van beide objecten op 0 en kijk wat er gebeurt.

<RigidBody
    ref={ cube }
    position={ [ 1.5, 2, 0 ] }
    gravityScale={ 1 }
    restitution={ 0 }
    friction={ 0 }
>
    {/* ... */}
</RigidBody>

<RigidBody
    type="fixed"
    restitution={ 0 }
    friction={ 0 }
>
    {/* ... */}
</RigidBody>

Klik op het object om te springen en observeer het effect. Cool!
Pas daarna de friction aan naar 0.7 of verwijder deze.

07

OBJECT SETTINGS

MASS

<RigidBody
    ref={ cube }
    position={ [ 1.5, 2, 0 ] }
    gravityScale={ 1 }
    restitution={ 0 }
    friction={ 0.7 }
    colliders={ false }
    position-x={2} 
    position-y={2}
>
    {/* ... */}
    <CuboidCollider args={ [ 1, 1, 1 ] }  />
    <Box onClick={cubeJump} scale={2}/> 
</RigidBody>

De massa van een RigidBody wordt bepaald door de geometrie en het volume van het object. Grotere massa verhoogt de impact bij botsingen, maar verandert de valversnelling niet.

Voor het aanpassen van de massa dient het object te zijn voorzien van een collider. Hier gebruiken we een CuboidCollider.

07

OBJECT SETTINGS

MASS

<CuboidCollider mass={ 0.5 } args={ [ 0.5, 0.5, 0.5 ] } />

De massa kan worden aangepast via het mass-attribuut van de betreffende collider.

Pas de mass aan naar 0.5 en klik eens op het object.

<CuboidCollider mass={ 2 } args={ [ 0.5, 0.5, 0.5 ] } />

Pas de mass aan naar 2 en klik eens op het object.

07

OBJECT SETTINGS

ACCESS MASS

const cubeJump = () =>
{
    const mass = cube.current.mass()
    console.log(mass)

}

Omdat onze massa nu hoger is, is de kracht van de toegepaste impulse niet sterk genoeg. Voor leerdoeleinden willen we de kracht van de impulse aanpassen, zodat het object ongeacht de massa even hoog springt.
Hiervoor moeten we de massa uit onze reference halen.

In onze cubeJump-functie halen we de massa uit de reference.

const cubeJump = () =>
{
    const mass = cube.current.mass()
    cube.current.applyImpulse({ x: 0, y: 10 * mass, z: 0 })
}

Vermenigvuldig de impulse met de massa om de kracht aan te passen

07

OBJECT SETTINGS

POSITION & ROTATION

Op onze RigidBody kunnen we ook de positie en rotatie aanpassen, zoals we eerder hebben gezien.
Let op: bij dynamische en fixed objecten mag je deze waarden tijdens runtime niet veranderen, omdat dit ongewenst gedrag kan veroorzaken.
Als je tijdens runtime updates nodig hebt, gebruik dan kinematic types.

VOICE API + PHYSICS

TYPES

08

KINEMATIC

We hebben het fixed type gezien voor de ground en het dynamic type voor objecten zoals de Ball en Cube. Er is echter nog een derde type: kinematic. Dit type gebruik je vooral als je iets tijdens runtime wilt bewegen of roteren.

Creëer een lange box die we gaan gebruiken als een draaiende balk in de scene.

08

TYPES

<Physics debug gravity={ [ 0, - 9.81, 0 ] }>

    {/* ... */}
    <RigidBody
        position={ [ 0, - 0.8, 0 ] }
        friction={ 0 }
        type="kinematicPosition"
    >
        <mesh castShadow scale={ [ 0.4, 0.4, 4 ] }>
            <boxGeometry />
            <meshStandardMaterial color="red" />
        </mesh>
    </RigidBody>
</Physics>

08

TYPES

KINEMATIC

We gaan de positie handmatig aanpassen, dus hebben we een reference naar ons object nodig.

Creëer een reference en voeg deze toe aan de RigidBody van de draaiende balk.

export default function Experience()
{
    const spinnerRef = useRef()
	
    <RigidBody
    ref={ spinnerRef }
    position={ [ 0, - 0.8, 0 ] }
    friction={ 0 }
    type="kinematicPosition"
>
    {/* ... */}
</RigidBody>
   
}

08

TYPES

KINEMATIC

We zullen onze balk elke seconde aanpassen. Hier moet meteen een belletje rinkelen: dit is een perfect moment om useFrame te gebruiken.

Importeer useFrame en haal de state ervan op.

import { useFrame } from '@react-three/fiber'

export default function Experience()
{
    useFrame((state) =>
    {
        
    })
}

We halen onze tijd op via de clock om onze balk te laten draaien a.d.h.v de tijd.

const time = state.clock.getElapsedTime()
console.log("Time:", time)

08

TYPES

KINEMATIC

Voor het aanpassen van rotatie en positie van een kinematic object dienen de methodes setNextKinematicRotation en setNextKinematicTranslation te worden gebruikt. Omdat rotatie in quaternion-vorm vereist is, wordt een Euler-conversie uitgevoerd.

Importeer THREE en plaats volgende code in de useFrame

import * as THREE from 'three'

useFrame((state) =>
{
    const time = state.clock.getElapsedTime()

    const eulerRotation = new THREE.Euler(0, time, 0)
    const quaternionRotation = new THREE.Quaternion()
    quaternionRotation.setFromEuler(eulerRotation)
})

08

TYPES

KINEMATIC

Zet de Kinematic Rotation van de ref op de rotatie die we hebben klaargemaakt.

useFrame((state) =>
{
    const time = state.clock.getElapsedTime()

    const eulerRotation = new THREE.Euler(0, time, 0)
    const quaternionRotation = new THREE.Quaternion()
    quaternionRotation.setFromEuler(eulerRotation)
    twister.current.setNextKinematicRotation(quaternionRotation)
})

De balk draait nu ... wil je deze sneller? Vermenigvuldig dan de tijd met een groter getal.

useFrame((state) =>
{
    const time = state.clock.getElapsedTime()

    const eulerRotation = new THREE.Euler(0, time * 4, 0)
})

08

TYPES

KINEMATIC

Maak een angle aan a.d.h.v de tijd en gebruik deze om de cos en sin te berekenen.

useFrame((state) =>
{
    const angle = time * 0.5
    const x = Math.cos(angle)
    const z = Math.sin(angle)
})

Nu gaan we de positie aanpassen zodat onze balk rond de vloer in een cirkel draait. Hiervoor gebruiken we functies zoals cos voor de x-as en sin voor de z-as.

Nu kan je makkelijk de translation aanpassen door deze aan de  x en z mee te geven.

useFrame((state) =>
{
    // ...
    twister.current.setNextKinematicTranslation({ x: x, y: - 0.8, z: z })
})

Wil je een grotere cirkel? Vermenigvuldig gewoon een groter getal bij de cos en sin.

VOICE API + PHYSICS

EVENTS

09

EVENTS

We kunnen luisteren naar bepaalde events op onze RigidBodies, wat het interessant maakt om reacties te programmeren op basis van deze events.

De events die je kunt gebruiken:

  • onCollisionEnter – wordt getriggerd bij aanraking.

  • onCollisionExit – wordt getriggerd bij het verlaten van contact.

  • onSleep – start als de RigidBody een tijd geen contact heeft.

  • onWake – start wanneer de RigidBody weer actief contact maakt.

09

EVENTS

Laten we elk event stap voor stap onderzoeken om te begrijpen hoe ze werken.

09

EVENTS

ENTER

We zullen ervoor zorgen dat er een klein geluidje wordt afgespeeld wanneer een collision plaatsvindt met een bepaald object.

Implementeer een collisionEnter-functie in de Experience en wijs deze toe aan het onCollisionEnter-attribuut van de betreffende RigidBody.

export default function Experience()
{
    const collisionEnter = () =>
    {
        console.log('collision!')
    }
    
<RigidBody
    ref={ cube }
    position={ [ 1.5, 2, 0 ] }
    gravityScale={ 1 }
    restitution={ 0 }
    friction={ 0.7 }
    colliders={ false }
    onCollisionEnter={ collisionEnter }
>

09

EVENTS

ENTER

Om onze sound af te spelen, moeten we deze natuurlijk eerst aanmaken.

import { useState, useRef } from 'react'

export default function Experience()
{
    const [ hitSound ] = useState(() => new Audio('./oof.mp3'))

    // ...
}

Gebruik nu deze audio file in onze collisionEnter function.

const collisionEnter = () =>
{
    hitSound.currentTime = 0.5
    hitSound.volume = Math.random()
    hitSound.play()
}

Ga terug naar je scene en luister. Hoor je niets? Klik dan eerst eens op je scene.

09

EVENTS

EXIT

Implementeer een console.log in de onCollisionExit-handler om de gebeurtenis te registreren.

<RigidBody
    ref={ cube }
    position={ [ 1.5, 2, 0 ] }
    gravityScale={ 1 }
    restitution={ 0 }
    friction={ 0.7 }
    colliders={ false }
    onCollisionEnter={ collisionEnter }
    onCollisionExit={ () => { console.log('exit') } }
>
    {/* ... */}
</RigidBody>

onCollisionExit wordt getriggerd wanneer een object geen contact meer heeft met het betreffende object.

09

EVENTS

ONSLEEP/WAKE

Geef nu een console mee op de sleep en wake function en zie wat er gebeurd in de console.

<RigidBody
    ref={ cube }
    position={ [ 1.5, 2, 0 ] }
    gravityScale={ 1 }
    restitution={ 0 }
    friction={ 0.7 }
    colliders={ false }
    onCollisionEnter={ collisionEnter }
    onCollisionExit={ () => { console.log('exit') } }
    onSleep={ () => { console.log('sleep') } }
    onWake={ () => { console.log('wake') } }
>
		{/* ... */}
</RigidBody>

Objecten die voor een bepaalde periode in rust zijn, worden in een sleep-modus geplaatst. Ze blijven in deze toestand totdat een kracht wordt uitgeoefend, waarna ze de sleep-modus verlaten (wake).

09

EVENTS

CONCLUSION

ENTER

EXIT

SLEEP/WAKE

WEB6/5 - Voice API + Physics

By Niels Minne

WEB6/5 - Voice API + Physics

  • 89