Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Official Expo team skill for building native UI in Expo apps, fine-tuned for Opus models and usable with Claude Code or Cursor.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/webgpu-three.md
1# WebGPU & Three.js for Expo23**Use this skill for ANY 3D graphics, games, GPU compute, or Three.js features in React Native.**45## Locked Versions (Tested & Working)67```json8{9"react-native-wgpu": "^0.4.1",10"three": "0.172.0",11"@react-three/fiber": "^9.4.0",12"wgpu-matrix": "^3.0.2",13"@types/three": "0.172.0"14}15```1617**Critical:** These versions are tested together. Mismatched versions cause type errors and runtime issues.1819## Installation2021```bash22npm install react-native-wgpu@^0.4.1 [email protected] @react-three/fiber@^9.4.0 wgpu-matrix@^3.0.2 @types/[email protected] --legacy-peer-deps23```2425**Note:** `--legacy-peer-deps` may be required due to peer dependency conflicts with canary Expo versions.2627## Metro Configuration2829Create `metro.config.js` in project root:3031```js32const { getDefaultConfig } = require("expo/metro-config");3334const config = getDefaultConfig(__dirname);3536config.resolver.resolveRequest = (context, moduleName, platform) => {37// Force 'three' to webgpu build38if (moduleName.startsWith("three")) {39moduleName = "three/webgpu";40}4142// Use standard react-three/fiber instead of React Native version43if (platform !== "web" && moduleName.startsWith("@react-three/fiber")) {44return context.resolveRequest(45{46...context,47unstable_conditionNames: ["module"],48mainFields: ["module"],49},50moduleName,51platform52);53}54return context.resolveRequest(context, moduleName, platform);55};5657module.exports = config;58```5960## Required Lib Files6162Create these files in `src/lib/`:6364### 1. make-webgpu-renderer.ts6566```ts67import type { NativeCanvas } from "react-native-wgpu";68import * as THREE from "three/webgpu";6970export class ReactNativeCanvas {71constructor(private canvas: NativeCanvas) {}7273get width() {74return this.canvas.width;75}76get height() {77return this.canvas.height;78}79set width(width: number) {80this.canvas.width = width;81}82set height(height: number) {83this.canvas.height = height;84}85get clientWidth() {86return this.canvas.width;87}88get clientHeight() {89return this.canvas.height;90}91set clientWidth(width: number) {92this.canvas.width = width;93}94set clientHeight(height: number) {95this.canvas.height = height;96}9798addEventListener(_type: string, _listener: EventListener) {}99removeEventListener(_type: string, _listener: EventListener) {}100dispatchEvent(_event: Event) {}101setPointerCapture() {}102releasePointerCapture() {}103}104105export const makeWebGPURenderer = (106context: GPUCanvasContext,107{ antialias = true }: { antialias?: boolean } = {}108) =>109new THREE.WebGPURenderer({110antialias,111// @ts-expect-error112canvas: new ReactNativeCanvas(context.canvas),113context,114});115```116117### 2. fiber-canvas.tsx118119```tsx120import * as THREE from "three/webgpu";121import React, { useEffect, useRef } from "react";122import type { ReconcilerRoot, RootState } from "@react-three/fiber";123import {124extend,125createRoot,126unmountComponentAtNode,127events,128} from "@react-three/fiber";129import type { ViewProps } from "react-native";130import { PixelRatio } from "react-native";131import { Canvas, type CanvasRef } from "react-native-wgpu";132133import {134makeWebGPURenderer,135ReactNativeCanvas,136} from "@/lib/make-webgpu-renderer";137138// Extend THREE namespace for R3F - add all components you use139extend({140AmbientLight: THREE.AmbientLight,141DirectionalLight: THREE.DirectionalLight,142PointLight: THREE.PointLight,143SpotLight: THREE.SpotLight,144Mesh: THREE.Mesh,145Group: THREE.Group,146Points: THREE.Points,147BoxGeometry: THREE.BoxGeometry,148SphereGeometry: THREE.SphereGeometry,149CylinderGeometry: THREE.CylinderGeometry,150ConeGeometry: THREE.ConeGeometry,151DodecahedronGeometry: THREE.DodecahedronGeometry,152BufferGeometry: THREE.BufferGeometry,153BufferAttribute: THREE.BufferAttribute,154MeshStandardMaterial: THREE.MeshStandardMaterial,155MeshBasicMaterial: THREE.MeshBasicMaterial,156PointsMaterial: THREE.PointsMaterial,157PerspectiveCamera: THREE.PerspectiveCamera,158Scene: THREE.Scene,159});160161interface FiberCanvasProps {162children: React.ReactNode;163style?: ViewProps["style"];164camera?: THREE.PerspectiveCamera;165scene?: THREE.Scene;166}167168export const FiberCanvas = ({169children,170style,171scene,172camera,173}: FiberCanvasProps) => {174const root = useRef<ReconcilerRoot<OffscreenCanvas>>(null!);175const canvasRef = useRef<CanvasRef>(null);176177useEffect(() => {178const context = canvasRef.current!.getContext("webgpu")!;179const renderer = makeWebGPURenderer(context);180181// @ts-expect-error - ReactNativeCanvas wraps native canvas182const canvas = new ReactNativeCanvas(context.canvas) as HTMLCanvasElement;183canvas.width = canvas.clientWidth * PixelRatio.get();184canvas.height = canvas.clientHeight * PixelRatio.get();185const size = {186top: 0,187left: 0,188width: canvas.clientWidth,189height: canvas.clientHeight,190};191192if (!root.current) {193root.current = createRoot(canvas);194}195root.current.configure({196size,197events,198scene,199camera,200gl: renderer,201frameloop: "always",202dpr: 1,203onCreated: async (state: RootState) => {204// @ts-expect-error - WebGPU renderer has init method205await state.gl.init();206const renderFrame = state.gl.render.bind(state.gl);207state.gl.render = (s: THREE.Scene, c: THREE.Camera) => {208renderFrame(s, c);209context?.present();210};211},212});213root.current.render(children);214return () => {215if (canvas != null) {216unmountComponentAtNode(canvas!);217}218};219});220221return <Canvas ref={canvasRef} style={style} />;222};223```224225## Basic 3D Scene226227```tsx228import * as THREE from "three/webgpu";229import { View } from "react-native";230import { useRef } from "react";231import { useFrame, useThree } from "@react-three/fiber";232import { FiberCanvas } from "@/lib/fiber-canvas";233234function RotatingBox() {235const ref = useRef<THREE.Mesh>(null!);236237useFrame((_, delta) => {238ref.current.rotation.x += delta;239ref.current.rotation.y += delta * 0.5;240});241242return (243<mesh ref={ref}>244<boxGeometry args={[1, 1, 1]} />245<meshStandardMaterial color="hotpink" />246</mesh>247);248}249250function Scene() {251const { camera } = useThree();252253useEffect(() => {254camera.position.set(0, 2, 5);255camera.lookAt(0, 0, 0);256}, [camera]);257258return (259<>260<ambientLight intensity={0.5} />261<directionalLight position={[10, 10, 5]} intensity={1} />262<RotatingBox />263</>264);265}266267export default function App() {268return (269<View style={{ flex: 1 }}>270<FiberCanvas style={{ flex: 1 }}>271<Scene />272</FiberCanvas>273</View>274);275}276```277278## Lazy Loading (Recommended)279280Use React.lazy to code-split Three.js for better loading:281282```tsx283import React, { Suspense } from "react";284import { ActivityIndicator, View } from "react-native";285286const Scene = React.lazy(() => import("@/components/scene"));287288export default function Page() {289return (290<View style={{ flex: 1 }}>291<Suspense fallback={<ActivityIndicator size="large" />}>292<Scene />293</Suspense>294</View>295);296}297```298299## Common Geometries300301```tsx302// Box303<mesh>304<boxGeometry args={[width, height, depth]} />305<meshStandardMaterial color="red" />306</mesh>307308// Sphere309<mesh>310<sphereGeometry args={[radius, widthSegments, heightSegments]} />311<meshStandardMaterial color="blue" />312</mesh>313314// Cylinder315<mesh>316<cylinderGeometry args={[radiusTop, radiusBottom, height, segments]} />317<meshStandardMaterial color="green" />318</mesh>319320// Cone321<mesh>322<coneGeometry args={[radius, height, segments]} />323<meshStandardMaterial color="yellow" />324</mesh>325```326327## Lighting328329```tsx330// Ambient (uniform light everywhere)331<ambientLight intensity={0.5} />332333// Directional (sun-like)334<directionalLight position={[10, 10, 5]} intensity={1} />335336// Point (light bulb)337<pointLight position={[0, 5, 0]} intensity={2} distance={10} />338339// Spot (flashlight)340<spotLight position={[0, 10, 0]} angle={0.3} penumbra={1} intensity={2} />341```342343## Animation with useFrame344345```tsx346import { useFrame } from "@react-three/fiber";347import { useRef } from "react";348import * as THREE from "three/webgpu";349350function AnimatedMesh() {351const ref = useRef<THREE.Mesh>(null!);352353// Runs every frame - delta is time since last frame354useFrame((state, delta) => {355// Rotate356ref.current.rotation.y += delta;357358// Oscillate position359ref.current.position.y = Math.sin(state.clock.elapsedTime) * 2;360});361362return (363<mesh ref={ref}>364<boxGeometry />365<meshStandardMaterial color="orange" />366</mesh>367);368}369```370371## Particle Systems372373```tsx374import * as THREE from "three/webgpu";375import { useRef, useEffect } from "react";376import { useFrame } from "@react-three/fiber";377378function Particles({ count = 500 }) {379const ref = useRef<THREE.Points>(null!);380const positions = useRef<Float32Array>(new Float32Array(count * 3));381382useEffect(() => {383for (let i = 0; i < count; i++) {384positions.current[i * 3] = (Math.random() - 0.5) * 50;385positions.current[i * 3 + 1] = (Math.random() - 0.5) * 50;386positions.current[i * 3 + 2] = (Math.random() - 0.5) * 50;387}388}, [count]);389390useFrame((_, delta) => {391// Animate particles392for (let i = 0; i < count; i++) {393positions.current[i * 3 + 1] -= delta * 2;394if (positions.current[i * 3 + 1] < -25) {395positions.current[i * 3 + 1] = 25;396}397}398ref.current.geometry.attributes.position.needsUpdate = true;399});400401return (402<points ref={ref}>403<bufferGeometry>404<bufferAttribute405attach="attributes-position"406args={[positions.current, 3]}407/>408</bufferGeometry>409<pointsMaterial color="#ffffff" size={0.2} sizeAttenuation />410</points>411);412}413```414415## Touch Controls (Orbit)416417See the full `orbit-controls.tsx` implementation in the lib files. Usage:418419```tsx420import { View } from "react-native";421import { FiberCanvas } from "@/lib/fiber-canvas";422import useControls from "@/lib/orbit-controls";423424function Scene() {425const [OrbitControls, events] = useControls();426427return (428<View style={{ flex: 1 }} {...events}>429<FiberCanvas style={{ flex: 1 }}>430<OrbitControls />431{/* Your 3D content */}432</FiberCanvas>433</View>434);435}436```437438## Common Issues & Solutions439440### 1. "X is not part of the THREE namespace"441442**Problem:** Error like `AmbientLight is not part of the THREE namespace`443444**Solution:** Add the missing component to the `extend()` call in fiber-canvas.tsx:445446```tsx447extend({448AmbientLight: THREE.AmbientLight,449// Add other missing components...450});451```452453### 2. TypeScript Errors with Three.js454455**Problem:** Type mismatches between three.js and R3F456457**Solution:** Use `@ts-expect-error` comments where needed:458459```tsx460// @ts-expect-error - WebGPU renderer types don't match461await state.gl.init();462```463464### 3. Blank Screen465466**Problem:** Canvas renders but nothing visible467468**Solution:**4694701. Ensure camera is positioned correctly and looking at scene4712. Add lighting (objects are black without light)4723. Check that `extend()` includes all components used473474### 4. Performance Issues475476**Problem:** Low frame rate or stuttering477478**Solution:**479480- Reduce polygon count in geometries481- Use `useMemo` for static data482- Limit particle count483- Use `instancedMesh` for many identical objects484485### 5. Peer Dependency Errors486487**Problem:** npm install fails with ERESOLVE488489**Solution:** Use `--legacy-peer-deps`:490491```bash492npm install <packages> --legacy-peer-deps493```494495## Building496497WebGPU requires a custom build:498499```bash500npx expo prebuild501npx expo run:ios502```503504**Note:** WebGPU does NOT work in Expo Go.505506## File Structure507508```509src/510├── app/511│ └── index.tsx # Entry point with lazy loading512├── components/513│ ├── scene.tsx # Main 3D scene514│ └── game.tsx # Game logic515└── lib/516├── fiber-canvas.tsx # R3F canvas wrapper517├── make-webgpu-renderer.ts # WebGPU renderer518└── orbit-controls.tsx # Touch controls519```520521## Decision Tree522523```524Need 3D graphics?525├── Simple shapes → mesh + geometry + material526├── Animated objects → useFrame + refs527├── Many objects → instancedMesh528├── Particles → Points + BufferGeometry529│530Need interaction?531├── Orbit camera → useControls hook532├── Touch objects → onClick on mesh533├── Gestures → react-native-gesture-handler534│535Performance critical?536├── Static geometry → useMemo537├── Many instances → InstancedMesh538└── Complex scenes → LOD (Level of Detail)539```540541## Example: Complete Game Scene542543```tsx544import * as THREE from "three/webgpu";545import { View, Text, Pressable } from "react-native";546import { useRef, useState, useCallback } from "react";547import { useFrame, useThree } from "@react-three/fiber";548import { FiberCanvas } from "@/lib/fiber-canvas";549550function Player({ position }: { position: THREE.Vector3 }) {551const ref = useRef<THREE.Mesh>(null!);552553useFrame(() => {554ref.current.position.copy(position);555});556557return (558<mesh ref={ref}>559<coneGeometry args={[0.5, 1, 8]} />560<meshStandardMaterial color="#00ffff" />561</mesh>562);563}564565function GameScene({ playerX }: { playerX: number }) {566const { camera } = useThree();567const playerPos = useRef(new THREE.Vector3(0, 0, 0));568569playerPos.current.x = playerX;570571useEffect(() => {572camera.position.set(0, 10, 15);573camera.lookAt(0, 0, 0);574}, [camera]);575576return (577<>578<ambientLight intensity={0.5} />579<directionalLight position={[5, 10, 5]} />580<Player position={playerPos.current} />581</>582);583}584585export default function Game() {586const [playerX, setPlayerX] = useState(0);587588return (589<View style={{ flex: 1, backgroundColor: "#000" }}>590<FiberCanvas style={{ flex: 1 }}>591<GameScene playerX={playerX} />592</FiberCanvas>593594<View style={{ position: "absolute", bottom: 40, flexDirection: "row" }}>595<Pressable onPress={() => setPlayerX((x) => x - 1)}>596<Text style={{ color: "#fff", fontSize: 32 }}>◀</Text>597</Pressable>598<Pressable onPress={() => setPlayerX((x) => x + 1)}>599<Text style={{ color: "#fff", fontSize: 32 }}>▶</Text>600</Pressable>601</View>602</View>603);604}605```606