react-three-game 0.0.49 → 0.0.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -163,9 +163,13 @@ Keys: **T**ranslate / **R**otate / **S**cale. Drag tree nodes to reparent. Impor
163
163
  ## Tree Utilities
164
164
 
165
165
  ```typescript
166
- import { findNode, updateNode, deleteNode, cloneNode } from 'react-three-game';
166
+ import { findNode, updateNode, updateNodeById, deleteNode, cloneNode, exportGLBData } from 'react-three-game';
167
167
 
168
- const updated = updateNode(root, nodeId, n => ({ ...n, disabled: true }));
168
+ const node = findNode(root, nodeId);
169
+ const updated = updateNode(root, nodeId, n => ({ ...n, disabled: true })); // or updateNodeById
170
+ const afterDelete = deleteNode(root, nodeId);
171
+ const cloned = cloneNode(node);
172
+ const glbData = await exportGLBData(sceneRoot); // export scene to GLB ArrayBuffer
169
173
  ```
170
174
 
171
175
  ## Development
@@ -1,8 +1,10 @@
1
1
  import { Group, Matrix4, Object3D, Texture } from "three";
2
2
  import { ThreeEvent } from "@react-three/fiber";
3
3
  import { Prefab, GameObject as GameObjectType } from "./types";
4
+ import type { RapierRigidBody } from "@react-three/rapier";
4
5
  export interface PrefabRootRef {
5
6
  root: Group | null;
7
+ rigidBodyRefs: Map<string, RapierRigidBody | null>;
6
8
  }
7
9
  export declare const PrefabRoot: import("react").ForwardRefExoticComponent<{
8
10
  editMode?: boolean;
@@ -20,6 +22,7 @@ interface RendererProps {
20
22
  onSelect?: (id: string) => void;
21
23
  onClick?: (event: ThreeEvent<PointerEvent>, entity: GameObjectType) => void;
22
24
  registerRef: (id: string, obj: Object3D | null) => void;
25
+ registerRigidBodyRef: (id: string, rb: RapierRigidBody | null) => void;
23
26
  loadedModels: Record<string, Object3D>;
24
27
  loadedTextures: Record<string, Texture>;
25
28
  editMode?: boolean;
@@ -30,16 +30,21 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
30
30
  const [textures, setTextures] = useState({});
31
31
  const loading = useRef(new Set());
32
32
  const objectRefs = useRef({});
33
+ const rigidBodyRefs = useRef(new Map());
33
34
  const [selectedObject, setSelectedObject] = useState(null);
34
35
  const rootRef = useRef(null);
35
36
  useImperativeHandle(ref, () => ({
36
- root: rootRef.current
37
+ root: rootRef.current,
38
+ rigidBodyRefs: rigidBodyRefs.current
37
39
  }), []);
38
40
  const registerRef = useCallback((id, obj) => {
39
41
  objectRefs.current[id] = obj;
40
42
  if (id === selectedId)
41
43
  setSelectedObject(obj);
42
44
  }, [selectedId]);
45
+ const registerRigidBodyRef = useCallback((id, rb) => {
46
+ rigidBodyRefs.current.set(id, rb);
47
+ }, []);
43
48
  useEffect(() => {
44
49
  const originalError = console.error;
45
50
  console.error = (...args) => {
@@ -103,7 +108,7 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
103
108
  });
104
109
  });
105
110
  }, [data, models, textures]);
106
- return (_jsxs("group", { ref: rootRef, children: [_jsx(GameInstanceProvider, { models: models, selectedId: selectedId, editMode: editMode, onSelect: editMode ? onSelect : undefined, registerRef: registerRef, children: _jsx(GameObjectRenderer, { gameObject: data.root, selectedId: selectedId, onSelect: editMode ? onSelect : undefined, onClick: onClick, registerRef: registerRef, loadedModels: models, loadedTextures: textures, editMode: editMode, parentMatrix: IDENTITY }) }), editMode && (_jsxs(_Fragment, { children: [_jsx(MapControls, { makeDefault: true }), selectedObject && (_jsx(TransformControls, { object: selectedObject, mode: transformMode, space: "local", onObjectChange: onTransformChange, translationSnap: snapResolution > 0 ? snapResolution : undefined, rotationSnap: snapResolution > 0 ? snapResolution : undefined, scaleSnap: snapResolution > 0 ? snapResolution : undefined }, `transform-${snapResolution}`))] }))] }));
111
+ return (_jsxs("group", { ref: rootRef, children: [_jsx(GameInstanceProvider, { models: models, selectedId: selectedId, editMode: editMode, onSelect: editMode ? onSelect : undefined, registerRef: registerRef, children: _jsx(GameObjectRenderer, { gameObject: data.root, selectedId: selectedId, onSelect: editMode ? onSelect : undefined, onClick: onClick, registerRef: registerRef, registerRigidBodyRef: registerRigidBodyRef, loadedModels: models, loadedTextures: textures, editMode: editMode, parentMatrix: IDENTITY }) }), editMode && (_jsxs(_Fragment, { children: [_jsx(MapControls, { makeDefault: true }), selectedObject && (_jsx(TransformControls, { object: selectedObject, mode: transformMode, space: "local", onObjectChange: onTransformChange, translationSnap: snapResolution > 0 ? snapResolution : undefined, rotationSnap: snapResolution > 0 ? snapResolution : undefined, scaleSnap: snapResolution > 0 ? snapResolution : undefined }, `transform-${snapResolution}`))] }))] }));
107
112
  });
