mujoco-react 0.1.0
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/LICENSE +177 -0
- package/README.md +510 -0
- package/dist/index.d.ts +1080 -0
- package/dist/index.js +3518 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
- package/src/components/ContactListener.tsx +26 -0
- package/src/components/ContactMarkers.tsx +81 -0
- package/src/components/Debug.tsx +227 -0
- package/src/components/DragInteraction.tsx +227 -0
- package/src/components/FlexRenderer.tsx +102 -0
- package/src/components/IkGizmo.tsx +146 -0
- package/src/components/SceneLights.tsx +131 -0
- package/src/components/SceneRenderer.tsx +104 -0
- package/src/components/SelectionHighlight.tsx +69 -0
- package/src/components/TendonRenderer.tsx +84 -0
- package/src/components/TrajectoryPlayer.tsx +44 -0
- package/src/core/GenericIK.ts +339 -0
- package/src/core/MujocoCanvas.tsx +72 -0
- package/src/core/MujocoProvider.tsx +78 -0
- package/src/core/MujocoSimProvider.tsx +1201 -0
- package/src/core/SceneLoader.ts +275 -0
- package/src/hooks/useActuators.ts +36 -0
- package/src/hooks/useBodyState.ts +56 -0
- package/src/hooks/useContacts.ts +125 -0
- package/src/hooks/useCtrl.ts +40 -0
- package/src/hooks/useCtrlNoise.ts +59 -0
- package/src/hooks/useGamepad.ts +77 -0
- package/src/hooks/useGravityCompensation.ts +22 -0
- package/src/hooks/useJointState.ts +64 -0
- package/src/hooks/useKeyboardTeleop.ts +97 -0
- package/src/hooks/usePolicy.ts +56 -0
- package/src/hooks/useSensor.ts +83 -0
- package/src/hooks/useSitePosition.ts +62 -0
- package/src/hooks/useTrajectoryPlayer.ts +105 -0
- package/src/hooks/useTrajectoryRecorder.ts +97 -0
- package/src/hooks/useVideoRecorder.ts +82 -0
- package/src/index.ts +108 -0
- package/src/rendering/CapsuleGeometry.ts +35 -0
- package/src/rendering/GeomBuilder.ts +140 -0
- package/src/rendering/Reflector.ts +225 -0
- package/src/types.ts +619 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Core
|
|
7
|
+
export { MujocoProvider, useMujoco } from './core/MujocoProvider';
|
|
8
|
+
export { MujocoCanvas } from './core/MujocoCanvas';
|
|
9
|
+
export { MujocoSimProvider, useMujocoSim, useBeforePhysicsStep, useAfterPhysicsStep } from './core/MujocoSimProvider';
|
|
10
|
+
export {
|
|
11
|
+
loadScene,
|
|
12
|
+
getName,
|
|
13
|
+
findSiteByName,
|
|
14
|
+
findActuatorByName,
|
|
15
|
+
findKeyframeByName,
|
|
16
|
+
findBodyByName,
|
|
17
|
+
findJointByName,
|
|
18
|
+
findGeomByName,
|
|
19
|
+
findSensorByName,
|
|
20
|
+
findTendonByName,
|
|
21
|
+
} from './core/SceneLoader';
|
|
22
|
+
|
|
23
|
+
// Components
|
|
24
|
+
export { SceneRenderer } from './components/SceneRenderer';
|
|
25
|
+
export { IkGizmo } from './components/IkGizmo';
|
|
26
|
+
export { ContactMarkers } from './components/ContactMarkers';
|
|
27
|
+
export { DragInteraction } from './components/DragInteraction';
|
|
28
|
+
export { SceneLights } from './components/SceneLights';
|
|
29
|
+
export { Debug } from './components/Debug';
|
|
30
|
+
export { TendonRenderer } from './components/TendonRenderer';
|
|
31
|
+
export { FlexRenderer } from './components/FlexRenderer';
|
|
32
|
+
export { ContactListener } from './components/ContactListener';
|
|
33
|
+
export { TrajectoryPlayer } from './components/TrajectoryPlayer';
|
|
34
|
+
export { SelectionHighlight } from './components/SelectionHighlight';
|
|
35
|
+
|
|
36
|
+
// Hooks
|
|
37
|
+
export { useActuators } from './hooks/useActuators';
|
|
38
|
+
export { useSitePosition } from './hooks/useSitePosition';
|
|
39
|
+
export { useGravityCompensation } from './hooks/useGravityCompensation';
|
|
40
|
+
export { useSensor, useSensors } from './hooks/useSensor';
|
|
41
|
+
export { useJointState } from './hooks/useJointState';
|
|
42
|
+
export { useBodyState } from './hooks/useBodyState';
|
|
43
|
+
export { useCtrl } from './hooks/useCtrl';
|
|
44
|
+
export { useContacts, useContactEvents } from './hooks/useContacts';
|
|
45
|
+
export { useKeyboardTeleop } from './hooks/useKeyboardTeleop';
|
|
46
|
+
export { usePolicy } from './hooks/usePolicy';
|
|
47
|
+
export { useTrajectoryPlayer } from './hooks/useTrajectoryPlayer';
|
|
48
|
+
export { useTrajectoryRecorder } from './hooks/useTrajectoryRecorder';
|
|
49
|
+
export { useGamepad } from './hooks/useGamepad';
|
|
50
|
+
export { useVideoRecorder } from './hooks/useVideoRecorder';
|
|
51
|
+
export { useCtrlNoise } from './hooks/useCtrlNoise';
|
|
52
|
+
|
|
53
|
+
// Types
|
|
54
|
+
export type {
|
|
55
|
+
// Scene config
|
|
56
|
+
SceneConfig,
|
|
57
|
+
SceneObject,
|
|
58
|
+
XmlPatch,
|
|
59
|
+
SceneMarker,
|
|
60
|
+
PhysicsConfig,
|
|
61
|
+
// IK
|
|
62
|
+
IKSolveFn,
|
|
63
|
+
// Callbacks
|
|
64
|
+
PhysicsStepCallback,
|
|
65
|
+
// State management
|
|
66
|
+
StateSnapshot,
|
|
67
|
+
// Model introspection
|
|
68
|
+
BodyInfo,
|
|
69
|
+
JointInfo,
|
|
70
|
+
GeomInfo,
|
|
71
|
+
SiteInfo,
|
|
72
|
+
ActuatorInfo,
|
|
73
|
+
SensorInfo,
|
|
74
|
+
// Contacts
|
|
75
|
+
ContactInfo,
|
|
76
|
+
// Raycast
|
|
77
|
+
RayHit,
|
|
78
|
+
// Model options
|
|
79
|
+
ModelOptions,
|
|
80
|
+
// Trajectory
|
|
81
|
+
TrajectoryFrame,
|
|
82
|
+
TrajectoryData,
|
|
83
|
+
// Keyboard teleop
|
|
84
|
+
KeyBinding,
|
|
85
|
+
KeyboardTeleopConfig,
|
|
86
|
+
// Policy
|
|
87
|
+
PolicyConfig,
|
|
88
|
+
// Component props
|
|
89
|
+
IkGizmoProps,
|
|
90
|
+
DragInteractionProps,
|
|
91
|
+
DebugProps,
|
|
92
|
+
SceneLightsProps,
|
|
93
|
+
TrajectoryPlayerProps,
|
|
94
|
+
SelectionHighlightProps,
|
|
95
|
+
ContactListenerProps,
|
|
96
|
+
// API
|
|
97
|
+
MujocoSimAPI,
|
|
98
|
+
MujocoCanvasProps,
|
|
99
|
+
MujocoContextValue,
|
|
100
|
+
// Hook return types
|
|
101
|
+
SitePositionResult,
|
|
102
|
+
SensorResult,
|
|
103
|
+
BodyStateResult,
|
|
104
|
+
JointStateResult,
|
|
105
|
+
} from './types';
|
|
106
|
+
|
|
107
|
+
// Re-export MuJoCo types for convenience
|
|
108
|
+
export type { MujocoModule, MujocoModel, MujocoData } from './types';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
import * as THREE from 'three';
|
|
8
|
+
|
|
9
|
+
interface InternalBufferGeometry extends THREE.BufferGeometry {
|
|
10
|
+
type: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* CapsuleGeometry
|
|
15
|
+
* Custom geometry for capsule shape.
|
|
16
|
+
*/
|
|
17
|
+
export class CapsuleGeometry extends THREE.BufferGeometry {
|
|
18
|
+
parameters: { radius: number; length: number; capSegments: number; radialSegments: number; };
|
|
19
|
+
|
|
20
|
+
constructor(radius = 1, length = 1, capSegments = 4, radialSegments = 8) {
|
|
21
|
+
super();
|
|
22
|
+
(this as unknown as InternalBufferGeometry).type = 'CapsuleGeometry';
|
|
23
|
+
this.parameters = { radius, length, capSegments, radialSegments };
|
|
24
|
+
const path = new THREE.Path();
|
|
25
|
+
path.absarc(0, -length / 2, radius, Math.PI * 1.5, 0, false);
|
|
26
|
+
path.absarc(0, length / 2, radius, 0, Math.PI * 0.5, false);
|
|
27
|
+
const latheGeometry = new THREE.LatheGeometry(path.getPoints(capSegments), radialSegments);
|
|
28
|
+
|
|
29
|
+
const self = this as THREE.BufferGeometry;
|
|
30
|
+
self.setIndex(latheGeometry.getIndex());
|
|
31
|
+
self.setAttribute('position', latheGeometry.getAttribute('position'));
|
|
32
|
+
self.setAttribute('normal', latheGeometry.getAttribute('normal'));
|
|
33
|
+
self.setAttribute('uv', latheGeometry.getAttribute('uv'));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
import * as THREE from 'three';
|
|
8
|
+
import { CapsuleGeometry } from './CapsuleGeometry';
|
|
9
|
+
import { Reflector } from './Reflector';
|
|
10
|
+
import { MujocoModel, MujocoModule } from '../types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* GeomBuilder
|
|
14
|
+
* RESPONSIBILITY: Manufacturing visual objects.
|
|
15
|
+
*
|
|
16
|
+
* This class knows how to read a single MuJoCo 'geom' (collision shape) definition
|
|
17
|
+
* and build the corresponding Three.js Mesh for it.
|
|
18
|
+
* It handles all the different shape types (Box, Sphere, Cylinder, generic Mesh, etc.).
|
|
19
|
+
*/
|
|
20
|
+
export class GeomBuilder {
|
|
21
|
+
private mujoco: MujocoModule;
|
|
22
|
+
|
|
23
|
+
constructor(mujoco: MujocoModule) {
|
|
24
|
+
this.mujoco = mujoco;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Creates a Three.js Object3D (usually a Mesh) for a specific geometry in the MuJoCo model.
|
|
29
|
+
* Returns null if the geometry shouldn't be rendered (e.g., invisible collision triggers).
|
|
30
|
+
*/
|
|
31
|
+
create(mjModel: MujocoModel, g: number): THREE.Object3D | null {
|
|
32
|
+
// 1. Check if this geom is meant to be visible
|
|
33
|
+
// Group 3 in MuJoCo is conventionally used for invisible 'helper' geoms.
|
|
34
|
+
if (mjModel.geom_group[g] === 3) return null;
|
|
35
|
+
|
|
36
|
+
// 2. Read raw data from MuJoCo's WASM memory arrays
|
|
37
|
+
const type = mjModel.geom_type[g];
|
|
38
|
+
const size = mjModel.geom_size.subarray(g * 3, g * 3 + 3); // [x, y, z] size parameters
|
|
39
|
+
const pos = mjModel.geom_pos.subarray(g * 3, g * 3 + 3); // [x, y, z] local position
|
|
40
|
+
const quat = mjModel.geom_quat.subarray(g * 4, g * 4 + 4); // [w, x, y, z] local rotation
|
|
41
|
+
|
|
42
|
+
// 3. Determine material color
|
|
43
|
+
// Sometimes color is on the geom itself, sometimes it uses a shared material definition.
|
|
44
|
+
const matId = mjModel.geom_matid[g];
|
|
45
|
+
const color = new THREE.Color(0xffffff);
|
|
46
|
+
let opacity = 1.0;
|
|
47
|
+
|
|
48
|
+
if (matId >= 0) {
|
|
49
|
+
// Use shared material
|
|
50
|
+
const rgba = mjModel.mat_rgba.subarray(matId * 4, matId * 4 + 4);
|
|
51
|
+
color.setRGB(rgba[0], rgba[1], rgba[2]);
|
|
52
|
+
opacity = rgba[3];
|
|
53
|
+
} else {
|
|
54
|
+
// Use geom-specific color
|
|
55
|
+
const rgba = mjModel.geom_rgba.subarray(g * 4, g * 4 + 4);
|
|
56
|
+
color.setRGB(rgba[0], rgba[1], rgba[2]);
|
|
57
|
+
opacity = rgba[3];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 4. Build the Geometry based on type
|
|
61
|
+
const MG = this.mujoco.mjtGeom; // Short alias for MuJoCo Geometry Types enum
|
|
62
|
+
let geo: THREE.BufferGeometry | null = null;
|
|
63
|
+
|
|
64
|
+
// The '.value ?? MG.XYZ' pattern handles slightly different versions of the mujoco-js bindings.
|
|
65
|
+
const getVal = (v: unknown) => (v as { value: number })?.value ?? v;
|
|
66
|
+
|
|
67
|
+
if (type === getVal(MG.mjGEOM_PLANE)) {
|
|
68
|
+
// Planes are infinite in MuJoCo, but we need a finite mesh for Three.js.
|
|
69
|
+
// Fallback reduced to 5m to match grid as requested.
|
|
70
|
+
geo = new THREE.PlaneGeometry(size[0] * 2 || 5, size[1] * 2 || 5);
|
|
71
|
+
} else if (type === getVal(MG.mjGEOM_SPHERE)) {
|
|
72
|
+
geo = new THREE.SphereGeometry(size[0], 24, 24);
|
|
73
|
+
} else if (type === getVal(MG.mjGEOM_CAPSULE)) {
|
|
74
|
+
// Capsules in MuJoCo are Z-axis aligned by default.
|
|
75
|
+
// Our custom CapsuleGeometry might need rotation to match.
|
|
76
|
+
geo = new CapsuleGeometry(size[0], size[1] * 2, 24, 12);
|
|
77
|
+
geo.rotateX(Math.PI / 2);
|
|
78
|
+
} else if (type === getVal(MG.mjGEOM_BOX)) {
|
|
79
|
+
// MuJoCo defines box size as "half-extents" (center to edge). Three.js uses full width.
|
|
80
|
+
geo = new THREE.BoxGeometry(size[0] * 2, size[1] * 2, size[2] * 2);
|
|
81
|
+
} else if (type === getVal(MG.mjGEOM_CYLINDER)) {
|
|
82
|
+
geo = new THREE.CylinderGeometry(size[0], size[0], size[1] * 2, 24);
|
|
83
|
+
geo.rotateX(Math.PI / 2);
|
|
84
|
+
} else if (type === getVal(MG.mjGEOM_MESH)) {
|
|
85
|
+
// Arbitrary 3D meshes (like the robot parts).
|
|
86
|
+
// We must read the vertex and face data directly from MuJoCo's buffers.
|
|
87
|
+
const mId = mjModel.geom_dataid[g];
|
|
88
|
+
const vAdr = mjModel.mesh_vertadr[mId];
|
|
89
|
+
const vNum = mjModel.mesh_vertnum[mId];
|
|
90
|
+
const fAdr = mjModel.mesh_faceadr[mId];
|
|
91
|
+
const fNum = mjModel.mesh_facenum[mId];
|
|
92
|
+
|
|
93
|
+
geo = new THREE.BufferGeometry();
|
|
94
|
+
// 'position' attribute = vertices
|
|
95
|
+
geo.setAttribute('position', new THREE.Float32BufferAttribute(mjModel.mesh_vert.subarray(vAdr * 3, (vAdr + vNum) * 3), 3));
|
|
96
|
+
// 'index' = faces (triangles connecting vertices)
|
|
97
|
+
geo.setIndex(Array.from(mjModel.mesh_face.subarray(fAdr * 3, (fAdr + fNum) * 3)));
|
|
98
|
+
geo.computeVertexNormals(); // Auto-calculate smooth lighting normals
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 5. Construct the final Mesh
|
|
102
|
+
if (geo) {
|
|
103
|
+
let mesh;
|
|
104
|
+
// Special handling for the floor plane to make it shiny
|
|
105
|
+
if (type === getVal(MG.mjGEOM_PLANE)) {
|
|
106
|
+
mesh = new Reflector(geo, {
|
|
107
|
+
clipBias: 0.003,
|
|
108
|
+
textureWidth: 1024, textureHeight: 1024,
|
|
109
|
+
color,
|
|
110
|
+
mixStrength: 0.25
|
|
111
|
+
});
|
|
112
|
+
} else {
|
|
113
|
+
// Standard physical material for everything else
|
|
114
|
+
mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({
|
|
115
|
+
color,
|
|
116
|
+
transparent: opacity < 1,
|
|
117
|
+
opacity,
|
|
118
|
+
roughness: 0.6,
|
|
119
|
+
metalness: 0.2
|
|
120
|
+
}));
|
|
121
|
+
// Enable shadows
|
|
122
|
+
mesh.castShadow = true;
|
|
123
|
+
mesh.receiveShadow = true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Apply the local position offset and rotation specified in the MJCF XML
|
|
127
|
+
mesh.position.set(pos[0], pos[1], pos[2]);
|
|
128
|
+
// MuJoCo quaternions are [w, x, y, z], Three.js are [x, y, z, w]
|
|
129
|
+
mesh.quaternion.set(quat[1], quat[2], quat[3], quat[0]);
|
|
130
|
+
|
|
131
|
+
// Tag the mesh with its MuJoCo body and geom IDs for interaction (picking/dragging)
|
|
132
|
+
mesh.userData.bodyID = mjModel.geom_bodyid[g];
|
|
133
|
+
mesh.userData.geomID = g;
|
|
134
|
+
|
|
135
|
+
return mesh;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
import * as THREE from 'three';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Options for configuring the Reflector.
|
|
11
|
+
*/
|
|
12
|
+
export interface ReflectorOptions {
|
|
13
|
+
color?: THREE.ColorRepresentation;
|
|
14
|
+
textureWidth?: number;
|
|
15
|
+
textureHeight?: number;
|
|
16
|
+
clipBias?: number;
|
|
17
|
+
multisample?: number;
|
|
18
|
+
texture?: THREE.Texture;
|
|
19
|
+
mixStrength?: number; // How strong the reflection is (0.0 - 1.0)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ReflectorMesh extends THREE.Mesh {
|
|
23
|
+
type: string;
|
|
24
|
+
material: THREE.MeshPhysicalMaterial;
|
|
25
|
+
// tslint:disable-next-line:no-any
|
|
26
|
+
onBeforeRender: (renderer: any, scene: any, camera: any) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Reflector
|
|
31
|
+
* Creates a reflective surface.
|
|
32
|
+
*/
|
|
33
|
+
export class Reflector extends THREE.Mesh {
|
|
34
|
+
isReflector = true;
|
|
35
|
+
camera: THREE.PerspectiveCamera;
|
|
36
|
+
private reflectorPlane = new THREE.Plane();
|
|
37
|
+
private normal = new THREE.Vector3();
|
|
38
|
+
private reflectorWorldPosition = new THREE.Vector3();
|
|
39
|
+
private cameraWorldPosition = new THREE.Vector3();
|
|
40
|
+
private rotationMatrix = new THREE.Matrix4();
|
|
41
|
+
private lookAtPosition = new THREE.Vector3(0, 0, -1);
|
|
42
|
+
private clipPlane = new THREE.Vector4();
|
|
43
|
+
private view = new THREE.Vector3();
|
|
44
|
+
private target = new THREE.Vector3();
|
|
45
|
+
private q = new THREE.Vector4();
|
|
46
|
+
private textureMatrix = new THREE.Matrix4();
|
|
47
|
+
private virtualCamera: THREE.PerspectiveCamera;
|
|
48
|
+
private renderTarget: THREE.WebGLRenderTarget;
|
|
49
|
+
|
|
50
|
+
constructor(geometry: THREE.BufferGeometry, options: ReflectorOptions = {}) {
|
|
51
|
+
super(geometry);
|
|
52
|
+
|
|
53
|
+
(this as unknown as ReflectorMesh).type = 'Reflector';
|
|
54
|
+
this.camera = new THREE.PerspectiveCamera();
|
|
55
|
+
|
|
56
|
+
const color = (options.color !== undefined) ? new THREE.Color(options.color) : new THREE.Color(0x7F7F7F);
|
|
57
|
+
const textureWidth = options.textureWidth || 512;
|
|
58
|
+
const textureHeight = options.textureHeight || 512;
|
|
59
|
+
const clipBias = options.clipBias || 0;
|
|
60
|
+
const multisample = (options.multisample !== undefined) ? options.multisample : 4;
|
|
61
|
+
const blendTexture = options.texture || undefined;
|
|
62
|
+
const mixStrength = (options.mixStrength !== undefined) ? options.mixStrength : 0.25;
|
|
63
|
+
|
|
64
|
+
this.virtualCamera = this.camera;
|
|
65
|
+
|
|
66
|
+
this.renderTarget = new THREE.WebGLRenderTarget(textureWidth, textureHeight, {
|
|
67
|
+
samples: multisample,
|
|
68
|
+
type: THREE.HalfFloatType
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
(this as unknown as ReflectorMesh).material = new THREE.MeshPhysicalMaterial({
|
|
72
|
+
map: blendTexture,
|
|
73
|
+
color,
|
|
74
|
+
roughness: 0.5,
|
|
75
|
+
metalness: 0.1,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
(this as unknown as ReflectorMesh).material.onBeforeCompile = (shader) => {
|
|
79
|
+
shader.uniforms.tDiffuse = { value: this.renderTarget.texture };
|
|
80
|
+
shader.uniforms.textureMatrix = { value: this.textureMatrix };
|
|
81
|
+
shader.uniforms.mixStrength = { value: mixStrength };
|
|
82
|
+
|
|
83
|
+
// Vertex Shader: Set Vertex Positions to the Unwrapped UV Positions
|
|
84
|
+
const bodyStart = shader.vertexShader.indexOf('void main() {');
|
|
85
|
+
shader.vertexShader =
|
|
86
|
+
'uniform mat4 textureMatrix;\n' +
|
|
87
|
+
'varying vec4 vUvReflection;\n' +
|
|
88
|
+
shader.vertexShader.slice(0, bodyStart) +
|
|
89
|
+
shader.vertexShader.slice(bodyStart, -1) +
|
|
90
|
+
' vUvReflection = textureMatrix * vec4( position, 1.0 );\n' +
|
|
91
|
+
'}';
|
|
92
|
+
|
|
93
|
+
// Fragment Shader: Mix reflection with base material
|
|
94
|
+
const fragmentBodyStart = shader.fragmentShader.indexOf('void main() {');
|
|
95
|
+
shader.fragmentShader =
|
|
96
|
+
'uniform sampler2D tDiffuse;\n' +
|
|
97
|
+
'uniform float mixStrength;\n' +
|
|
98
|
+
'varying vec4 vUvReflection;\n' +
|
|
99
|
+
shader.fragmentShader.slice(0, fragmentBodyStart) +
|
|
100
|
+
shader.fragmentShader.slice(fragmentBodyStart, -1) +
|
|
101
|
+
' vec4 reflectionColor = texture2DProj( tDiffuse, vUvReflection );\n' +
|
|
102
|
+
' gl_FragColor = vec4( mix( gl_FragColor.rgb, reflectionColor.rgb, mixStrength ), gl_FragColor.a );\n' +
|
|
103
|
+
'}';
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
(this as THREE.Object3D).receiveShadow = true;
|
|
107
|
+
|
|
108
|
+
(this as unknown as ReflectorMesh).onBeforeRender = (renderer: THREE.WebGLRenderer, scene: THREE.Scene, camera: THREE.Camera) => {
|
|
109
|
+
this.reflectorWorldPosition.setFromMatrixPosition((this as THREE.Object3D).matrixWorld);
|
|
110
|
+
this.cameraWorldPosition.setFromMatrixPosition(camera.matrixWorld);
|
|
111
|
+
|
|
112
|
+
this.rotationMatrix.extractRotation((this as THREE.Object3D).matrixWorld);
|
|
113
|
+
|
|
114
|
+
this.normal.set(0, 0, 1);
|
|
115
|
+
this.normal.applyMatrix4(this.rotationMatrix);
|
|
116
|
+
|
|
117
|
+
this.view.subVectors(this.reflectorWorldPosition, this.cameraWorldPosition);
|
|
118
|
+
|
|
119
|
+
// Avoid rendering when reflector is facing away
|
|
120
|
+
if (this.view.dot(this.normal) > 0) return;
|
|
121
|
+
|
|
122
|
+
this.view.reflect(this.normal).negate();
|
|
123
|
+
this.view.add(this.reflectorWorldPosition);
|
|
124
|
+
|
|
125
|
+
this.rotationMatrix.extractRotation(camera.matrixWorld);
|
|
126
|
+
|
|
127
|
+
this.lookAtPosition.set(0, 0, -1);
|
|
128
|
+
this.lookAtPosition.applyMatrix4(this.rotationMatrix);
|
|
129
|
+
this.lookAtPosition.add(this.cameraWorldPosition);
|
|
130
|
+
|
|
131
|
+
this.target.subVectors(this.reflectorWorldPosition, this.lookAtPosition);
|
|
132
|
+
this.target.reflect(this.normal).negate();
|
|
133
|
+
this.target.add(this.reflectorWorldPosition);
|
|
134
|
+
|
|
135
|
+
this.virtualCamera.position.copy(this.view);
|
|
136
|
+
this.virtualCamera.up.set(0, 1, 0);
|
|
137
|
+
this.virtualCamera.up.applyMatrix4(this.rotationMatrix);
|
|
138
|
+
this.virtualCamera.up.reflect(this.normal);
|
|
139
|
+
this.virtualCamera.lookAt(this.target);
|
|
140
|
+
|
|
141
|
+
this.virtualCamera.far = (camera as THREE.PerspectiveCamera).far;
|
|
142
|
+
|
|
143
|
+
this.virtualCamera.updateMatrixWorld();
|
|
144
|
+
this.virtualCamera.projectionMatrix.copy((camera as THREE.PerspectiveCamera).projectionMatrix);
|
|
145
|
+
|
|
146
|
+
// Update the texture matrix
|
|
147
|
+
this.textureMatrix.set(
|
|
148
|
+
0.5, 0.0, 0.0, 0.5,
|
|
149
|
+
0.0, 0.5, 0.0, 0.5,
|
|
150
|
+
0.0, 0.0, 0.5, 0.5,
|
|
151
|
+
0.0, 0.0, 0.0, 1.0
|
|
152
|
+
);
|
|
153
|
+
this.textureMatrix.multiply(this.virtualCamera.projectionMatrix);
|
|
154
|
+
this.textureMatrix.multiply(this.virtualCamera.matrixWorldInverse);
|
|
155
|
+
this.textureMatrix.multiply((this as THREE.Object3D).matrixWorld);
|
|
156
|
+
|
|
157
|
+
// Now update projection matrix with new clip plane
|
|
158
|
+
this.reflectorPlane.setFromNormalAndCoplanarPoint(this.normal, this.reflectorWorldPosition);
|
|
159
|
+
this.reflectorPlane.applyMatrix4(this.virtualCamera.matrixWorldInverse);
|
|
160
|
+
|
|
161
|
+
this.clipPlane.set(this.reflectorPlane.normal.x, this.reflectorPlane.normal.y, this.reflectorPlane.normal.z, this.reflectorPlane.constant);
|
|
162
|
+
|
|
163
|
+
const projectionMatrix = this.virtualCamera.projectionMatrix;
|
|
164
|
+
|
|
165
|
+
this.q.x = (Math.sign(this.clipPlane.x) + projectionMatrix.elements[8]) / projectionMatrix.elements[0];
|
|
166
|
+
this.q.y = (Math.sign(this.clipPlane.y) + projectionMatrix.elements[9]) / projectionMatrix.elements[5];
|
|
167
|
+
this.q.z = -1.0;
|
|
168
|
+
this.q.w = (1.0 + projectionMatrix.elements[10]) / projectionMatrix.elements[14];
|
|
169
|
+
|
|
170
|
+
// Calculate the scaled plane vector
|
|
171
|
+
this.clipPlane.multiplyScalar(2.0 / this.clipPlane.dot(this.q));
|
|
172
|
+
|
|
173
|
+
// Replacing the third row of the projection matrix
|
|
174
|
+
projectionMatrix.elements[2] = this.clipPlane.x;
|
|
175
|
+
projectionMatrix.elements[6] = this.clipPlane.y;
|
|
176
|
+
projectionMatrix.elements[10] = this.clipPlane.z + 1.0 - clipBias;
|
|
177
|
+
projectionMatrix.elements[14] = this.clipPlane.w;
|
|
178
|
+
|
|
179
|
+
// Render
|
|
180
|
+
(this as THREE.Object3D).visible = false;
|
|
181
|
+
|
|
182
|
+
const currentRenderTarget = renderer.getRenderTarget();
|
|
183
|
+
const currentXrEnabled = renderer.xr.enabled;
|
|
184
|
+
const currentShadowAutoUpdate = renderer.shadowMap.autoUpdate;
|
|
185
|
+
|
|
186
|
+
renderer.xr.enabled = false;
|
|
187
|
+
renderer.shadowMap.autoUpdate = false;
|
|
188
|
+
|
|
189
|
+
renderer.setRenderTarget(this.renderTarget);
|
|
190
|
+
|
|
191
|
+
renderer.state.buffers.depth.setMask(true);
|
|
192
|
+
|
|
193
|
+
if (renderer.autoClear === false) renderer.clear();
|
|
194
|
+
renderer.render(scene, this.virtualCamera);
|
|
195
|
+
|
|
196
|
+
renderer.xr.enabled = currentXrEnabled;
|
|
197
|
+
renderer.shadowMap.autoUpdate = currentShadowAutoUpdate;
|
|
198
|
+
|
|
199
|
+
renderer.setRenderTarget(currentRenderTarget);
|
|
200
|
+
|
|
201
|
+
// Restore viewport
|
|
202
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
203
|
+
const viewport = (camera as any).viewport;
|
|
204
|
+
if (viewport !== undefined) {
|
|
205
|
+
renderer.state.viewport(viewport);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
(this as THREE.Object3D).visible = true;
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
getRenderTarget() {
|
|
213
|
+
return this.renderTarget;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
dispose() {
|
|
217
|
+
this.renderTarget.dispose();
|
|
218
|
+
const mesh = this as THREE.Mesh;
|
|
219
|
+
if (Array.isArray(mesh.material)) {
|
|
220
|
+
mesh.material.forEach((m) => m.dispose());
|
|
221
|
+
} else {
|
|
222
|
+
mesh.material.dispose();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|