react-three-game 0.0.17 → 0.0.18
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/dist/tools/prefabeditor/InstanceProvider.d.ts +4 -4
- package/dist/tools/prefabeditor/InstanceProvider.js +21 -13
- package/dist/tools/prefabeditor/PrefabRoot.js +26 -27
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +114 -0
- package/dist/tools/prefabeditor/components/ModelComponent.js +12 -4
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +10 -5
- package/dist/tools/prefabeditor/components/index.js +2 -0
- package/dist/tools/prefabeditor/hooks/useModelLoader.d.ts +10 -0
- package/dist/tools/prefabeditor/hooks/useModelLoader.js +40 -0
- package/package.json +3 -3
- package/src/tools/prefabeditor/InstanceProvider.tsx +43 -32
- package/src/tools/prefabeditor/PrefabRoot.tsx +30 -41
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +332 -0
- package/src/tools/prefabeditor/components/ModelComponent.tsx +14 -4
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +27 -7
- package/src/tools/prefabeditor/components/index.ts +2 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { Component } from "./ComponentRegistry";
|
|
2
|
+
import { useRef, useEffect } from "react";
|
|
3
|
+
import { useFrame, useThree } from "@react-three/fiber";
|
|
4
|
+
import { CameraHelper, DirectionalLight, Object3D, Vector3 } from "three";
|
|
5
|
+
|
|
6
|
+
function DirectionalLightComponentEditor({ component, onUpdate }: { component: any; onUpdate: (newComp: any) => void }) {
|
|
7
|
+
const props = {
|
|
8
|
+
color: component.properties.color ?? '#ffffff',
|
|
9
|
+
intensity: component.properties.intensity ?? 1.0,
|
|
10
|
+
castShadow: component.properties.castShadow ?? true,
|
|
11
|
+
shadowMapSize: component.properties.shadowMapSize ?? 1024,
|
|
12
|
+
shadowCameraNear: component.properties.shadowCameraNear ?? 0.1,
|
|
13
|
+
shadowCameraFar: component.properties.shadowCameraFar ?? 100,
|
|
14
|
+
shadowCameraTop: component.properties.shadowCameraTop ?? 30,
|
|
15
|
+
shadowCameraBottom: component.properties.shadowCameraBottom ?? -30,
|
|
16
|
+
shadowCameraLeft: component.properties.shadowCameraLeft ?? -30,
|
|
17
|
+
shadowCameraRight: component.properties.shadowCameraRight ?? 30,
|
|
18
|
+
targetOffset: component.properties.targetOffset ?? [0, -5, 0]
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return <div className="flex flex-col gap-2">
|
|
22
|
+
<div>
|
|
23
|
+
<label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5">Color</label>
|
|
24
|
+
<div className="flex gap-0.5">
|
|
25
|
+
<input
|
|
26
|
+
type="color"
|
|
27
|
+
className="h-5 w-5 bg-transparent border-none cursor-pointer"
|
|
28
|
+
value={props.color}
|
|
29
|
+
onChange={e => onUpdate({ ...component.properties, 'color': e.target.value })}
|
|
30
|
+
/>
|
|
31
|
+
<input
|
|
32
|
+
type="text"
|
|
33
|
+
className="flex-1 bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50"
|
|
34
|
+
value={props.color}
|
|
35
|
+
onChange={e => onUpdate({ ...component.properties, 'color': e.target.value })}
|
|
36
|
+
/>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
<div>
|
|
40
|
+
<label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5">Intensity</label>
|
|
41
|
+
<input
|
|
42
|
+
type="number"
|
|
43
|
+
step="0.1"
|
|
44
|
+
className="w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50"
|
|
45
|
+
value={props.intensity}
|
|
46
|
+
onChange={e => onUpdate({ ...component.properties, 'intensity': parseFloat(e.target.value) })}
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
49
|
+
<div>
|
|
50
|
+
<label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5">Cast Shadow</label>
|
|
51
|
+
<input
|
|
52
|
+
type="checkbox"
|
|
53
|
+
className="h-4 w-4 bg-black/40 border border-cyan-500/30 cursor-pointer"
|
|
54
|
+
checked={props.castShadow}
|
|
55
|
+
onChange={e => onUpdate({ ...component.properties, 'castShadow': e.target.checked })}
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
<div>
|
|
59
|
+
<label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5">Shadow Map Size</label>
|
|
60
|
+
<input
|
|
61
|
+
type="number"
|
|
62
|
+
step="256"
|
|
63
|
+
className="w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50"
|
|
64
|
+
value={props.shadowMapSize}
|
|
65
|
+
onChange={e => onUpdate({ ...component.properties, 'shadowMapSize': parseFloat(e.target.value) })}
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
<div className="border-t border-cyan-500/20 pt-2 mt-2">
|
|
69
|
+
<label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-1">Shadow Camera</label>
|
|
70
|
+
<div className="grid grid-cols-2 gap-1">
|
|
71
|
+
<div>
|
|
72
|
+
<label className="block text-[8px] text-cyan-400/50 mb-0.5">Near</label>
|
|
73
|
+
<input
|
|
74
|
+
type="number"
|
|
75
|
+
step="0.1"
|
|
76
|
+
className="w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50"
|
|
77
|
+
value={props.shadowCameraNear}
|
|
78
|
+
onChange={e => onUpdate({ ...component.properties, 'shadowCameraNear': parseFloat(e.target.value) })}
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
<div>
|
|
82
|
+
<label className="block text-[8px] text-cyan-400/50 mb-0.5">Far</label>
|
|
83
|
+
<input
|
|
84
|
+
type="number"
|
|
85
|
+
step="1"
|
|
86
|
+
className="w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50"
|
|
87
|
+
value={props.shadowCameraFar}
|
|
88
|
+
onChange={e => onUpdate({ ...component.properties, 'shadowCameraFar': parseFloat(e.target.value) })}
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
<div>
|
|
92
|
+
<label className="block text-[8px] text-cyan-400/50 mb-0.5">Top</label>
|
|
93
|
+
<input
|
|
94
|
+
type="number"
|
|
95
|
+
step="1"
|
|
96
|
+
className="w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50"
|
|
97
|
+
value={props.shadowCameraTop}
|
|
98
|
+
onChange={e => onUpdate({ ...component.properties, 'shadowCameraTop': parseFloat(e.target.value) })}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
<div>
|
|
102
|
+
<label className="block text-[8px] text-cyan-400/50 mb-0.5">Bottom</label>
|
|
103
|
+
<input
|
|
104
|
+
type="number"
|
|
105
|
+
step="1"
|
|
106
|
+
className="w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50"
|
|
107
|
+
value={props.shadowCameraBottom}
|
|
108
|
+
onChange={e => onUpdate({ ...component.properties, 'shadowCameraBottom': parseFloat(e.target.value) })}
|
|
109
|
+
/>
|
|
110
|
+
</div>
|
|
111
|
+
<div>
|
|
112
|
+
<label className="block text-[8px] text-cyan-400/50 mb-0.5">Left</label>
|
|
113
|
+
<input
|
|
114
|
+
type="number"
|
|
115
|
+
step="1"
|
|
116
|
+
className="w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50"
|
|
117
|
+
value={props.shadowCameraLeft}
|
|
118
|
+
onChange={e => onUpdate({ ...component.properties, 'shadowCameraLeft': parseFloat(e.target.value) })}
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
<div>
|
|
122
|
+
<label className="block text-[8px] text-cyan-400/50 mb-0.5">Right</label>
|
|
123
|
+
<input
|
|
124
|
+
type="number"
|
|
125
|
+
step="1"
|
|
126
|
+
className="w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50"
|
|
127
|
+
value={props.shadowCameraRight}
|
|
128
|
+
onChange={e => onUpdate({ ...component.properties, 'shadowCameraRight': parseFloat(e.target.value) })}
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
<div className="border-t border-cyan-500/20 pt-2 mt-2">
|
|
134
|
+
<label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-1">Target Offset</label>
|
|
135
|
+
<div className="grid grid-cols-3 gap-1">
|
|
136
|
+
<div>
|
|
137
|
+
<label className="block text-[8px] text-cyan-400/50 mb-0.5">X</label>
|
|
138
|
+
<input
|
|
139
|
+
type="number"
|
|
140
|
+
step="0.5"
|
|
141
|
+
className="w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50"
|
|
142
|
+
value={props.targetOffset[0]}
|
|
143
|
+
onChange={e => onUpdate({
|
|
144
|
+
...component.properties,
|
|
145
|
+
'targetOffset': [parseFloat(e.target.value), props.targetOffset[1], props.targetOffset[2]]
|
|
146
|
+
})}
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
149
|
+
<div>
|
|
150
|
+
<label className="block text-[8px] text-cyan-400/50 mb-0.5">Y</label>
|
|
151
|
+
<input
|
|
152
|
+
type="number"
|
|
153
|
+
step="0.5"
|
|
154
|
+
className="w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50"
|
|
155
|
+
value={props.targetOffset[1]}
|
|
156
|
+
onChange={e => onUpdate({
|
|
157
|
+
...component.properties,
|
|
158
|
+
'targetOffset': [props.targetOffset[0], parseFloat(e.target.value), props.targetOffset[2]]
|
|
159
|
+
})}
|
|
160
|
+
/>
|
|
161
|
+
</div>
|
|
162
|
+
<div>
|
|
163
|
+
<label className="block text-[8px] text-cyan-400/50 mb-0.5">Z</label>
|
|
164
|
+
<input
|
|
165
|
+
type="number"
|
|
166
|
+
step="0.5"
|
|
167
|
+
className="w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50"
|
|
168
|
+
value={props.targetOffset[2]}
|
|
169
|
+
onChange={e => onUpdate({
|
|
170
|
+
...component.properties,
|
|
171
|
+
'targetOffset': [props.targetOffset[0], props.targetOffset[1], parseFloat(e.target.value)]
|
|
172
|
+
})}
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function DirectionalLightView({ properties, editMode }: { properties: any; editMode?: boolean }) {
|
|
181
|
+
const color = properties.color ?? '#ffffff';
|
|
182
|
+
const intensity = properties.intensity ?? 1.0;
|
|
183
|
+
const castShadow = properties.castShadow ?? true;
|
|
184
|
+
const shadowMapSize = properties.shadowMapSize ?? 1024;
|
|
185
|
+
const shadowCameraNear = properties.shadowCameraNear ?? 0.1;
|
|
186
|
+
const shadowCameraFar = properties.shadowCameraFar ?? 100;
|
|
187
|
+
const shadowCameraTop = properties.shadowCameraTop ?? 30;
|
|
188
|
+
const shadowCameraBottom = properties.shadowCameraBottom ?? -30;
|
|
189
|
+
const shadowCameraLeft = properties.shadowCameraLeft ?? -30;
|
|
190
|
+
const shadowCameraRight = properties.shadowCameraRight ?? 30;
|
|
191
|
+
const targetOffset = properties.targetOffset ?? [0, -5, 0];
|
|
192
|
+
|
|
193
|
+
const { scene } = useThree();
|
|
194
|
+
const directionalLightRef = useRef<DirectionalLight>(null);
|
|
195
|
+
const targetRef = useRef<Object3D>(new Object3D());
|
|
196
|
+
const lastUpdate = useRef(0);
|
|
197
|
+
const cameraHelperRef = useRef<CameraHelper | null>(null);
|
|
198
|
+
const lastPositionRef = useRef<Vector3>(new Vector3());
|
|
199
|
+
|
|
200
|
+
// Add target to scene
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
if (targetRef.current) {
|
|
203
|
+
scene.add(targetRef.current);
|
|
204
|
+
return () => {
|
|
205
|
+
scene.remove(targetRef.current);
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}, [scene]);
|
|
209
|
+
|
|
210
|
+
// Update target position when light position or offset changes
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (directionalLightRef.current && targetRef.current) {
|
|
213
|
+
const lightWorldPos = new Vector3();
|
|
214
|
+
directionalLightRef.current.getWorldPosition(lightWorldPos);
|
|
215
|
+
targetRef.current.position.set(
|
|
216
|
+
lightWorldPos.x + targetOffset[0],
|
|
217
|
+
lightWorldPos.y + targetOffset[1],
|
|
218
|
+
lightWorldPos.z + targetOffset[2]
|
|
219
|
+
);
|
|
220
|
+
directionalLightRef.current.target = targetRef.current;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
// Create camera helper for edit mode and add to scene
|
|
226
|
+
if (editMode && directionalLightRef.current && directionalLightRef.current.shadow.camera) {
|
|
227
|
+
const helper = new CameraHelper(directionalLightRef.current.shadow.camera);
|
|
228
|
+
cameraHelperRef.current = helper;
|
|
229
|
+
scene.add(helper);
|
|
230
|
+
|
|
231
|
+
return () => {
|
|
232
|
+
if (cameraHelperRef.current) {
|
|
233
|
+
scene.remove(cameraHelperRef.current);
|
|
234
|
+
cameraHelperRef.current.dispose();
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}, [editMode, scene]);
|
|
239
|
+
|
|
240
|
+
useFrame(({ clock }) => {
|
|
241
|
+
if (!directionalLightRef.current || !directionalLightRef.current.shadow) return;
|
|
242
|
+
|
|
243
|
+
// Disable auto-update for shadows
|
|
244
|
+
if (directionalLightRef.current.shadow.autoUpdate) {
|
|
245
|
+
directionalLightRef.current.shadow.autoUpdate = false;
|
|
246
|
+
directionalLightRef.current.shadow.needsUpdate = true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Check if position has changed
|
|
250
|
+
const currentPosition = new Vector3();
|
|
251
|
+
directionalLightRef.current.getWorldPosition(currentPosition);
|
|
252
|
+
|
|
253
|
+
const positionChanged = !currentPosition.equals(lastPositionRef.current);
|
|
254
|
+
|
|
255
|
+
if (positionChanged) {
|
|
256
|
+
lastPositionRef.current.copy(currentPosition);
|
|
257
|
+
directionalLightRef.current.shadow.needsUpdate = true;
|
|
258
|
+
lastUpdate.current = clock.elapsedTime; // Reset timer on position change
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Update shadow map infrequently (every 5 seconds) if position hasn't changed
|
|
262
|
+
if (!editMode && !positionChanged && clock.elapsedTime - lastUpdate.current > 5) {
|
|
263
|
+
lastUpdate.current = clock.elapsedTime;
|
|
264
|
+
directionalLightRef.current.shadow.needsUpdate = true;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Update camera helper in edit mode
|
|
268
|
+
if (editMode && cameraHelperRef.current) {
|
|
269
|
+
cameraHelperRef.current.update();
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<>
|
|
275
|
+
<directionalLight
|
|
276
|
+
ref={directionalLightRef}
|
|
277
|
+
color={color}
|
|
278
|
+
intensity={intensity}
|
|
279
|
+
castShadow={castShadow}
|
|
280
|
+
shadow-mapSize={[shadowMapSize, shadowMapSize]}
|
|
281
|
+
shadow-bias={-0.001}
|
|
282
|
+
shadow-normalBias={0.02}
|
|
283
|
+
>
|
|
284
|
+
<orthographicCamera
|
|
285
|
+
attach="shadow-camera"
|
|
286
|
+
near={shadowCameraNear}
|
|
287
|
+
far={shadowCameraFar}
|
|
288
|
+
top={shadowCameraTop}
|
|
289
|
+
bottom={shadowCameraBottom}
|
|
290
|
+
left={shadowCameraLeft}
|
|
291
|
+
right={shadowCameraRight}
|
|
292
|
+
/>
|
|
293
|
+
</directionalLight>
|
|
294
|
+
{editMode && (
|
|
295
|
+
<>
|
|
296
|
+
{/* Light source indicator */}
|
|
297
|
+
<mesh>
|
|
298
|
+
<sphereGeometry args={[0.3, 8, 6]} />
|
|
299
|
+
<meshBasicMaterial color={color} wireframe />
|
|
300
|
+
</mesh>
|
|
301
|
+
{/* Target indicator */}
|
|
302
|
+
<mesh position={targetOffset as [number, number, number]}>
|
|
303
|
+
<sphereGeometry args={[0.2, 8, 6]} />
|
|
304
|
+
<meshBasicMaterial color={color} wireframe opacity={0.5} transparent />
|
|
305
|
+
</mesh>
|
|
306
|
+
{/* Direction line */}
|
|
307
|
+
<line>
|
|
308
|
+
<bufferGeometry
|
|
309
|
+
onUpdate={(geo) => {
|
|
310
|
+
const points = [
|
|
311
|
+
new Vector3(0, 0, 0),
|
|
312
|
+
new Vector3(targetOffset[0], targetOffset[1], targetOffset[2])
|
|
313
|
+
];
|
|
314
|
+
geo.setFromPoints(points);
|
|
315
|
+
}}
|
|
316
|
+
/>
|
|
317
|
+
<lineBasicMaterial color={color} opacity={0.6} transparent />
|
|
318
|
+
</line>
|
|
319
|
+
</>
|
|
320
|
+
)}
|
|
321
|
+
</>
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const DirectionalLightComponent: Component = {
|
|
326
|
+
name: 'DirectionalLight',
|
|
327
|
+
Editor: DirectionalLightComponentEditor,
|
|
328
|
+
View: DirectionalLightView,
|
|
329
|
+
defaultProperties: {}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
export default DirectionalLightComponent;
|
|
@@ -47,12 +47,22 @@ function ModelComponentEditor({ component, onUpdate, basePath = "" }: { componen
|
|
|
47
47
|
// View for Model component
|
|
48
48
|
function ModelComponentView({ properties, loadedModels, children }: { properties: any, loadedModels?: Record<string, any>, children?: React.ReactNode }) {
|
|
49
49
|
// Instanced models are handled elsewhere (GameInstance), so only render non-instanced here
|
|
50
|
-
if (!properties.filename || properties.instanced) return children
|
|
50
|
+
if (!properties.filename || properties.instanced) return <>{children}</>;
|
|
51
|
+
|
|
51
52
|
if (loadedModels && loadedModels[properties.filename]) {
|
|
52
|
-
|
|
53
|
+
const clonedModel = loadedModels[properties.filename].clone();
|
|
54
|
+
// Enable shadows on all meshes in the model
|
|
55
|
+
clonedModel.traverse((obj: any) => {
|
|
56
|
+
if (obj.isMesh) {
|
|
57
|
+
obj.castShadow = true;
|
|
58
|
+
obj.receiveShadow = true;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
return <primitive object={clonedModel}>{children}</primitive>;
|
|
53
62
|
}
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
|
|
64
|
+
// Model not loaded yet - render children only
|
|
65
|
+
return <>{children}</>;
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
const ModelComponent: Component = {
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
|
|
2
1
|
import { Component } from "./ComponentRegistry";
|
|
3
|
-
import { useRef } from "react";
|
|
2
|
+
import { useRef, useEffect } from "react";
|
|
4
3
|
|
|
5
4
|
function SpotLightComponentEditor({ component, onUpdate }: { component: any; onUpdate: (newComp: any) => void }) {
|
|
6
|
-
// Provide default values to prevent NaN
|
|
7
5
|
const props = {
|
|
8
6
|
color: component.properties.color ?? '#ffffff',
|
|
9
7
|
intensity: component.properties.intensity ?? 1.0,
|
|
@@ -88,10 +86,7 @@ function SpotLightComponentEditor({ component, onUpdate }: { component: any; onU
|
|
|
88
86
|
</div>;
|
|
89
87
|
}
|
|
90
88
|
|
|
91
|
-
|
|
92
|
-
// The view component for SpotLight
|
|
93
|
-
function SpotLightView({ properties }: { properties: any }) {
|
|
94
|
-
// Provide defaults in case properties are missing
|
|
89
|
+
function SpotLightView({ properties, editMode }: { properties: any; editMode?: boolean }) {
|
|
95
90
|
const color = properties.color ?? '#ffffff';
|
|
96
91
|
const intensity = properties.intensity ?? 1.0;
|
|
97
92
|
const angle = properties.angle ?? Math.PI / 6;
|
|
@@ -99,16 +94,41 @@ function SpotLightView({ properties }: { properties: any }) {
|
|
|
99
94
|
const distance = properties.distance ?? 100;
|
|
100
95
|
const castShadow = properties.castShadow ?? true;
|
|
101
96
|
|
|
97
|
+
const spotLightRef = useRef<any>(null);
|
|
98
|
+
const targetRef = useRef<any>(null);
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (spotLightRef.current && targetRef.current) {
|
|
102
|
+
spotLightRef.current.target = targetRef.current;
|
|
103
|
+
}
|
|
104
|
+
}, []);
|
|
105
|
+
|
|
102
106
|
return (
|
|
103
107
|
<>
|
|
104
108
|
<spotLight
|
|
109
|
+
ref={spotLightRef}
|
|
105
110
|
color={color}
|
|
106
111
|
intensity={intensity}
|
|
107
112
|
angle={angle}
|
|
108
113
|
penumbra={penumbra}
|
|
109
114
|
distance={distance}
|
|
110
115
|
castShadow={castShadow}
|
|
116
|
+
shadow-bias={-0.0001}
|
|
117
|
+
shadow-normalBias={0.02}
|
|
111
118
|
/>
|
|
119
|
+
<object3D ref={targetRef} position={[0, -5, 0]} />
|
|
120
|
+
{editMode && (
|
|
121
|
+
<>
|
|
122
|
+
<mesh>
|
|
123
|
+
<sphereGeometry args={[0.2, 8, 6]} />
|
|
124
|
+
<meshBasicMaterial color={color} wireframe />
|
|
125
|
+
</mesh>
|
|
126
|
+
<mesh position={[0, -5, 0]}>
|
|
127
|
+
<sphereGeometry args={[0.15, 8, 6]} />
|
|
128
|
+
<meshBasicMaterial color={color} wireframe opacity={0.5} transparent />
|
|
129
|
+
</mesh>
|
|
130
|
+
</>
|
|
131
|
+
)}
|
|
112
132
|
</>
|
|
113
133
|
);
|
|
114
134
|
}
|
|
@@ -3,6 +3,7 @@ import TransformComponent from './TransformComponent';
|
|
|
3
3
|
import MaterialComponent from './MaterialComponent';
|
|
4
4
|
import PhysicsComponent from './PhysicsComponent';
|
|
5
5
|
import SpotLightComponent from './SpotLightComponent';
|
|
6
|
+
import DirectionalLightComponent from './DirectionalLightComponent';
|
|
6
7
|
import ModelComponent from './ModelComponent';
|
|
7
8
|
|
|
8
9
|
export default [
|
|
@@ -11,6 +12,7 @@ export default [
|
|
|
11
12
|
MaterialComponent,
|
|
12
13
|
PhysicsComponent,
|
|
13
14
|
SpotLightComponent,
|
|
15
|
+
DirectionalLightComponent,
|
|
14
16
|
ModelComponent
|
|
15
17
|
];
|
|
16
18
|
|