108
113
  export function GameObjectRenderer(props) {
109
114
  var _a, _b, _c;
@@ -160,7 +165,7 @@ function InstancedNode({ gameObject, parentMatrix = IDENTITY, editMode, register
160
165
  }
161
166
  return (_jsx(GameInstance, { id: gameObject.id, modelUrl: (_k = (_j = (_h = gameObject.components) === null || _h === void 0 ? void 0 : _h.model) === null || _j === void 0 ? void 0 : _j.properties) === null || _k === void 0 ? void 0 : _k.filename, position: worldPosition, rotation: worldRotation, scale: worldScale, physics: physicsProps }));
162
167
  }
163
- function StandardNode({ gameObject, selectedId, onSelect, onClick, registerRef, loadedModels, loadedTextures, editMode, parentMatrix = IDENTITY, }) {
168
+ function StandardNode({ gameObject, selectedId, onSelect, onClick, registerRef, registerRigidBodyRef, loadedModels, loadedTextures, editMode, parentMatrix = IDENTITY, }) {
164
169
  var _a, _b, _c, _d, _e, _f;
165
170
  const groupRef = useRef(null);
166
171
  const helperRef = useRef(null);
@@ -193,12 +198,12 @@ function StandardNode({ gameObject, selectedId, onSelect, onClick, registerRef,
193
198
  const physicsDef = hasPhysics ? getComponent("Physics") : null;
194
199
  const isInstanced = (_e = (_d = (_c = gameObject.components) === null || _c === void 0 ? void 0 : _c.model) === null || _d === void 0 ? void 0 : _d.properties) === null || _e === void 0 ? void 0 : _e.instanced;
195
200
  const physicsKey = `physics_${gameObject.id}_${isInstanced ? 'instanced' : 'standard'}`;
196
- const inner = (_jsxs("group", { onPointerDown: editMode ? onDown : undefined, onPointerMove: editMode ? () => (clickValid.current = false) : undefined, onPointerUp: editMode ? onUp : undefined, children: [renderCoreNode(gameObject, { loadedModels, loadedTextures, editMode, registerRef }, parentMatrix), (_f = gameObject.children) === null || _f === void 0 ? void 0 : _f.map(child => (_jsx(GameObjectRenderer, { gameObject: child, selectedId: selectedId, onSelect: onSelect, onClick: onClick, registerRef: registerRef, loadedModels: loadedModels, loadedTextures: loadedTextures, editMode: editMode, parentMatrix: world }, child.id)))] }));
201
+ const inner = (_jsxs("group", { onPointerDown: editMode ? onDown : undefined, onPointerMove: editMode ? () => (clickValid.current = false) : undefined, onPointerUp: editMode ? onUp : undefined, children: [renderCoreNode(gameObject, { loadedModels, loadedTextures, editMode, registerRef }, parentMatrix), (_f = gameObject.children) === null || _f === void 0 ? void 0 : _f.map(child => (_jsx(GameObjectRenderer, { gameObject: child, selectedId: selectedId, onSelect: onSelect, onClick: onClick, registerRef: registerRef, registerRigidBodyRef: registerRigidBodyRef, loadedModels: loadedModels, loadedTextures: loadedTextures, editMode: editMode, parentMatrix: world }, child.id)))] }));
197
202
  if (editMode) {
198
- return (_jsxs(_Fragment, { children: [_jsx("group", { ref: groupRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: _jsx("mesh", { visible: false, children: _jsx("boxGeometry", { args: [0.01, 0.01, 0.01] }) }) }), _jsx("group", { ref: helperRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: inner }), hasPhysics && (physicsDef === null || physicsDef === void 0 ? void 0 : physicsDef.View) ? (_jsx(physicsDef.View, { properties: physics.properties, position: transform.position, rotation: transform.rotation, scale: transform.scale, editMode: editMode, children: inner }, physicsKey)) : null] }));
203
+ return (_jsxs(_Fragment, { children: [_jsx("group", { ref: groupRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: _jsx("mesh", { visible: false, children: _jsx("boxGeometry", { args: [0.01, 0.01, 0.01] }) }) }), _jsx("group", { ref: helperRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: inner }), hasPhysics && (physicsDef === null || physicsDef === void 0 ? void 0 : physicsDef.View) ? (_jsx(physicsDef.View, { properties: physics.properties, position: transform.position, rotation: transform.rotation, scale: transform.scale, editMode: editMode, nodeId: gameObject.id, registerRigidBodyRef: registerRigidBodyRef, children: inner }, physicsKey)) : null] }));
199
204
  }
200
205
  if (hasPhysics && (physicsDef === null || physicsDef === void 0 ? void 0 : physicsDef.View)) {
201
- return (_jsx(physicsDef.View, { properties: physics.properties, position: transform.position, rotation: transform.rotation, scale: transform.scale, editMode: editMode, children: inner }, physicsKey));
206
+ return (_jsx(physicsDef.View, { properties: physics.properties, position: transform.position, rotation: transform.rotation, scale: transform.scale, editMode: editMode, nodeId: gameObject.id, registerRigidBodyRef: registerRigidBodyRef, children: inner }, physicsKey));
202
207
  }
203
208
  return (_jsx("group", { ref: groupRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, onPointerDown: onDown, onPointerMove: () => (clickValid.current = false), onPointerUp: onUp, children: inner }));
204
209
  }
@@ -1,3 +1,12 @@
1
1
  import { Component } from './ComponentRegistry';
2
+ import { MeshStandardMaterialProperties } from 'three';
3
+ export interface MaterialProps extends Omit<MeshStandardMaterialProperties, 'args'> {
4
+ texture?: string;
5
+ repeat?: boolean;
6
+ repeatCount?: [number, number];
7
+ generateMipmaps?: boolean;
8
+ minFilter?: string;
9
+ magFilter?: string;
10
+ }
2
11
  declare const MaterialComponent: Component;
3
12
  export default MaterialComponent;
@@ -1,3 +1,14 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
1
12
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
13
  import { SingleTextureViewer, TextureListViewer } from '../../assetviewer/page';
3
14
  import { useEffect, useState } from 'react';
@@ -25,6 +36,13 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
25
36
  const fields = [
26
37
  { name: 'color', type: 'color', label: 'Color' },
27
38
  { name: 'wireframe', type: 'boolean', label: 'Wireframe' },
39
+ { name: 'transparent', type: 'boolean', label: 'Transparent' },
40
+ { name: 'opacity', type: 'number', label: 'Opacity', min: 0, max: 1, step: 0.01 },
41
+ { name: 'metalness', type: 'number', label: 'Metalness', min: 0, max: 1, step: 0.01 },
42
+ { name: 'roughness', type: 'number', label: 'Roughness', min: 0, max: 1, step: 0.01 },
43
+ { name: 'transmission', type: 'number', label: 'Transmission', min: 0, max: 1, step: 0.01 },
44
+ { name: 'thickness', type: 'number', label: 'Thickness', min: 0, step: 0.1 },
45
+ { name: 'ior', type: 'number', label: 'IOR (Index of Refraction)', min: 1, max: 2.333, step: 0.01 },
28
46
  {
29
47
  name: 'texture',
30
48
  type: 'custom',
@@ -80,6 +98,9 @@ function MaterialComponentView({ properties, loadedTextures }) {
80
98
  const minFilter = (properties === null || properties === void 0 ? void 0 : properties.minFilter) || 'LinearMipmapLinearFilter';
81
99
  const magFilter = (properties === null || properties === void 0 ? void 0 : properties.magFilter) || 'LinearFilter';
82
100
  const texture = textureName && loadedTextures ? loadedTextures[textureName] : undefined;
101
+ // Destructure all material props and separate custom texture handling props
102
+ const _b = properties || {}, { texture: _texture, repeat: _repeat, repeatCount: _repeatCount, generateMipmaps: _generateMipmaps, minFilter: _minFilter, magFilter: _magFilter, map: _map } = _b, // Filter out map since we set it explicitly
103
+ materialProps = __rest(_b, ["texture", "repeat", "repeatCount", "generateMipmaps", "minFilter", "magFilter", "map"]);
83
104
  const minFilterMap = {
84
105
  NearestFilter,
85
106
  LinearFilter,
@@ -116,8 +137,7 @@ function MaterialComponentView({ properties, loadedTextures }) {
116
137
  if (!properties) {
117
138
  return _jsx("meshStandardMaterial", { color: "red", wireframe: true });
118
139
  }
119
- const { color, wireframe = false } = properties;
120
- return (_jsx("meshStandardMaterial", { color: color, wireframe: wireframe, map: finalTexture, transparent: !!finalTexture }, (_a = finalTexture === null || finalTexture === void 0 ? void 0 : finalTexture.uuid) !== null && _a !== void 0 ? _a : 'no-texture'));
140
+ return (_jsx("meshStandardMaterial", Object.assign({ map: finalTexture }, materialProps), (_a = finalTexture === null || finalTexture === void 0 ? void 0 : finalTexture.uuid) !== null && _a !== void 0 ? _a : 'no-texture'));
121
141
  }
122
142
  const MaterialComponent = {
123
143
  name: 'Material',
@@ -126,7 +146,11 @@ const MaterialComponent = {
126
146
  nonComposable: true,
127
147
  defaultProperties: {
128
148
  color: '#ffffff',
129
- wireframe: false
149
+ wireframe: false,
150
+ transparent: false,
151
+ opacity: 1,
152
+ metalness: 0,
153
+ roughness: 1
130
154
  }
131
155
  };
132
156
  export default MaterialComponent;
@@ -11,6 +11,7 @@ var __rest = (this && this.__rest) || function (s, e) {
11
11
  };
12
12
  import { jsx as _jsx } from "react/jsx-runtime";
13
13
  import { RigidBody } from "@react-three/rapier";
14
+ import { useRef, useEffect } from 'react';
14
15
  import { FieldRenderer } from "./Input";
15
16
  const physicsFields = [
16
17
  {
@@ -79,15 +80,27 @@ const physicsFields = [
79
80
  function PhysicsComponentEditor({ component, onUpdate }) {
80
81
  return (_jsx(FieldRenderer, { fields: physicsFields, values: component.properties, onChange: (props) => onUpdate(Object.assign(Object.assign({}, component), { properties: Object.assign(Object.assign({}, component.properties), props) })) }));
81
82
  }
82
- function PhysicsComponentView({ properties, children, position, rotation, scale, editMode }) {
83
+ function PhysicsComponentView({ properties, children, position, rotation, scale, editMode, nodeId, registerRigidBodyRef }) {
83
84
  const { type, colliders } = properties, otherProps = __rest(properties, ["type", "colliders"]);
84
85
  const colliderType = colliders || (type === 'fixed' ? 'trimesh' : 'hull');
86
+ const rigidBodyRef = useRef(null);
87
+ // Register RigidBody ref when it's available
88
+ useEffect(() => {
89
+ if (nodeId && registerRigidBodyRef && rigidBodyRef.current) {
90
+ registerRigidBodyRef(nodeId, rigidBodyRef.current);
91
+ }
92
+ return () => {
93
+ if (nodeId && registerRigidBodyRef) {
94
+ registerRigidBodyRef(nodeId, null);
95
+ }
96
+ };
97
+ }, [nodeId, registerRigidBodyRef]);
85
98
  // In edit mode, include position/rotation in key to force remount when transform changes
86
99
  // This ensures the RigidBody debug visualization updates even when physics is paused
87
100
  const rbKey = editMode
88
101
  ? `${type || 'dynamic'}_${colliderType}_${position === null || position === void 0 ? void 0 : position.join(',')}_${rotation === null || rotation === void 0 ? void 0 : rotation.join(',')}`
89
102
  : `${type || 'dynamic'}_${colliderType}`;
90
- return (_jsx(RigidBody, Object.assign({ type: type, colliders: colliderType, position: position, rotation: rotation, scale: scale }, otherProps, { children: children }), rbKey));
103
+ return (_jsx(RigidBody, Object.assign({ ref: rigidBodyRef, type: type, colliders: colliderType, position: position, rotation: rotation, scale: scale }, otherProps, { children: children }), rbKey));
91
104
  }
92
105
  const PhysicsComponent = {
93
106
  name: 'Physics',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.49",
3
+ "version": "0.0.50",
4
4
  "description": "Batteries included React Three Fiber game engine",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -116,7 +116,7 @@ Scenes are defined as JSON prefabs with a root node containing children:
116
116
  | Transform | `Transform` | `position: [x,y,z]`, `rotation: [x,y,z]` (radians), `scale: [x,y,z]` |
117
117
  | Geometry | `Geometry` | `geometryType`: box/sphere/plane/cylinder, `args`: dimension array |
118
118
  | Material | `Material` | `color`, `texture?`, `metalness?`, `roughness?`, `repeat?`, `repeatCount?` |
119
- | Physics | `Physics` | `type`: dynamic/fixed/kinematicPosition/kinematicVelocity, `mass?`, `restitution?`, `friction?`, `linearDamping?`, `angularDamping?`, `gravityScale?`, plus any Rapier RigidBody props |
119
+ | Physics | `Physics` | `type`: dynamic/fixed/kinematicPosition/kinematicVelocity, `mass?`, `restitution?`, `friction?`, `linearDamping?`, `angularDamping?`, `gravityScale?`, plus any Rapier RigidBody props - [See advanced physics guide](./rules/ADVANCED_PHYSICS.md) |
120
120
  | Model | `Model` | `filename` (GLB/FBX path), `instanced?` for GPU batching |
121
121
  | SpotLight | `SpotLight` | `color`, `intensity`, `angle`, `penumbra`, `distance?`, `castShadow?` |
122
122
  | DirectionalLight | `DirectionalLight` | `color`, `intensity`, `castShadow?`, `targetOffset?: [x,y,z]` |
@@ -191,102 +191,71 @@ import { PrefabEditor } from 'react-three-game';
191
191
  ### Tree Utilities
192
192
 
193
193
  ```typescript
194
- import { updateNodeById, findNode, deleteNode, cloneNode, exportGLBData } from 'react-three-game';
194
+ import { findNode, updateNode, updateNodeById, deleteNode, cloneNode, exportGLBData } from 'react-three-game';
195
195
 
196
- const updated = updateNodeById(root, nodeId, node => ({ ...node, disabled: true }));
197
196
  const node = findNode(root, nodeId);
197
+ const updated = updateNode(root, nodeId, n => ({ ...n, disabled: true })); // or updateNodeById (identical)
198
198
  const afterDelete = deleteNode(root, nodeId);
199
199
  const cloned = cloneNode(node);
200
200
  const glbData = await exportGLBData(sceneRoot);
201
201
  ```
202
202
 
203
- ## Level Patterns
203
+ ## Hybrid JSON + R3F Children Pattern
204
204
 
205
- ### Floor
205
+ **Prefabs define static scene structure, R3F children add dynamic behavior**:
206
206
 
207
- ```json
208
- {
209
- "id": "floor",
210
- "components": {
211
- "transform": { "type": "Transform", "properties": { "position": [0, -0.5, 0] } },
212
- "geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [40, 1, 40] } },
213
- "material": { "type": "Material", "properties": { "texture": "/textures/floor.png", "repeat": true, "repeatCount": [20, 20] } },
214
- "physics": { "type": "Physics", "properties": { "type": "fixed" } }
215
- }
216
- }
217
- ```
218
-
219
- ### Platform
220
-
221
- ```json
222
- {
223
- "id": "platform",
224
- "components": {
225
- "transform": { "type": "Transform", "properties": { "position": [-8, 2, -5] } },
226
- "geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [6, 0.5, 4] } },
227
- "physics": { "type": "Physics", "properties": { "type": "fixed" } }
228
- }
229
- }
230
- ```
231
-
232
- ### Ramp
233
-
234
- ```json
235
- {
236
- "id": "ramp",
237
- "components": {
238
- "transform": { "type": "Transform", "properties": { "position": [-12, 1, -5], "rotation": [0, 0, 0.3] } },
239
- "geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [5, 0.3, 3] } },
240
- "physics": { "type": "Physics", "properties": { "type": "fixed" } }
241
- }
242
- }
243
- ```
244
-
245
- ### Wall Pattern
246
- ### Wall
207
+ ```tsx
208
+ import { useRef } from 'react';
209
+ import { useFrame } from '@react-three/fiber';
210
+ import { PrefabEditor, findNode } from 'react-three-game';
211
+ import type { PrefabEditorRef } from 'react-three-game';
247
212
 
248
- ```json
249
- {
250
- "id": "wall",
251
- "components": {
252
- "transform": { "type": "Transform", "properties": { "position": [0, 3, -20] } },
253
- "geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [40, 7, 1] } },
254
- "physics": { "type": "Physics", "properties": { "type": "fixed" } }
255
- }
213
+ function DynamicLight() {
214
+ const lightRef = useRef<THREE.SpotLight>(null!);
215
+
216
+ useFrame(({ clock }) => {
217
+ lightRef.current.intensity = 100 + Math.sin(clock.elapsedTime) * 50;
218
+ });
219
+
220
+ return <spotLight ref={lightRef} position={[10, 15, 10]} angle={0.5} />;
256
221
  }
257
- ```
258
222
 
259
- ### Lighting
260
-
261
- ```json
262
- [
263
- { "id": "spot", "components": { "transform": { "properties": { "position": [10, 15, 10] } }, "spotlight": { "type": "SpotLight", "properties": { "intensity": 200, "angle": 0.8, "castShadow": true } } } },
264
- { "id": "ambient", "components": { "ambientlight": { "type": "AmbientLight", "properties": { "intensity": 0.4 } } } }
265
- ]
223
+ <PrefabEditor initialPrefab={staticScenePrefab}>
224
+ <DynamicLight />
225
+ <CustomController />
226
+ </PrefabEditor>
266
227
  ```
267
228
 
268
- ### Text
269
-
270
- ```json
271
- {
272
- "id": "text",
273
- "components": {
274
- "transform": { "type": "Transform", "properties": { "position": [0, 3, 0] } },
275
- "text": { "type": "Text", "properties": { "text": "Welcome", "font": "/fonts/font.ttf", "size": 1, "depth": 0.1 } }
276
- }
277
- }
278
- ```
229
+ **Use cases**: Player controllers, AI behaviors, procedural animation, real-time effects.
279
230
 
280
- ### Model
231
+ ## Quick Reference Examples
281
232
 
282
233
  ```json
283
- {
284
- "id": "model",
285
- "components": {
286
- "transform": { "type": "Transform", "properties": { "position": [0, 0, 0], "scale": [1.5, 1.5, 1.5] } },
287
- "model": { "type": "Model", "properties": { "filename": "/models/tree.glb" } }
288
- }
289
- }
234
+ // Static geometry with physics (floor, wall, platform, ramp)
235
+ { "id": "floor", "components": {
236
+ "transform": { "type": "Transform", "properties": { "position": [0, -0.5, 0] } },
237
+ "geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [40, 1, 40] } },
238
+ "material": { "type": "Material", "properties": { "texture": "/textures/floor.png", "repeat": true, "repeatCount": [20, 20] } },
239
+ "physics": { "type": "Physics", "properties": { "type": "fixed" } }
240
+ }}
241
+
242
+ // Lighting
243
+ { "id": "spot", "components": {
244
+ "transform": { "type": "Transform", "properties": { "position": [10, 15, 10] } },
245
+ "spotlight": { "type": "SpotLight", "properties": { "intensity": 200, "angle": 0.8, "castShadow": true } }
246
+ }}
247
+
248
+ // 3D Text
249
+ { "id": "title", "components": {
250
+ "transform": { "type": "Transform", "properties": { "position": [0, 3, 0] } },
251
+ "text": { "type": "Text", "properties": { "text": "Welcome", "font": "/fonts/font.ttf", "size": 1, "depth": 0.1 } }
252
+ }}
253
+
254
+ // GLB Model
255
+ { "id": "tree", "components": {
256
+ "transform": { "type": "Transform", "properties": { "position": [0, 0, 0], "scale": [1.5, 1.5, 1.5] } },
257
+ "model": { "type": "Model", "properties": { "filename": "/models/tree.glb" } }
258
+ }}
290
259
  ```
291
260
 
292
261
  ## Editor
@@ -301,6 +270,21 @@ import { PrefabEditor } from 'react-three-game';
301
270
 
302
271
  Keyboard shortcuts: **T** (Translate), **R** (Rotate), **S** (Scale)
303
272
 
273
+ ### Camera Control
274
+
275
+ By default, `PrefabEditor` uses an orbit camera. **Override it by adding a custom camera with `makeDefault`**:
276
+
277
+ ```tsx
278
+ import { PerspectiveCamera } from '@react-three/drei';
279
+ import { PrefabEditor } from 'react-three-game';
280
+
281
+ <PrefabEditor initialPrefab={prefab}>
282
+ <PerspectiveCamera makeDefault position={[0, 5, 10]} fov={75} />
283
+ </PrefabEditor>
284
+ ```
285
+
286
+ Any R3F camera component works: `PerspectiveCamera`, `OrthographicCamera`, or custom camera controllers.
287
+
304
288
  ### Programmatic Updates
305
289
 
306
290
  ```jsx
@@ -0,0 +1,349 @@
1
+ # Advanced Physics & Patterns
2
+
3
+ ## Physics Type Decision Tree
4
+
5
+ ```
6
+ Need physics?
7
+ ├─ No → Don't add Physics component
8
+ └─ Yes → Does it move?
9
+ ├─ Never moves (walls, floor, static props)
10
+ │ └─ type: "fixed"
11
+
12
+ ├─ Moves via forces/gravity (balls, boxes, ragdolls)
13
+ │ └─ type: "dynamic"
14
+ │ ├─ Fast moving? → ccd: true
15
+ │ └─ Heavy? → mass: 10+
16
+
17
+ ├─ Scripted animation (moving platforms, doors)
18
+ │ └─ type: "kinematicPosition"
19
+ │ └─ Update transform via updateNodeById
20
+
21
+ └─ Velocity-driven (conveyor belts, wind zones)
22
+ └─ type: "kinematicVelocity"
23
+ └─ Set velocity via RigidBody ref
24
+ ```
25
+
26
+ **Type descriptions**:
27
+ - **fixed**: Immovable, infinite mass (ground, walls, buildings)
28
+ - **dynamic**: Affected by forces and gravity (player, projectiles, props)
29
+ - **kinematicPosition**: Move via code, push dynamic bodies (elevators, doors)
30
+ - **kinematicVelocity**: Set constant velocity, push dynamic bodies (conveyors)
31
+
32
+ **Performance tip**: Use `fixed` for anything that never moves - it's cheapest.
33
+
34
+ ## Physics Material Properties
35
+
36
+ Complete reference for `Physics` component properties:
37
+
38
+ | Property | Type | Default | Description |
39
+ |----------|------|---------|-------------|
40
+ | `type` | `'dynamic'` \| `'fixed'` \| `'kinematicPosition'` \| `'kinematicVelocity'` | `'dynamic'` | Body type (see decision tree above) |
41
+ | `mass` | `number` | `1` | Body mass (dynamic only) |
42
+ | `restitution` | `number` | `0` | Bounciness (0 = no bounce, 1 = perfect bounce) |
43
+ | `friction` | `number` | `0.5` | Surface friction (0 = ice, 1+ = sticky) |
44
+ | `linearDamping` | `number` | `0` | Velocity decay (0 = none, 1 = full stop) |
45
+ | `angularDamping` | `number` | `0` | Rotation decay |
46
+ | `gravityScale` | `number` | `1` | Gravity multiplier (0 = floating, 2 = heavy) |
47
+ | `lockTranslations` | `boolean` | `false` | Freeze position |
48
+ | `lockRotations` | `boolean` | `false` | Freeze rotation |
49
+ | `enabledTranslations` | `[bool, bool, bool]` | `[true, true, true]` | Lock per axis (X, Y, Z) |
50
+ | `enabledRotations` | `[bool, bool, bool]` | `[true, true, true]` | Lock rotation per axis |
51
+ | `ccd` | `boolean` | `false` | Continuous collision detection (fast objects) |
52
+ | `sensor` | `boolean` | `false` | Trigger only, no collision response |
53
+ | `collisionGroups` | `number` | - | Rapier collision groups bitfield |
54
+ | `solverGroups` | `number` | - | Rapier solver groups bitfield |
55
+
56
+ **Example - Bouncy Ball**:
57
+ ```json
58
+ {
59
+ "physics": {
60
+ "type": "Physics",
61
+ "properties": {
62
+ "type": "dynamic",
63
+ "mass": 0.5,
64
+ "restitution": 0.9,
65
+ "friction": 0.1,
66
+ "linearDamping": 0.05
67
+ }
68
+ }
69
+ }
70
+ ```
71
+
72
+ **Example - Ice Surface**:
73
+ ```json
74
+ {
75
+ "physics": {
76
+ "type": "Physics",
77
+ "properties": {
78
+ "type": "fixed",
79
+ "friction": 0,
80
+ "restitution": 0.1
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ ## Force & Impulse Application
87
+
88
+ Access RigidBody refs via `PrefabRootRef.rigidBodyRefs` to apply physics forces to prefab objects:
89
+
90
+ ```tsx
91
+ import { useRef, useEffect } from 'react';
92
+ import { PrefabEditor } from 'react-three-game';
93
+ import type { PrefabEditorRef } from 'react-three-game';
94
+ import type { RapierRigidBody } from '@react-three/rapier';
95
+
96
+ function ForceApplier({ editorRef }: { editorRef: React.RefObject<PrefabEditorRef> }) {
97
+ useEffect(() => {
98
+ const interval = setInterval(() => {
99
+ const rootRef = editorRef.current?.rootRef.current;
100
+ if (!rootRef) return;
101
+
102
+ // Access RigidBody ref by node ID
103
+ const rigidBody = rootRef.rigidBodyRefs.get('ball') as RapierRigidBody;
104
+
105
+ if (rigidBody) {
106
+ // Apply upward impulse
107
+ rigidBody.applyImpulse({ x: 0, y: 5, z: 0 }, true);
108
+
109
+ // Or apply continuous force
110
+ rigidBody.addForce({ x: 0, y: 10, z: 0 }, true);
111
+
112
+ // Apply torque
113
+ rigidBody.applyTorqueImpulse({ x: 0, y: 1, z: 0 }, true);
114
+ }
115
+ }, 2000);
116
+
117
+ return () => clearInterval(interval);
118
+ }, [editorRef]);
119
+
120
+ return null;
121
+ }
122
+
123
+ function Scene() {
124
+ const editorRef = useRef<PrefabEditorRef>(null);
125
+
126
+ return (
127
+ <PrefabEditor ref={editorRef} initialPrefab={prefab}>
128
+ <ForceApplier editorRef={editorRef} />
129
+ </PrefabEditor>
130
+ );
131
+ }
132
+ ```
133
+
134
+ **Alternative: Custom R3F components**
135
+
136
+ For fully custom physics objects, create R3F components with their own RigidBody refs:
137
+
138
+ ```tsx
139
+ import { useRef } from 'react';
140
+ import { RigidBody } from '@react-three/rapier';
141
+ import type { RapierRigidBody } from '@react-three/rapier';
142
+ import { useFrame } from '@react-three/fiber';
143
+ import { PrefabEditor } from 'react-three-game';
144
+
145
+ function PhysicsBall() {
146
+ const rigidBodyRef = useRef<RapierRigidBody>(null);
147
+
148
+ useFrame(() => {
149
+ if (rigidBodyRef.current) {
150
+ // Apply jump force on interval
151
+ rigidBodyRef.current.applyImpulse({ x: 0, y: 5, z: 0 }, true);
152
+ }
153
+ });
154
+
155
+ return (
156
+ <RigidBody ref={rigidBodyRef} position={[0, 5, 0]} type="dynamic">
157
+ <mesh castShadow>
158
+ <sphereGeometry args={[0.5, 32, 32]} />
159
+ <meshStandardMaterial color="orange" />
160
+ </mesh>
161
+ </RigidBody>
162
+ );
163
+ }
164
+
165
+ <PrefabEditor initialPrefab={prefab}>
166
+ <PhysicsBall />
167
+ </PrefabEditor>
168
+ ```
169
+
170
+ **Alternative: Kinematic position updates**
171
+
172
+ For smooth animated movement without forces, use `kinematicPosition` and update via `updateNodeById`:
173
+
174
+ ```tsx
175
+ import { useRef } from 'react';
176
+ import { useFrame } from '@react-three/fiber';
177
+ import { PrefabEditor, updateNodeById } from 'react-three-game';
178
+ import type { PrefabEditorRef } from 'react-three-game';
179
+
180
+ function KinematicMover({ editorRef }: { editorRef: React.RefObject<PrefabEditorRef> }) {
181
+ useFrame(({ clock }) => {
182
+ const prefab = editorRef.current?.prefab;
183
+ if (!prefab) return;
184
+
185
+ const y = 2 + Math.sin(clock.elapsedTime * 2) * 3;
186
+
187
+ const newRoot = updateNodeById(prefab.root, "platform", node => ({
188
+ ...node,
189
+ components: {
190
+ ...node.components,
191
+ transform: {
192
+ ...node.components!.transform!,
193
+ properties: {
194
+ ...node.components!.transform!.properties,
195
+ position: [0, y, 0]
196
+ }
197
+ }
198
+ }
199
+ }));
200
+
201
+ editorRef.current!.setPrefab({ ...prefab, root: newRoot });
202
+ });
203
+
204
+ return null;
205
+ }
206
+ ```
207
+
208
+ **Rapier RigidBody methods**:
209
+ - `applyImpulse(vector, wakeUp)` - Instantaneous velocity change
210
+ - `addForce(vector, wakeUp)` - Continuous force application
211
+ - `applyTorqueImpulse(vector, wakeUp)` - Rotational impulse
212
+ - `addTorque(vector, wakeUp)` - Continuous torque
213
+ - `setLinvel(vector, wakeUp)` - Set linear velocity directly
214
+ - `setAngvel(vector, wakeUp)` - Set angular velocity directly
215
+
216
+ ## Tilted Surfaces & Containment
217
+
218
+ **⚠️ Tilted walls don't contain objects** - physics objects slide off angled surfaces.
219
+
220
+ ### ❌ Wrong Approach
221
+ ```json
222
+ {
223
+ "id": "tilted-wall",
224
+ "components": {
225
+ "transform": { "type": "Transform", "properties": { "rotation": [0, 0, 0.3] } },
226
+ "geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [10, 5, 1] } },
227
+ "physics": { "type": "Physics", "properties": { "type": "fixed" } }
228
+ }
229
+ }
230
+ ```
231
+ Objects will **slide off** the tilted surface.
232
+
233
+ ### ✅ Correct Pattern - Perpendicular Walls
234
+ ```json
235
+ {
236
+ "id": "container",
237
+ "children": [
238
+ {
239
+ "id": "floor",
240
+ "components": {
241
+ "transform": { "type": "Transform", "properties": { "position": [0, 0, 0] } },
242
+ "geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [20, 1, 20] } },
243
+ "physics": { "type": "Physics", "properties": { "type": "fixed" } }
244
+ }
245
+ },
246
+ {
247
+ "id": "wall-north",
248
+ "components": {
249
+ "transform": { "type": "Transform", "properties": { "position": [0, 2.5, -10] } },
250
+ "geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [20, 5, 1] } },
251
+ "physics": { "type": "Physics", "properties": { "type": "fixed" } }
252
+ }
253
+ },
254
+ {
255
+ "id": "wall-south",
256
+ "components": {
257
+ "transform": { "type": "Transform", "properties": { "position": [0, 2.5, 10] } },
258
+ "geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [20, 5, 1] } },
259
+ "physics": { "type": "Physics", "properties": { "type": "fixed" } }
260
+ }
261
+ },
262
+ {
263
+ "id": "wall-east",
264
+ "components": {
265
+ "transform": { "type": "Transform", "properties": { "position": [10, 2.5, 0] } },
266
+ "geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [1, 5, 20] } },
267
+ "physics": { "type": "Physics", "properties": { "type": "fixed" } }
268
+ }
269
+ },
270
+ {
271
+ "id": "wall-west",
272
+ "components": {
273
+ "transform": { "type": "Transform", "properties": { "position": [-10, 2.5, 0] } },
274
+ "geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [1, 5, 20] } },
275
+ "physics": { "type": "Physics", "properties": { "type": "fixed" } }
276
+ }
277
+ }
278
+ ]
279
+ }
280
+ ```
281
+
282
+ **Key principle**: Walls must be **perpendicular to gravity** to contain dynamic objects.
283
+
284
+ ## Instanced Physics
285
+
286
+ When using `"instanced": true` on models, physics behaves differently than standard objects. **All instances of the same model share a single `InstancedRigidBodies` component** for optimal GPU performance.
287
+
288
+ ### Standard vs Instanced Physics
289
+
290
+ | Aspect | Standard Physics | Instanced Physics |
291
+ |--------|------------------|-------------------|
292
+ | RigidBody Component | Individual `<RigidBody>` per object | Single `<InstancedRigidBodies>` for all instances |
293
+ | Ref Access | `rigidBodyRefs.get(nodeId)` returns single RigidBody | Not accessible via `rigidBodyRefs` |
294
+ | Force Application | Direct per-object | Must access via InstancedRigidBodies ref |
295
+ | Collider Type | `hull` (dynamic) or `trimesh` (fixed) | Same, auto-selected |
296
+ | Performance | One draw call per object | One draw call for all instances |
297
+
298
+ ### Defining Instanced Objects
299
+
300
+ Set `"instanced": true` in the model component. **All instances of the same model+physics type are automatically batched**:
301
+
302
+ ```json
303
+ {
304
+ "id": "tree1",
305
+ "components": {
306
+ "transform": { "type": "Transform", "properties": { "position": [0, 0, 0] } },
307
+ "model": { "type": "Model", "properties": { "filename": "models/tree.glb", "instanced": true } },
308
+ "physics": { "type": "Physics", "properties": { "type": "fixed" } }
309
+ }
310
+ }
311
+ ```
312
+
313
+ Add multiple instances - they'll be automatically batched:
314
+
315
+ ```json
316
+ {
317
+ "id": "tree2",
318
+ "components": {
319
+ "transform": { "type": "Transform", "properties": { "position": [5, 0, 3] } },
320
+ "model": { "type": "Model", "properties": { "filename": "models/tree.glb", "instanced": true } },
321
+ "physics": { "type": "Physics", "properties": { "type": "fixed" } }
322
+ }
323
+ }
324
+ ```
325
+
326
+ ### Force Application on Instanced Objects
327
+
328
+ **Instanced physics bodies are not individually accessible.** For objects requiring force/impulse control, use non-instanced physics (`"instanced": false` or omit the property).
329
+
330
+ ### When to Use Instanced Physics
331
+
332
+ ✅ **Good for:**
333
+ - Many copies of the same static object (trees, rocks, buildings)
334
+ - Large scenes with 100+ similar objects
335
+ - Fixed physics bodies that never move
336
+ - Background props and decorations
337
+
338
+ ❌ **Avoid for:**
339
+ - Objects requiring individual force/impulse control
340
+ - Dynamic objects with unique behaviors
341
+ - Objects that need to be individually removed/spawned
342
+ - Fewer than ~20 instances (overhead not worth it)
343
+
344
+ ### Performance Notes
345
+
346
+ - **Batching**: All instances with the same `filename` and `physics.type` are rendered in a single draw call
347
+ - **Scale handling**: Visual scale is applied per-instance, but collider scale may differ
348
+ - **Transform updates**: Use `updateNodeById` to move instances (triggers re-sync)
349
+ - **Memory**: One set of GPU buffers shared across all instances
@@ -11,6 +11,7 @@ import { GameInstance, GameInstanceProvider, useInstanceCheck } from "./Instance
11
11
  import { updateNode } from "./utils";
12
12
  import { PhysicsProps } from "./components/PhysicsComponent";
13
13
  import { EditorContext } from "./EditorContext";
14
+ import type { RapierRigidBody } from "@react-three/rapier";
14
15
 
15
16
  components.forEach(registerComponent);
16
17
 
@@ -18,6 +19,7 @@ const IDENTITY = new Matrix4();
18
19
 
19
20
  export interface PrefabRootRef {
20
21
  root: Group | null;
22
+ rigidBodyRefs: Map<string, RapierRigidBody | null>;
21
23
  }
22
24
 
23
25
  export const PrefabRoot = forwardRef<PrefabRootRef, {
@@ -40,11 +42,13 @@ export const PrefabRoot = forwardRef<PrefabRootRef, {
40
42
  const [textures, setTextures] = useState<Record<string, Texture>>({});
41
43
  const loading = useRef(new Set<string>());
42
44
  const objectRefs = useRef<Record<string, Object3D | null>>({});
45
+ const rigidBodyRefs = useRef<Map<string, RapierRigidBody | null>>(new Map());
43
46
  const [selectedObject, setSelectedObject] = useState<Object3D | null>(null);
44
47
  const rootRef = useRef<Group>(null);
45
48
 
46
49
  useImperativeHandle(ref, () => ({
47
- root: rootRef.current
50
+ root: rootRef.current,
51
+ rigidBodyRefs: rigidBodyRefs.current
48
52
  }), []);
49
53
 
50
54
  const registerRef = useCallback((id: string, obj: Object3D | null) => {
@@ -52,6 +56,10 @@ export const PrefabRoot = forwardRef<PrefabRootRef, {
52
56
  if (id === selectedId) setSelectedObject(obj);
53
57
  }, [selectedId]);
54
58
 
59
+ const registerRigidBodyRef = useCallback((id: string, rb: RapierRigidBody | null) => {
60
+ rigidBodyRefs.current.set(id, rb);
61
+ }, []);
62
+
55
63
  useEffect(() => {
56
64
  const originalError = console.error;
57
65
  console.error = (...args: any[]) => {
@@ -145,6 +153,7 @@ export const PrefabRoot = forwardRef<PrefabRootRef, {
145
153
  onSelect={editMode ? onSelect : undefined}
146
154
  onClick={onClick}
147
155
  registerRef={registerRef}
156
+ registerRigidBodyRef={registerRigidBodyRef}
148
157
  loadedModels={models}
149
158
  loadedTextures={textures}
150
159
  editMode={editMode}
@@ -278,6 +287,7 @@ function StandardNode({
278
287
  onSelect,
279
288
  onClick,
280
289
  registerRef,
290
+ registerRigidBodyRef,
281
291
  loadedModels,
282
292
  loadedTextures,
283
293
  editMode,
@@ -341,6 +351,7 @@ function StandardNode({
341
351
  onSelect={onSelect}
342
352
  onClick={onClick}
343
353
  registerRef={registerRef}
354
+ registerRigidBodyRef={registerRigidBodyRef}
344
355
  loadedModels={loadedModels}
345
356
  loadedTextures={loadedTextures}
346
357
  editMode={editMode}
@@ -379,6 +390,8 @@ function StandardNode({
379
390
  rotation={transform.rotation}
380
391
  scale={transform.scale}
381
392
  editMode={editMode}
393
+ nodeId={gameObject.id}
394
+ registerRigidBodyRef={registerRigidBodyRef}
382
395
  >{inner}</physicsDef.View>
383
396
  ) : null}
384
397
  </>
@@ -394,6 +407,8 @@ function StandardNode({
394
407
  rotation={transform.rotation}
395
408
  scale={transform.scale}
396
409
  editMode={editMode}
410
+ nodeId={gameObject.id}
411
+ registerRigidBodyRef={registerRigidBodyRef}
397
412
  >{inner}</physicsDef.View>
398
413
  );
399
414
  }
@@ -419,6 +434,7 @@ interface RendererProps {
419
434
  onSelect?: (id: string) => void;
420
435
  onClick?: (event: ThreeEvent<PointerEvent>, entity: GameObjectType) => void;
421
436
  registerRef: (id: string, obj: Object3D | null) => void;
437
+ registerRigidBodyRef: (id: string, rb: RapierRigidBody | null) => void;
422
438
  loadedModels: Record<string, Object3D>;
423
439
  loadedTextures: Record<string, Texture>;
424
440
  editMode?: boolean;
@@ -15,9 +15,19 @@ import {
15
15
  LinearMipmapNearestFilter,
16
16
  LinearMipmapLinearFilter,
17
17
  MinificationTextureFilter,
18
- MagnificationTextureFilter
18
+ MagnificationTextureFilter,
19
+ MeshStandardMaterialProperties
19
20
  } from 'three';
20
21
 
22
+ export interface MaterialProps extends Omit<MeshStandardMaterialProperties, 'args'> {
23
+ texture?: string;
24
+ repeat?: boolean;
25
+ repeatCount?: [number, number];
26
+ generateMipmaps?: boolean;
27
+ minFilter?: string;
28
+ magFilter?: string;
29
+ }
30
+
21
31
  function TexturePicker({
22
32
  value,
23
33
  onChange,
@@ -71,6 +81,13 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }: { compo
71
81
  const fields: FieldDefinition[] = [
72
82
  { name: 'color', type: 'color', label: 'Color' },
73
83
  { name: 'wireframe', type: 'boolean', label: 'Wireframe' },
84
+ { name: 'transparent', type: 'boolean', label: 'Transparent' },
85
+ { name: 'opacity', type: 'number', label: 'Opacity', min: 0, max: 1, step: 0.01 },
86
+ { name: 'metalness', type: 'number', label: 'Metalness', min: 0, max: 1, step: 0.01 },
87
+ { name: 'roughness', type: 'number', label: 'Roughness', min: 0, max: 1, step: 0.01 },
88
+ { name: 'transmission', type: 'number', label: 'Transmission', min: 0, max: 1, step: 0.01 },
89
+ { name: 'thickness', type: 'number', label: 'Thickness', min: 0, step: 0.1 },
90
+ { name: 'ior', type: 'number', label: 'IOR (Index of Refraction)', min: 1, max: 2.333, step: 0.01 },
74
91
  {
75
92
  name: 'texture',
76
93
  type: 'custom',
@@ -135,7 +152,7 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }: { compo
135
152
  }
136
153
 
137
154
  // View for Material component
138
- function MaterialComponentView({ properties, loadedTextures }: { properties: any, loadedTextures?: Record<string, Texture> }) {
155
+ function MaterialComponentView({ properties, loadedTextures }: { properties: MaterialProps, loadedTextures?: Record<string, Texture> }) {
139
156
  const textureName = properties?.texture;
140
157
  const repeat = properties?.repeat;
141
158
  const repeatCount = properties?.repeatCount;
@@ -144,6 +161,18 @@ function MaterialComponentView({ properties, loadedTextures }: { properties: any
144
161
  const magFilter = properties?.magFilter || 'LinearFilter';
145
162
  const texture = textureName && loadedTextures ? loadedTextures[textureName] : undefined;
146
163
 
164
+ // Destructure all material props and separate custom texture handling props
165
+ const {
166
+ texture: _texture,
167
+ repeat: _repeat,
168
+ repeatCount: _repeatCount,
169
+ generateMipmaps: _generateMipmaps,
170
+ minFilter: _minFilter,
171
+ magFilter: _magFilter,
172
+ map: _map, // Filter out map since we set it explicitly
173
+ ...materialProps
174
+ } = properties || {};
175
+
147
176
  const minFilterMap: Record<string, MinificationTextureFilter> = {
148
177
  NearestFilter,
149
178
  LinearFilter,
@@ -180,15 +209,11 @@ function MaterialComponentView({ properties, loadedTextures }: { properties: any
180
209
  return <meshStandardMaterial color="red" wireframe />;
181
210
  }
182
211
 
183
- const { color, wireframe = false } = properties;
184
-
185
212
  return (
186
213
  <meshStandardMaterial
187
214
  key={finalTexture?.uuid ?? 'no-texture'}
188
- color={color}
189
- wireframe={wireframe}
190
215
  map={finalTexture}
191
- transparent={!!finalTexture}
216
+ {...materialProps}
192
217
  />
193
218
  );
194
219
  }
@@ -200,7 +225,11 @@ const MaterialComponent: Component = {
200
225
  nonComposable: true,
201
226
  defaultProperties: {
202
227
  color: '#ffffff',
203
- wireframe: false
228
+ wireframe: false,
229
+ transparent: false,
230
+ opacity: 1,
231
+ metalness: 0,
232
+ roughness: 1
204
233
  }
205
234
  };
206
235
 
@@ -1,6 +1,7 @@
1
1
  import { RigidBody, RapierRigidBody } from "@react-three/rapier";
2
2
  import type { RigidBodyOptions } from "@react-three/rapier";
3
3
  import type { ReactNode } from 'react';
4
+ import { useRef, useEffect } from 'react';
4
5
  import { Component } from "./ComponentRegistry";
5
6
  import { FieldRenderer, FieldDefinition } from "./Input";
6
7
  import { ComponentData } from "../types";
@@ -89,11 +90,26 @@ interface PhysicsViewProps {
89
90
  position?: [number, number, number];
90
91
  rotation?: [number, number, number];
91
92
  scale?: [number, number, number];
93
+ nodeId?: string;
94
+ registerRigidBodyRef?: (id: string, rb: RapierRigidBody | null) => void;
92
95
  }
93
96
 
94
- function PhysicsComponentView({ properties, children, position, rotation, scale, editMode }: PhysicsViewProps) {
97
+ function PhysicsComponentView({ properties, children, position, rotation, scale, editMode, nodeId, registerRigidBodyRef }: PhysicsViewProps) {
95
98
  const { type, colliders, ...otherProps } = properties;
96
99
  const colliderType = colliders || (type === 'fixed' ? 'trimesh' : 'hull');
100
+ const rigidBodyRef = useRef<RapierRigidBody>(null);
101
+
102
+ // Register RigidBody ref when it's available
103
+ useEffect(() => {
104
+ if (nodeId && registerRigidBodyRef && rigidBodyRef.current) {
105
+ registerRigidBodyRef(nodeId, rigidBodyRef.current);
106
+ }
107
+ return () => {
108
+ if (nodeId && registerRigidBodyRef) {
109
+ registerRigidBodyRef(nodeId, null);
110
+ }
111
+ };
112
+ }, [nodeId, registerRigidBodyRef]);
97
113
 
98
114
  // In edit mode, include position/rotation in key to force remount when transform changes
99
115
  // This ensures the RigidBody debug visualization updates even when physics is paused
@@ -104,6 +120,7 @@ function PhysicsComponentView({ properties, children, position, rotation, scale,
104
120
  return (
105
121
  <RigidBody
106
122
  key={rbKey}
123
+ ref={rigidBodyRef}
107
124
  type={type}
108
125
  colliders={colliderType as any}
109
126
  position={position}