react-three-game 0.0.21 → 0.0.22
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.js +28 -27
- package/dist/tools/prefabeditor/PrefabRoot.js +6 -15
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +10 -46
- package/dist/tools/prefabeditor/components/MaterialComponent.js +1 -1
- package/dist/tools/prefabeditor/components/ModelComponent.js +13 -9
- package/dist/tools/prefabeditor/components/PhysicsComponent.js +2 -14
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +1 -1
- package/package.json +1 -1
- package/src/tools/prefabeditor/InstanceProvider.tsx +64 -42
- package/src/tools/prefabeditor/PrefabRoot.tsx +20 -33
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +20 -61
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +10 -8
- package/src/tools/prefabeditor/components/ModelComponent.tsx +13 -9
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +6 -5
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +2 -0
|
@@ -51,19 +51,19 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef }
|
|
|
51
51
|
});
|
|
52
52
|
}, []);
|
|
53
53
|
// Flatten all model meshes once (models → flat mesh parts)
|
|
54
|
+
// Note: Geometry is cloned with baked transforms for instancing
|
|
54
55
|
const { flatMeshes, modelParts } = useMemo(() => {
|
|
55
56
|
const flatMeshes = {};
|
|
56
57
|
const modelParts = {};
|
|
57
58
|
Object.entries(models).forEach(([modelKey, model]) => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const rootInverse = new Matrix4().copy(root.matrixWorld).invert();
|
|
59
|
+
model.updateWorldMatrix(false, true);
|
|
60
|
+
const rootInverse = new Matrix4().copy(model.matrixWorld).invert();
|
|
61
61
|
let partIndex = 0;
|
|
62
|
-
|
|
62
|
+
model.traverse((obj) => {
|
|
63
63
|
if (obj.isMesh) {
|
|
64
|
+
// Clone geometry and bake relative transform
|
|
64
65
|
const geom = obj.geometry.clone();
|
|
65
|
-
|
|
66
|
-
geom.applyMatrix4(relativeTransform);
|
|
66
|
+
geom.applyMatrix4(obj.matrixWorld.clone().premultiply(rootInverse));
|
|
67
67
|
const partKey = `${modelKey}__${partIndex}`;
|
|
68
68
|
flatMeshes[partKey] = new Mesh(geom, obj.material);
|
|
69
69
|
partIndex++;
|
|
@@ -73,6 +73,12 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef }
|
|
|
73
73
|
});
|
|
74
74
|
return { flatMeshes, modelParts };
|
|
75
75
|
}, [models]);
|
|
76
|
+
// Cleanup geometries when models change
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
return () => {
|
|
79
|
+
Object.values(flatMeshes).forEach(mesh => mesh.geometry.dispose());
|
|
80
|
+
};
|
|
81
|
+
}, [flatMeshes]);
|
|
76
82
|
// Group instances by meshPath + physics type for batch rendering
|
|
77
83
|
const grouped = useMemo(() => {
|
|
78
84
|
var _a;
|
|
@@ -124,35 +130,30 @@ function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes }) {
|
|
|
124
130
|
rotation: inst.rotation,
|
|
125
131
|
scale: inst.scale,
|
|
126
132
|
})), [group.instances]);
|
|
127
|
-
|
|
133
|
+
const colliders = group.physicsType === 'fixed' ? 'trimesh' : 'hull';
|
|
134
|
+
return (_jsx(InstancedRigidBodies, { instances: instances, colliders: colliders, type: group.physicsType, children: Array.from({ length: partCount }).map((_, i) => {
|
|
128
135
|
const mesh = flatMeshes[`${modelKey}__${i}`];
|
|
136
|
+
if (!mesh)
|
|
137
|
+
return null;
|
|
129
138
|
return (_jsx("instancedMesh", { args: [mesh.geometry, mesh.material, group.instances.length], castShadow: true, receiveShadow: true, frustumCulled: false }, i));
|
|
130
139
|
}) }));
|
|
131
140
|
}
|
|
132
141
|
// Render non-physics instances using Merged's per-instance groups
|
|
133
142
|
function NonPhysicsInstancedGroup({ modelKey, group, partCount, instancesMap, onSelect, registerRef }) {
|
|
143
|
+
// Pre-compute which Instance components exist for this model
|
|
144
|
+
const InstanceComponents = useMemo(() => Array.from({ length: partCount }, (_, i) => instancesMap[`${modelKey}__${i}`]).filter(Boolean), [instancesMap, modelKey, partCount]);
|
|
145
|
+
return (_jsx(_Fragment, { children: group.instances.map(inst => (_jsx(InstanceGroupItem, { instance: inst, InstanceComponents: InstanceComponents, onSelect: onSelect, registerRef: registerRef }, inst.id))) }));
|
|
146
|
+
}
|
|
147
|
+
// Individual instance item with its own click state
|
|
148
|
+
function InstanceGroupItem({ instance, InstanceComponents, onSelect, registerRef }) {
|
|
134
149
|
const clickValid = useRef(false);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (clickValid.current)
|
|
150
|
+
return (_jsx("group", { ref: (el) => registerRef === null || registerRef === void 0 ? void 0 : registerRef(instance.id, el), position: instance.position, rotation: instance.rotation, scale: instance.scale, onPointerDown: (e) => { e.stopPropagation(); clickValid.current = true; }, onPointerMove: () => { clickValid.current = false; }, onPointerUp: (e) => {
|
|
151
|
+
if (clickValid.current) {
|
|
152
|
+
e.stopPropagation();
|
|
153
|
+
onSelect === null || onSelect === void 0 ? void 0 : onSelect(instance.id);
|
|
154
|
+
}
|
|
141
155
|
clickValid.current = false;
|
|
142
|
-
|
|
143
|
-
const handlePointerUp = (e, id) => {
|
|
144
|
-
if (clickValid.current) {
|
|
145
|
-
e.stopPropagation();
|
|
146
|
-
onSelect === null || onSelect === void 0 ? void 0 : onSelect(id);
|
|
147
|
-
}
|
|
148
|
-
clickValid.current = false;
|
|
149
|
-
};
|
|
150
|
-
return (_jsx(_Fragment, { children: group.instances.map(inst => (_jsx("group", { ref: (el) => { registerRef === null || registerRef === void 0 ? void 0 : registerRef(inst.id, el); }, position: inst.position, rotation: inst.rotation, scale: inst.scale, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: (e) => handlePointerUp(e, inst.id), children: Array.from({ length: partCount }).map((_, i) => {
|
|
151
|
-
const Instance = instancesMap[`${modelKey}__${i}`];
|
|
152
|
-
if (!Instance)
|
|
153
|
-
return null;
|
|
154
|
-
return _jsx(Instance, {}, i);
|
|
155
|
-
}) }, inst.id))) }));
|
|
156
|
+
}, children: InstanceComponents.map((Instance, i) => _jsx(Instance, {}, i)) }));
|
|
156
157
|
}
|
|
157
158
|
// GameInstance component: registers an instance for batch rendering (renders nothing itself)
|
|
158
159
|
export const GameInstance = React.forwardRef(({ id, modelUrl, position, rotation, scale, physics = undefined, }, ref) => {
|
|
@@ -146,26 +146,17 @@ function GameObjectRenderer({ gameObject, selectedId, onSelect, registerRef, loa
|
|
|
146
146
|
const core = renderCoreNode(gameObject, ctx, parentMatrix);
|
|
147
147
|
// --- 5. Render children recursively (always relative transforms) ---
|
|
148
148
|
const children = ((_d = gameObject.children) !== null && _d !== void 0 ? _d : []).map((child) => (_jsx(GameObjectRenderer, { gameObject: child, selectedId: selectedId, onSelect: onSelect, registerRef: registerRef, loadedModels: loadedModels, loadedTextures: loadedTextures, editMode: editMode, parentMatrix: worldMatrix }, child.id)));
|
|
149
|
-
// --- 6.
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
rotation: transformProps.rotation,
|
|
153
|
-
onPointerDown: handlePointerDown,
|
|
154
|
-
onPointerMove: handlePointerMove,
|
|
155
|
-
onPointerUp: handlePointerUp,
|
|
156
|
-
};
|
|
157
|
-
// --- 7. Check if physics is needed ---
|
|
149
|
+
// --- 6. Inner content group with full transform ---
|
|
150
|
+
const innerGroup = (_jsxs("group", { ref: (el) => registerRef(gameObject.id, el), position: transformProps.position, rotation: transformProps.rotation, scale: transformProps.scale, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, children: [core, children] }));
|
|
151
|
+
// --- 7. Wrap with physics if needed (RigidBody as outer parent, no transform) ---
|
|
158
152
|
const physics = (_e = gameObject.components) === null || _e === void 0 ? void 0 : _e.physics;
|
|
159
|
-
|
|
160
|
-
// --- 8. Final structure: RigidBody outside with position/rotation, scale inside ---
|
|
161
|
-
if (hasPhysics) {
|
|
153
|
+
if (physics && !editMode) {
|
|
162
154
|
const physicsDef = getComponent('Physics');
|
|
163
155
|
if (physicsDef === null || physicsDef === void 0 ? void 0 : physicsDef.View) {
|
|
164
|
-
return (_jsx(physicsDef.View,
|
|
156
|
+
return (_jsx(physicsDef.View, { properties: physics.properties, children: innerGroup }));
|
|
165
157
|
}
|
|
166
158
|
}
|
|
167
|
-
|
|
168
|
-
return (_jsxs("group", Object.assign({ ref: (el) => registerRef(gameObject.id, el), scale: transformProps.scale }, wrapperProps, { children: [core, children] })));
|
|
159
|
+
return innerGroup;
|
|
169
160
|
}
|
|
170
161
|
// Helper: render an instanced GameInstance (terminal node)
|
|
171
162
|
function renderInstancedNode(gameObject, worldMatrix, ctx) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useRef, useEffect } from "react";
|
|
3
|
-
import { useFrame
|
|
4
|
-
import {
|
|
3
|
+
import { useFrame } from "@react-three/fiber";
|
|
4
|
+
import { Vector3 } from "three";
|
|
5
5
|
function DirectionalLightComponentEditor({ component, onUpdate }) {
|
|
6
6
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
7
7
|
const props = {
|
|
@@ -32,60 +32,24 @@ function DirectionalLightView({ properties, editMode }) {
|
|
|
32
32
|
const shadowCameraLeft = (_j = properties.shadowCameraLeft) !== null && _j !== void 0 ? _j : -30;
|
|
33
33
|
const shadowCameraRight = (_k = properties.shadowCameraRight) !== null && _k !== void 0 ? _k : 30;
|
|
34
34
|
const targetOffset = (_l = properties.targetOffset) !== null && _l !== void 0 ? _l : [0, -5, 0];
|
|
35
|
-
const { scene } = useThree();
|
|
36
35
|
const directionalLightRef = useRef(null);
|
|
37
|
-
const targetRef = useRef(
|
|
38
|
-
|
|
39
|
-
// Add target to scene once
|
|
36
|
+
const targetRef = useRef(null);
|
|
37
|
+
// Set up light target reference when both refs are ready
|
|
40
38
|
useEffect(() => {
|
|
41
|
-
|
|
42
|
-
scene.add(target);
|
|
43
|
-
return () => {
|
|
44
|
-
scene.remove(target);
|
|
45
|
-
};
|
|
46
|
-
}, [scene]);
|
|
47
|
-
// Set up light target reference once
|
|
48
|
-
useEffect(() => {
|
|
49
|
-
if (directionalLightRef.current) {
|
|
39
|
+
if (directionalLightRef.current && targetRef.current) {
|
|
50
40
|
directionalLightRef.current.target = targetRef.current;
|
|
51
41
|
}
|
|
52
42
|
}, []);
|
|
53
|
-
// Update target position
|
|
43
|
+
// Update target world position based on light position + offset
|
|
54
44
|
useFrame(() => {
|
|
55
|
-
if (!directionalLightRef.current)
|
|
45
|
+
if (!directionalLightRef.current || !targetRef.current)
|
|
56
46
|
return;
|
|
57
47
|
const lightWorldPos = new Vector3();
|
|
58
48
|
directionalLightRef.current.getWorldPosition(lightWorldPos);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (!targetRef.current.position.equals(newTargetPos)) {
|
|
62
|
-
targetRef.current.position.copy(newTargetPos);
|
|
63
|
-
if (directionalLightRef.current.shadow) {
|
|
64
|
-
directionalLightRef.current.shadow.needsUpdate = true;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
// Update camera helper in edit mode
|
|
68
|
-
if (editMode && cameraHelperRef.current) {
|
|
69
|
-
cameraHelperRef.current.update();
|
|
70
|
-
}
|
|
49
|
+
// Target is positioned relative to light's world position
|
|
50
|
+
targetRef.current.position.set(lightWorldPos.x + targetOffset[0], lightWorldPos.y + targetOffset[1], lightWorldPos.z + targetOffset[2]);
|
|
71
51
|
});
|
|
72
|
-
|
|
73
|
-
useEffect(() => {
|
|
74
|
-
var _a;
|
|
75
|
-
if (editMode && ((_a = directionalLightRef.current) === null || _a === void 0 ? void 0 : _a.shadow.camera)) {
|
|
76
|
-
const helper = new CameraHelper(directionalLightRef.current.shadow.camera);
|
|
77
|
-
cameraHelperRef.current = helper;
|
|
78
|
-
scene.add(helper);
|
|
79
|
-
return () => {
|
|
80
|
-
if (cameraHelperRef.current) {
|
|
81
|
-
scene.remove(cameraHelperRef.current);
|
|
82
|
-
cameraHelperRef.current.dispose();
|
|
83
|
-
cameraHelperRef.current = null;
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
}, [editMode, scene]);
|
|
88
|
-
return (_jsxs(_Fragment, { children: [_jsx("directionalLight", { ref: directionalLightRef, color: color, intensity: intensity, castShadow: castShadow, "shadow-mapSize": [shadowMapSize, shadowMapSize], "shadow-bias": -0.001, "shadow-normalBias": 0.02, children: _jsx("orthographicCamera", { attach: "shadow-camera", near: shadowCameraNear, far: shadowCameraFar, top: shadowCameraTop, bottom: shadowCameraBottom, left: shadowCameraLeft, right: shadowCameraRight }) }), editMode && (_jsxs(_Fragment, { children: [_jsxs("mesh", { children: [_jsx("sphereGeometry", { args: [0.3, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true })] }), _jsxs("mesh", { position: targetOffset, children: [_jsx("sphereGeometry", { args: [0.2, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, opacity: 0.5, transparent: true })] }), _jsxs("line", { children: [_jsx("bufferGeometry", { onUpdate: (geo) => {
|
|
52
|
+
return (_jsxs(_Fragment, { children: [_jsx("directionalLight", { ref: directionalLightRef, color: color, intensity: intensity, castShadow: castShadow, "shadow-mapSize-width": shadowMapSize, "shadow-mapSize-height": shadowMapSize, "shadow-camera-near": shadowCameraNear, "shadow-camera-far": shadowCameraFar, "shadow-camera-top": shadowCameraTop, "shadow-camera-bottom": shadowCameraBottom, "shadow-camera-left": shadowCameraLeft, "shadow-camera-right": shadowCameraRight, "shadow-bias": -0.001, "shadow-normalBias": 0.02 }), _jsx("object3D", { ref: targetRef }), editMode && (_jsxs(_Fragment, { children: [_jsxs("mesh", { children: [_jsx("sphereGeometry", { args: [0.3, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true })] }), _jsxs("mesh", { position: targetOffset, children: [_jsx("sphereGeometry", { args: [0.2, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, opacity: 0.5, transparent: true })] }), _jsxs("line", { children: [_jsx("bufferGeometry", { onUpdate: (geo) => {
|
|
89
53
|
const points = [
|
|
90
54
|
new Vector3(0, 0, 0),
|
|
91
55
|
new Vector3(targetOffset[0], targetOffset[1], targetOffset[2])
|
|
@@ -53,7 +53,7 @@ function MaterialComponentView({ properties, loadedTextures, isSelected }) {
|
|
|
53
53
|
}
|
|
54
54
|
const { color, wireframe = false } = properties;
|
|
55
55
|
const displayColor = isSelected ? "cyan" : color;
|
|
56
|
-
return _jsx("meshStandardMaterial", { color: displayColor, wireframe: wireframe, map: finalTexture, transparent: !!finalTexture, side: DoubleSide }, (_a = finalTexture === null || finalTexture === void 0 ? void 0 : finalTexture.uuid) !== null && _a !== void 0 ? _a : 'no-texture');
|
|
56
|
+
return (_jsx("meshStandardMaterial", { color: displayColor, wireframe: wireframe, map: finalTexture, transparent: !!finalTexture, side: DoubleSide }, (_a = finalTexture === null || finalTexture === void 0 ? void 0 : finalTexture.uuid) !== null && _a !== void 0 ? _a : 'no-texture'));
|
|
57
57
|
}
|
|
58
58
|
const MaterialComponent = {
|
|
59
59
|
name: 'Material',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { ModelListViewer } from '../../assetviewer/page';
|
|
3
|
-
import { useEffect, useState } from 'react';
|
|
3
|
+
import { useEffect, useState, useMemo } from 'react';
|
|
4
4
|
function ModelComponentEditor({ component, onUpdate, basePath = "" }) {
|
|
5
5
|
const [modelFiles, setModelFiles] = useState([]);
|
|
6
6
|
useEffect(() => {
|
|
@@ -22,19 +22,23 @@ function ModelComponentView({ properties, loadedModels, children }) {
|
|
|
22
22
|
// Instanced models are handled elsewhere (GameInstance), so only render non-instanced here
|
|
23
23
|
if (!properties.filename || properties.instanced)
|
|
24
24
|
return _jsx(_Fragment, { children: children });
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
const sourceModel = loadedModels === null || loadedModels === void 0 ? void 0 : loadedModels[properties.filename];
|
|
26
|
+
// Clone model once and set up shadows - memoized to avoid cloning on every render
|
|
27
|
+
const clonedModel = useMemo(() => {
|
|
28
|
+
if (!sourceModel)
|
|
29
|
+
return null;
|
|
30
|
+
const clone = sourceModel.clone();
|
|
31
|
+
clone.traverse((obj) => {
|
|
29
32
|
if (obj.isMesh) {
|
|
30
33
|
obj.castShadow = true;
|
|
31
34
|
obj.receiveShadow = true;
|
|
32
35
|
}
|
|
33
36
|
});
|
|
34
|
-
return
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
return clone;
|
|
38
|
+
}, [sourceModel]);
|
|
39
|
+
if (!clonedModel)
|
|
40
|
+
return _jsx(_Fragment, { children: children });
|
|
41
|
+
return _jsx("primitive", { object: clonedModel, children: children });
|
|
38
42
|
}
|
|
39
43
|
const ModelComponent = {
|
|
40
44
|
name: 'Model',
|
|
@@ -1,14 +1,3 @@
|
|
|
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
|
-
};
|
|
12
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
13
2
|
import { RigidBody } from "@react-three/rapier";
|
|
14
3
|
const selectClass = "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";
|
|
@@ -17,12 +6,11 @@ function PhysicsComponentEditor({ component, onUpdate }) {
|
|
|
17
6
|
const { type, collider = 'hull' } = component.properties;
|
|
18
7
|
return (_jsxs("div", { children: [_jsx("label", { className: labelClass, children: "Type" }), _jsxs("select", { className: selectClass, value: type, onChange: e => onUpdate({ type: e.target.value }), children: [_jsx("option", { value: "dynamic", children: "Dynamic" }), _jsx("option", { value: "fixed", children: "Fixed" })] }), _jsx("label", { className: `${labelClass} mt-2`, children: "Collider" }), _jsxs("select", { className: selectClass, value: collider, onChange: e => onUpdate({ collider: e.target.value }), children: [_jsx("option", { value: "hull", children: "Hull (convex)" }), _jsx("option", { value: "trimesh", children: "Trimesh (exact)" }), _jsx("option", { value: "cuboid", children: "Cuboid (box)" }), _jsx("option", { value: "ball", children: "Ball (sphere)" })] })] }));
|
|
19
8
|
}
|
|
20
|
-
function PhysicsComponentView(
|
|
21
|
-
var { properties, editMode, children } = _a, rigidBodyProps = __rest(_a, ["properties", "editMode", "children"]);
|
|
9
|
+
function PhysicsComponentView({ properties, editMode, children }) {
|
|
22
10
|
if (editMode)
|
|
23
11
|
return _jsx(_Fragment, { children: children });
|
|
24
12
|
const colliders = properties.collider || (properties.type === 'fixed' ? 'trimesh' : 'hull');
|
|
25
|
-
return (_jsx(RigidBody,
|
|
13
|
+
return (_jsx(RigidBody, { type: properties.type, colliders: colliders, children: children }));
|
|
26
14
|
}
|
|
27
15
|
const PhysicsComponent = {
|
|
28
16
|
name: 'Physics',
|
|
@@ -27,7 +27,7 @@ function SpotLightView({ properties, editMode }) {
|
|
|
27
27
|
spotLightRef.current.target = targetRef.current;
|
|
28
28
|
}
|
|
29
29
|
}, []);
|
|
30
|
-
return (_jsxs(_Fragment, { children: [_jsx("spotLight", { ref: spotLightRef, color: color, intensity: intensity, angle: angle, penumbra: penumbra, distance: distance, castShadow: castShadow, "shadow-bias": -0.0001, "shadow-normalBias": 0.02 }), _jsx("object3D", { ref: targetRef, position: [0, -5, 0] }), editMode && (_jsxs(_Fragment, { children: [_jsxs("mesh", { children: [_jsx("sphereGeometry", { args: [0.2, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true })] }), _jsxs("mesh", { position: [0, -5, 0], children: [_jsx("sphereGeometry", { args: [0.15, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, opacity: 0.5, transparent: true })] })] }))] }));
|
|
30
|
+
return (_jsxs(_Fragment, { children: [_jsx("spotLight", { ref: spotLightRef, color: color, intensity: intensity, angle: angle, penumbra: penumbra, distance: distance, castShadow: castShadow, "shadow-mapSize-width": 1024, "shadow-mapSize-height": 1024, "shadow-bias": -0.0001, "shadow-normalBias": 0.02 }), _jsx("object3D", { ref: targetRef, position: [0, -5, 0] }), editMode && (_jsxs(_Fragment, { children: [_jsxs("mesh", { children: [_jsx("sphereGeometry", { args: [0.2, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true })] }), _jsxs("mesh", { position: [0, -5, 0], children: [_jsx("sphereGeometry", { args: [0.15, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, opacity: 0.5, transparent: true })] })] }))] }));
|
|
31
31
|
}
|
|
32
32
|
const SpotLightComponent = {
|
|
33
33
|
name: 'SpotLight',
|
package/package.json
CHANGED
|
@@ -81,22 +81,21 @@ export function GameInstanceProvider({
|
|
|
81
81
|
}, []);
|
|
82
82
|
|
|
83
83
|
// Flatten all model meshes once (models → flat mesh parts)
|
|
84
|
+
// Note: Geometry is cloned with baked transforms for instancing
|
|
84
85
|
const { flatMeshes, modelParts } = useMemo(() => {
|
|
85
86
|
const flatMeshes: Record<string, Mesh> = {};
|
|
86
87
|
const modelParts: Record<string, number> = {};
|
|
87
88
|
|
|
88
89
|
Object.entries(models).forEach(([modelKey, model]) => {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const rootInverse = new Matrix4().copy(root.matrixWorld).invert();
|
|
90
|
+
model.updateWorldMatrix(false, true);
|
|
91
|
+
const rootInverse = new Matrix4().copy(model.matrixWorld).invert();
|
|
92
92
|
|
|
93
93
|
let partIndex = 0;
|
|
94
|
-
|
|
95
|
-
root.traverse((obj: any) => {
|
|
94
|
+
model.traverse((obj: any) => {
|
|
96
95
|
if (obj.isMesh) {
|
|
96
|
+
// Clone geometry and bake relative transform
|
|
97
97
|
const geom = obj.geometry.clone();
|
|
98
|
-
|
|
99
|
-
geom.applyMatrix4(relativeTransform);
|
|
98
|
+
geom.applyMatrix4(obj.matrixWorld.clone().premultiply(rootInverse));
|
|
100
99
|
|
|
101
100
|
const partKey = `${modelKey}__${partIndex}`;
|
|
102
101
|
flatMeshes[partKey] = new Mesh(geom, obj.material);
|
|
@@ -109,6 +108,13 @@ export function GameInstanceProvider({
|
|
|
109
108
|
return { flatMeshes, modelParts };
|
|
110
109
|
}, [models]);
|
|
111
110
|
|
|
111
|
+
// Cleanup geometries when models change
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
return () => {
|
|
114
|
+
Object.values(flatMeshes).forEach(mesh => mesh.geometry.dispose());
|
|
115
|
+
};
|
|
116
|
+
}, [flatMeshes]);
|
|
117
|
+
|
|
112
118
|
// Group instances by meshPath + physics type for batch rendering
|
|
113
119
|
const grouped = useMemo(() => {
|
|
114
120
|
const groups: Record<string, { physicsType: string, instances: InstanceData[] }> = {};
|
|
@@ -213,21 +219,24 @@ function InstancedRigidGroup({
|
|
|
213
219
|
[group.instances]
|
|
214
220
|
);
|
|
215
221
|
|
|
222
|
+
const colliders = group.physicsType === 'fixed' ? 'trimesh' : 'hull';
|
|
223
|
+
|
|
216
224
|
return (
|
|
217
225
|
<InstancedRigidBodies
|
|
218
226
|
instances={instances}
|
|
219
|
-
colliders={
|
|
227
|
+
colliders={colliders}
|
|
220
228
|
type={group.physicsType as 'dynamic' | 'fixed'}
|
|
221
229
|
>
|
|
222
230
|
{Array.from({ length: partCount }).map((_, i) => {
|
|
223
231
|
const mesh = flatMeshes[`${modelKey}__${i}`];
|
|
232
|
+
if (!mesh) return null;
|
|
224
233
|
return (
|
|
225
234
|
<instancedMesh
|
|
226
235
|
key={i}
|
|
227
236
|
args={[mesh.geometry, mesh.material, group.instances.length]}
|
|
228
237
|
castShadow
|
|
229
238
|
receiveShadow
|
|
230
|
-
frustumCulled={false}
|
|
239
|
+
frustumCulled={false} // Required: culling first instance hides all
|
|
231
240
|
/>
|
|
232
241
|
);
|
|
233
242
|
})}
|
|
@@ -251,49 +260,62 @@ function NonPhysicsInstancedGroup({
|
|
|
251
260
|
onSelect?: (id: string | null) => void;
|
|
252
261
|
registerRef?: (id: string, obj: Object3D | null) => void;
|
|
253
262
|
}) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
const handlePointerMove = () => {
|
|
262
|
-
if (clickValid.current) clickValid.current = false;
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
const handlePointerUp = (e: any, id: string) => {
|
|
266
|
-
if (clickValid.current) {
|
|
267
|
-
e.stopPropagation();
|
|
268
|
-
onSelect?.(id);
|
|
269
|
-
}
|
|
270
|
-
clickValid.current = false;
|
|
271
|
-
};
|
|
263
|
+
// Pre-compute which Instance components exist for this model
|
|
264
|
+
const InstanceComponents = useMemo(() =>
|
|
265
|
+
Array.from({ length: partCount }, (_, i) => instancesMap[`${modelKey}__${i}`]).filter(Boolean),
|
|
266
|
+
[instancesMap, modelKey, partCount]
|
|
267
|
+
);
|
|
272
268
|
|
|
273
269
|
return (
|
|
274
270
|
<>
|
|
275
271
|
{group.instances.map(inst => (
|
|
276
|
-
<
|
|
272
|
+
<InstanceGroupItem
|
|
277
273
|
key={inst.id}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
onPointerMove={handlePointerMove}
|
|
284
|
-
onPointerUp={(e) => handlePointerUp(e, inst.id)}
|
|
285
|
-
>
|
|
286
|
-
{Array.from({ length: partCount }).map((_, i) => {
|
|
287
|
-
const Instance = instancesMap[`${modelKey}__${i}`];
|
|
288
|
-
if (!Instance) return null;
|
|
289
|
-
return <Instance key={i} />;
|
|
290
|
-
})}
|
|
291
|
-
</group>
|
|
274
|
+
instance={inst}
|
|
275
|
+
InstanceComponents={InstanceComponents}
|
|
276
|
+
onSelect={onSelect}
|
|
277
|
+
registerRef={registerRef}
|
|
278
|
+
/>
|
|
292
279
|
))}
|
|
293
280
|
</>
|
|
294
281
|
);
|
|
295
282
|
}
|
|
296
283
|
|
|
284
|
+
// Individual instance item with its own click state
|
|
285
|
+
function InstanceGroupItem({
|
|
286
|
+
instance,
|
|
287
|
+
InstanceComponents,
|
|
288
|
+
onSelect,
|
|
289
|
+
registerRef
|
|
290
|
+
}: {
|
|
291
|
+
instance: InstanceData;
|
|
292
|
+
InstanceComponents: React.ComponentType<any>[];
|
|
293
|
+
onSelect?: (id: string | null) => void;
|
|
294
|
+
registerRef?: (id: string, obj: Object3D | null) => void;
|
|
295
|
+
}) {
|
|
296
|
+
const clickValid = useRef(false);
|
|
297
|
+
|
|
298
|
+
return (
|
|
299
|
+
<group
|
|
300
|
+
ref={(el) => registerRef?.(instance.id, el)}
|
|
301
|
+
position={instance.position}
|
|
302
|
+
rotation={instance.rotation}
|
|
303
|
+
scale={instance.scale}
|
|
304
|
+
onPointerDown={(e) => { e.stopPropagation(); clickValid.current = true; }}
|
|
305
|
+
onPointerMove={() => { clickValid.current = false; }}
|
|
306
|
+
onPointerUp={(e) => {
|
|
307
|
+
if (clickValid.current) {
|
|
308
|
+
e.stopPropagation();
|
|
309
|
+
onSelect?.(instance.id);
|
|
310
|
+
}
|
|
311
|
+
clickValid.current = false;
|
|
312
|
+
}}
|
|
313
|
+
>
|
|
314
|
+
{InstanceComponents.map((Instance, i) => <Instance key={i} />)}
|
|
315
|
+
</group>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
297
319
|
|
|
298
320
|
// GameInstance component: registers an instance for batch rendering (renders nothing itself)
|
|
299
321
|
export const GameInstance = React.forwardRef<Group, {
|
|
@@ -237,49 +237,36 @@ function GameObjectRenderer({
|
|
|
237
237
|
/>
|
|
238
238
|
));
|
|
239
239
|
|
|
240
|
-
// --- 6.
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
240
|
+
// --- 6. Inner content group with full transform ---
|
|
241
|
+
const innerGroup = (
|
|
242
|
+
<group
|
|
243
|
+
ref={(el) => registerRef(gameObject.id, el)}
|
|
244
|
+
position={transformProps.position}
|
|
245
|
+
rotation={transformProps.rotation}
|
|
246
|
+
scale={transformProps.scale}
|
|
247
|
+
onPointerDown={handlePointerDown}
|
|
248
|
+
onPointerMove={handlePointerMove}
|
|
249
|
+
onPointerUp={handlePointerUp}
|
|
250
|
+
>
|
|
251
|
+
{core}
|
|
252
|
+
{children}
|
|
253
|
+
</group>
|
|
254
|
+
);
|
|
248
255
|
|
|
249
|
-
// --- 7.
|
|
256
|
+
// --- 7. Wrap with physics if needed (RigidBody as outer parent, no transform) ---
|
|
250
257
|
const physics = gameObject.components?.physics;
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
// --- 8. Final structure: RigidBody outside with position/rotation, scale inside ---
|
|
254
|
-
if (hasPhysics) {
|
|
258
|
+
if (physics && !editMode) {
|
|
255
259
|
const physicsDef = getComponent('Physics');
|
|
256
260
|
if (physicsDef?.View) {
|
|
257
261
|
return (
|
|
258
|
-
<physicsDef.View
|
|
259
|
-
|
|
260
|
-
ref={(obj: Object3D | null) => registerRef(gameObject.id, obj)}
|
|
261
|
-
{...wrapperProps}
|
|
262
|
-
>
|
|
263
|
-
<group scale={transformProps.scale}>
|
|
264
|
-
{core}
|
|
265
|
-
{children}
|
|
266
|
-
</group>
|
|
262
|
+
<physicsDef.View properties={physics.properties}>
|
|
263
|
+
{innerGroup}
|
|
267
264
|
</physicsDef.View>
|
|
268
265
|
);
|
|
269
266
|
}
|
|
270
267
|
}
|
|
271
268
|
|
|
272
|
-
|
|
273
|
-
return (
|
|
274
|
-
<group
|
|
275
|
-
ref={(el) => registerRef(gameObject.id, el)}
|
|
276
|
-
scale={transformProps.scale}
|
|
277
|
-
{...wrapperProps}
|
|
278
|
-
>
|
|
279
|
-
{core}
|
|
280
|
-
{children}
|
|
281
|
-
</group>
|
|
282
|
-
);
|
|
269
|
+
return innerGroup;
|
|
283
270
|
}
|
|
284
271
|
|
|
285
272
|
// Helper: render an instanced GameInstance (terminal node)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Component } from "./ComponentRegistry";
|
|
2
2
|
import { useRef, useEffect } from "react";
|
|
3
|
-
import { useFrame
|
|
4
|
-
import {
|
|
3
|
+
import { useFrame } from "@react-three/fiber";
|
|
4
|
+
import { DirectionalLight, Object3D, Vector3 } from "three";
|
|
5
5
|
|
|
6
6
|
function DirectionalLightComponentEditor({ component, onUpdate }: { component: any; onUpdate: (newComp: any) => void }) {
|
|
7
7
|
const props = {
|
|
@@ -190,71 +190,31 @@ function DirectionalLightView({ properties, editMode }: { properties: any; editM
|
|
|
190
190
|
const shadowCameraRight = properties.shadowCameraRight ?? 30;
|
|
191
191
|
const targetOffset = properties.targetOffset ?? [0, -5, 0];
|
|
192
192
|
|
|
193
|
-
const { scene } = useThree();
|
|
194
193
|
const directionalLightRef = useRef<DirectionalLight>(null);
|
|
195
|
-
const targetRef = useRef<Object3D>(
|
|
196
|
-
const cameraHelperRef = useRef<CameraHelper | null>(null);
|
|
194
|
+
const targetRef = useRef<Object3D>(null);
|
|
197
195
|
|
|
198
|
-
//
|
|
196
|
+
// Set up light target reference when both refs are ready
|
|
199
197
|
useEffect(() => {
|
|
200
|
-
|
|
201
|
-
scene.add(target);
|
|
202
|
-
return () => {
|
|
203
|
-
scene.remove(target);
|
|
204
|
-
};
|
|
205
|
-
}, [scene]);
|
|
206
|
-
|
|
207
|
-
// Set up light target reference once
|
|
208
|
-
useEffect(() => {
|
|
209
|
-
if (directionalLightRef.current) {
|
|
198
|
+
if (directionalLightRef.current && targetRef.current) {
|
|
210
199
|
directionalLightRef.current.target = targetRef.current;
|
|
211
200
|
}
|
|
212
201
|
}, []);
|
|
213
202
|
|
|
214
|
-
// Update target position
|
|
203
|
+
// Update target world position based on light position + offset
|
|
215
204
|
useFrame(() => {
|
|
216
|
-
if (!directionalLightRef.current) return;
|
|
205
|
+
if (!directionalLightRef.current || !targetRef.current) return;
|
|
217
206
|
|
|
218
207
|
const lightWorldPos = new Vector3();
|
|
219
208
|
directionalLightRef.current.getWorldPosition(lightWorldPos);
|
|
220
209
|
|
|
221
|
-
|
|
210
|
+
// Target is positioned relative to light's world position
|
|
211
|
+
targetRef.current.position.set(
|
|
222
212
|
lightWorldPos.x + targetOffset[0],
|
|
223
213
|
lightWorldPos.y + targetOffset[1],
|
|
224
214
|
lightWorldPos.z + targetOffset[2]
|
|
225
215
|
);
|
|
226
|
-
|
|
227
|
-
// Only update if position actually changed
|
|
228
|
-
if (!targetRef.current.position.equals(newTargetPos)) {
|
|
229
|
-
targetRef.current.position.copy(newTargetPos);
|
|
230
|
-
if (directionalLightRef.current.shadow) {
|
|
231
|
-
directionalLightRef.current.shadow.needsUpdate = true;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Update camera helper in edit mode
|
|
236
|
-
if (editMode && cameraHelperRef.current) {
|
|
237
|
-
cameraHelperRef.current.update();
|
|
238
|
-
}
|
|
239
216
|
});
|
|
240
217
|
|
|
241
|
-
// Create/destroy camera helper for edit mode
|
|
242
|
-
useEffect(() => {
|
|
243
|
-
if (editMode && directionalLightRef.current?.shadow.camera) {
|
|
244
|
-
const helper = new CameraHelper(directionalLightRef.current.shadow.camera);
|
|
245
|
-
cameraHelperRef.current = helper;
|
|
246
|
-
scene.add(helper);
|
|
247
|
-
|
|
248
|
-
return () => {
|
|
249
|
-
if (cameraHelperRef.current) {
|
|
250
|
-
scene.remove(cameraHelperRef.current);
|
|
251
|
-
cameraHelperRef.current.dispose();
|
|
252
|
-
cameraHelperRef.current = null;
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
}, [editMode, scene]);
|
|
257
|
-
|
|
258
218
|
return (
|
|
259
219
|
<>
|
|
260
220
|
<directionalLight
|
|
@@ -262,20 +222,19 @@ function DirectionalLightView({ properties, editMode }: { properties: any; editM
|
|
|
262
222
|
color={color}
|
|
263
223
|
intensity={intensity}
|
|
264
224
|
castShadow={castShadow}
|
|
265
|
-
shadow-mapSize={
|
|
225
|
+
shadow-mapSize-width={shadowMapSize}
|
|
226
|
+
shadow-mapSize-height={shadowMapSize}
|
|
227
|
+
shadow-camera-near={shadowCameraNear}
|
|
228
|
+
shadow-camera-far={shadowCameraFar}
|
|
229
|
+
shadow-camera-top={shadowCameraTop}
|
|
230
|
+
shadow-camera-bottom={shadowCameraBottom}
|
|
231
|
+
shadow-camera-left={shadowCameraLeft}
|
|
232
|
+
shadow-camera-right={shadowCameraRight}
|
|
266
233
|
shadow-bias={-0.001}
|
|
267
234
|
shadow-normalBias={0.02}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
near={shadowCameraNear}
|
|
272
|
-
far={shadowCameraFar}
|
|
273
|
-
top={shadowCameraTop}
|
|
274
|
-
bottom={shadowCameraBottom}
|
|
275
|
-
left={shadowCameraLeft}
|
|
276
|
-
right={shadowCameraRight}
|
|
277
|
-
/>
|
|
278
|
-
</directionalLight>
|
|
235
|
+
/>
|
|
236
|
+
{/* Target object - rendered declaratively in scene graph */}
|
|
237
|
+
<object3D ref={targetRef} />
|
|
279
238
|
{editMode && (
|
|
280
239
|
<>
|
|
281
240
|
{/* Light source indicator */}
|
|
@@ -130,14 +130,16 @@ function MaterialComponentView({ properties, loadedTextures, isSelected }: { pro
|
|
|
130
130
|
const { color, wireframe = false } = properties;
|
|
131
131
|
const displayColor = isSelected ? "cyan" : color;
|
|
132
132
|
|
|
133
|
-
return
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
133
|
+
return (
|
|
134
|
+
<meshStandardMaterial
|
|
135
|
+
key={finalTexture?.uuid ?? 'no-texture'}
|
|
136
|
+
color={displayColor}
|
|
137
|
+
wireframe={wireframe}
|
|
138
|
+
map={finalTexture}
|
|
139
|
+
transparent={!!finalTexture}
|
|
140
|
+
side={DoubleSide}
|
|
141
|
+
/>
|
|
142
|
+
);
|
|
141
143
|
}
|
|
142
144
|
|
|
143
145
|
const MaterialComponent: Component = {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ModelListViewer } from '../../assetviewer/page';
|
|
2
|
-
import { useEffect, useState } from 'react';
|
|
2
|
+
import { useEffect, useState, useMemo } from 'react';
|
|
3
3
|
import { Component } from './ComponentRegistry';
|
|
4
4
|
|
|
5
5
|
function ModelComponentEditor({ component, onUpdate, basePath = "" }: { component: any; onUpdate: (newComp: any) => void; basePath?: string }) {
|
|
@@ -49,20 +49,24 @@ function ModelComponentView({ properties, loadedModels, children }: { properties
|
|
|
49
49
|
// Instanced models are handled elsewhere (GameInstance), so only render non-instanced here
|
|
50
50
|
if (!properties.filename || properties.instanced) return <>{children}</>;
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
const sourceModel = loadedModels?.[properties.filename];
|
|
53
|
+
|
|
54
|
+
// Clone model once and set up shadows - memoized to avoid cloning on every render
|
|
55
|
+
const clonedModel = useMemo(() => {
|
|
56
|
+
if (!sourceModel) return null;
|
|
57
|
+
const clone = sourceModel.clone();
|
|
58
|
+
clone.traverse((obj: any) => {
|
|
56
59
|
if (obj.isMesh) {
|
|
57
60
|
obj.castShadow = true;
|
|
58
61
|
obj.receiveShadow = true;
|
|
59
62
|
}
|
|
60
63
|
});
|
|
61
|
-
return
|
|
62
|
-
}
|
|
64
|
+
return clone;
|
|
65
|
+
}, [sourceModel]);
|
|
66
|
+
|
|
67
|
+
if (!clonedModel) return <>{children}</>;
|
|
63
68
|
|
|
64
|
-
|
|
65
|
-
return <>{children}</>;
|
|
69
|
+
return <primitive object={clonedModel}>{children}</primitive>;
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
const ModelComponent: Component = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RigidBody
|
|
1
|
+
import { RigidBody } from "@react-three/rapier";
|
|
2
2
|
import { Component } from "./ComponentRegistry";
|
|
3
3
|
|
|
4
4
|
const selectClass = "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";
|
|
@@ -25,18 +25,19 @@ function PhysicsComponentEditor({ component, onUpdate }: { component: any; onUpd
|
|
|
25
25
|
);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
interface PhysicsViewProps
|
|
29
|
-
properties: { type:
|
|
28
|
+
interface PhysicsViewProps {
|
|
29
|
+
properties: { type: 'dynamic' | 'fixed'; collider?: string };
|
|
30
30
|
editMode?: boolean;
|
|
31
|
+
children?: React.ReactNode;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
function PhysicsComponentView({ properties, editMode, children
|
|
34
|
+
function PhysicsComponentView({ properties, editMode, children }: PhysicsViewProps) {
|
|
34
35
|
if (editMode) return <>{children}</>;
|
|
35
36
|
|
|
36
37
|
const colliders = properties.collider || (properties.type === 'fixed' ? 'trimesh' : 'hull');
|
|
37
38
|
|
|
38
39
|
return (
|
|
39
|
-
<RigidBody type={properties.type} colliders={colliders as any}
|
|
40
|
+
<RigidBody type={properties.type} colliders={colliders as any}>
|
|
40
41
|
{children}
|
|
41
42
|
</RigidBody>
|
|
42
43
|
);
|
|
@@ -113,6 +113,8 @@ function SpotLightView({ properties, editMode }: { properties: any; editMode?: b
|
|
|
113
113
|
penumbra={penumbra}
|
|
114
114
|
distance={distance}
|
|
115
115
|
castShadow={castShadow}
|
|
116
|
+
shadow-mapSize-width={1024}
|
|
117
|
+
shadow-mapSize-height={1024}
|
|
116
118
|
shadow-bias={-0.0001}
|
|
117
119
|
shadow-normalBias={0.02}
|
|
118
120
|
/>
|