/** * @license * SPDX-License-Identifier: Apache-2.0 * * FlexRenderer — render deformable flex bodies (spec 6.4) */ import { useEffect, useRef } from 'react'; import { useFrame } from '@react-three/fiber'; import type { ThreeElements } from '@react-three/fiber'; import * as THREE from 'three'; import { useMujocoContext } from '../core/MujocoSimProvider'; /** * Renders MuJoCo flex (deformable) bodies as dynamic meshes. * Vertices are updated every frame from flexvert_xpos. */ export function FlexRenderer(props: Omit) { const { mjModelRef, mjDataRef, status } = useMujocoContext(); const groupRef = useRef(null); const meshesRef = useRef([]); // Build flex meshes once when model is ready useEffect(() => { const model = mjModelRef.current; const group = groupRef.current; if (!model || !group || status !== 'ready') return; const nflex = model.nflex ?? 0; if (nflex === 0) return; for (let f = 0; f < nflex; f++) { const vertAdr = model.flex_vertadr[f]; const vertNum = model.flex_vertnum[f]; if (vertNum === 0) continue; const geometry = new THREE.BufferGeometry(); const positions = new Float32Array(vertNum * 3); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); // Note: flex_faceadr/flex_facenum/flex_face may not be available in all MuJoCo WASM builds. // Without face data we render as a point cloud. If future WASM versions expose // face arrays, index-based triangle rendering can be added here. geometry.computeVertexNormals(); let color = new THREE.Color(0.5, 0.5, 0.5); if (model.flex_rgba) { color = new THREE.Color( model.flex_rgba[4 * f], model.flex_rgba[4 * f + 1], model.flex_rgba[4 * f + 2], ); } const material = new THREE.MeshStandardMaterial({ color, roughness: 0.7, side: THREE.DoubleSide, }); const mesh = new THREE.Mesh(geometry, material); mesh.userData.flexId = f; mesh.userData.vertAdr = vertAdr; mesh.userData.vertNum = vertNum; group.add(mesh); meshesRef.current.push(mesh); } return () => { for (const mesh of meshesRef.current) { group.remove(mesh); mesh.geometry.dispose(); (mesh.material as THREE.Material).dispose(); } meshesRef.current = []; }; }, [status, mjModelRef]); // Update vertex positions every frame useFrame(() => { const data = mjDataRef.current; if (!data || !data.flexvert_xpos) return; for (const mesh of meshesRef.current) { const vertAdr = mesh.userData.vertAdr; const vertNum = mesh.userData.vertNum; const posAttr = mesh.geometry.getAttribute('position') as THREE.BufferAttribute; if (!posAttr) continue; for (let v = 0; v < vertNum; v++) { const srcIdx = (vertAdr + v) * 3; posAttr.setXYZ(v, data.flexvert_xpos[srcIdx], data.flexvert_xpos[srcIdx + 1], data.flexvert_xpos[srcIdx + 2]); } posAttr.needsUpdate = true; mesh.geometry.computeVertexNormals(); } }); if (status !== 'ready') return null; return ; }