mujoco-react 8.5.0 → 8.6.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/README.md +63 -10
- package/bin/mujoco-react-codegen.mjs +3 -0
- package/bin/mujoco-react.mjs +86 -0
- package/dist/index.d.ts +36 -3
- package/dist/index.js +453 -76
- package/dist/index.js.map +1 -1
- package/dist/vite.d.ts +47 -0
- package/dist/vite.js +162 -0
- package/dist/vite.js.map +1 -0
- package/package.json +13 -1
- package/src/components/InstancedGeomRenderer.tsx +158 -0
- package/src/components/SceneRenderer.tsx +51 -12
- package/src/core/MujocoCanvas.tsx +2 -0
- package/src/core/MujocoPhysics.tsx +2 -0
- package/src/core/MujocoSimProvider.tsx +138 -10
- package/src/core/SceneLoader.ts +190 -60
- package/src/index.ts +1 -0
- package/src/types.ts +19 -1
- package/src/vite.ts +223 -0
package/dist/index.js
CHANGED
|
@@ -742,7 +742,152 @@ function loadModelFromPath(mujoco, path) {
|
|
|
742
742
|
}
|
|
743
743
|
throw new Error("MuJoCo WASM module does not expose an XML path loader");
|
|
744
744
|
}
|
|
745
|
+
function isModelTextFile(fname) {
|
|
746
|
+
const lower = fname.toLowerCase();
|
|
747
|
+
return lower.endsWith(".xml") || lower.endsWith(".urdf") || lower.endsWith(".mjcf");
|
|
748
|
+
}
|
|
749
|
+
function normalizeVfsPath(path) {
|
|
750
|
+
const parts = path.replace(/\\/g, "/").split("/");
|
|
751
|
+
const norm = [];
|
|
752
|
+
for (const part of parts) {
|
|
753
|
+
if (!part || part === ".") continue;
|
|
754
|
+
if (part === "..") norm.pop();
|
|
755
|
+
else norm.push(part);
|
|
756
|
+
}
|
|
757
|
+
return norm.join("/");
|
|
758
|
+
}
|
|
759
|
+
function localFilePath(file) {
|
|
760
|
+
return normalizeVfsPath(file.webkitRelativePath || file.name);
|
|
761
|
+
}
|
|
762
|
+
function inferSceneFile(files, options) {
|
|
763
|
+
if (options?.sceneFile) return normalizeVfsPath(options.sceneFile);
|
|
764
|
+
const paths = files.map(localFilePath);
|
|
765
|
+
const preferred = ["scene.xml", "model.xml", "robot.xml", "scene.urdf", "model.urdf", "robot.urdf"];
|
|
766
|
+
for (const name of preferred) {
|
|
767
|
+
const match = paths.find((path) => path.endsWith(name));
|
|
768
|
+
if (match) return match;
|
|
769
|
+
}
|
|
770
|
+
const firstModel = paths.find(isModelTextFile);
|
|
771
|
+
if (!firstModel) throw new Error("No MJCF XML or URDF file found in FileList");
|
|
772
|
+
return firstModel;
|
|
773
|
+
}
|
|
774
|
+
function createSceneConfigFromFiles(files, options = {}) {
|
|
775
|
+
const fileArray = Array.from(files);
|
|
776
|
+
return {
|
|
777
|
+
src: "",
|
|
778
|
+
sceneFile: inferSceneFile(fileArray, options),
|
|
779
|
+
files: fileArray,
|
|
780
|
+
homeJoints: options.homeJoints,
|
|
781
|
+
xmlPatches: options.xmlPatches,
|
|
782
|
+
sceneObjects: options.sceneObjects,
|
|
783
|
+
onReset: options.onReset
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
function applyXmlPatches(text, fname, config) {
|
|
787
|
+
let result = text;
|
|
788
|
+
for (const patch of config.xmlPatches ?? []) {
|
|
789
|
+
if (fname.endsWith(patch.target) || fname === patch.target) {
|
|
790
|
+
if (patch.replace) {
|
|
791
|
+
const [from, to] = patch.replace;
|
|
792
|
+
if (result.includes(from)) {
|
|
793
|
+
result = result.replace(from, to);
|
|
794
|
+
} else {
|
|
795
|
+
const preview = from.length > 80 ? `${from.slice(0, 80)}...` : from;
|
|
796
|
+
console.warn(`XML patch replace pattern not found in ${fname}: "${preview}"`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
if (patch.inject && patch.injectAfter) {
|
|
800
|
+
const idx = result.indexOf(patch.injectAfter);
|
|
801
|
+
if (idx !== -1) {
|
|
802
|
+
const tagEnd = result.indexOf(">", idx + patch.injectAfter.length);
|
|
803
|
+
if (tagEnd !== -1) {
|
|
804
|
+
result = result.slice(0, tagEnd + 1) + patch.inject + result.slice(tagEnd + 1);
|
|
805
|
+
} else {
|
|
806
|
+
console.warn(`XML patch inject failed in ${fname}: could not find tag end after "${patch.injectAfter}"`);
|
|
807
|
+
}
|
|
808
|
+
} else {
|
|
809
|
+
const preview = patch.injectAfter.length > 80 ? `${patch.injectAfter.slice(0, 80)}...` : patch.injectAfter;
|
|
810
|
+
console.warn(`XML patch inject anchor not found in ${fname}: "${preview}"`);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
if (fname === config.sceneFile && config.sceneObjects?.length && result.includes("</worldbody>")) {
|
|
816
|
+
const xml = config.sceneObjects.map((obj) => sceneObjectToXml(obj)).join("");
|
|
817
|
+
result = result.replace("</worldbody>", xml + "</worldbody>");
|
|
818
|
+
}
|
|
819
|
+
return result;
|
|
820
|
+
}
|
|
821
|
+
async function loadSceneFromFiles(mujoco, config, onProgress) {
|
|
822
|
+
const files = config.files ?? [];
|
|
823
|
+
if (!files.length) throw new Error("loadFromFiles requires at least one File");
|
|
824
|
+
try {
|
|
825
|
+
mujoco.FS.unmount("/working");
|
|
826
|
+
} catch {
|
|
827
|
+
}
|
|
828
|
+
try {
|
|
829
|
+
mujoco.FS.mkdir("/working");
|
|
830
|
+
} catch {
|
|
831
|
+
}
|
|
832
|
+
const parser = new DOMParser();
|
|
833
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
834
|
+
const byBasename = /* @__PURE__ */ new Map();
|
|
835
|
+
const written = /* @__PURE__ */ new Set();
|
|
836
|
+
const textByPath = /* @__PURE__ */ new Map();
|
|
837
|
+
for (const file of files) {
|
|
838
|
+
const path = localFilePath(file);
|
|
839
|
+
byPath.set(path, file);
|
|
840
|
+
byBasename.set(path.split("/").pop() ?? path, file);
|
|
841
|
+
}
|
|
842
|
+
for (const [path, file] of byPath) {
|
|
843
|
+
onProgress?.(`Reading ${path}...`);
|
|
844
|
+
ensureDir(mujoco, path);
|
|
845
|
+
if (isModelTextFile(path)) {
|
|
846
|
+
const text = applyXmlPatches(await file.text(), path, config);
|
|
847
|
+
textByPath.set(path, text);
|
|
848
|
+
mujoco.FS.writeFile(`/working/${path}`, text);
|
|
849
|
+
} else {
|
|
850
|
+
mujoco.FS.writeFile(`/working/${path}`, new Uint8Array(await file.arrayBuffer()));
|
|
851
|
+
}
|
|
852
|
+
written.add(path);
|
|
853
|
+
}
|
|
854
|
+
for (const [path, text] of textByPath) {
|
|
855
|
+
const deps = collectDependencyPaths(text, path, parser);
|
|
856
|
+
for (const dep of deps) {
|
|
857
|
+
if (written.has(dep)) continue;
|
|
858
|
+
const file = byPath.get(dep) ?? byBasename.get(dep.split("/").pop() ?? dep);
|
|
859
|
+
if (!file) continue;
|
|
860
|
+
ensureDir(mujoco, dep);
|
|
861
|
+
if (isModelTextFile(dep)) {
|
|
862
|
+
mujoco.FS.writeFile(`/working/${dep}`, applyXmlPatches(await file.text(), dep, config));
|
|
863
|
+
} else {
|
|
864
|
+
mujoco.FS.writeFile(`/working/${dep}`, new Uint8Array(await file.arrayBuffer()));
|
|
865
|
+
}
|
|
866
|
+
written.add(dep);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
onProgress?.("Loading model...");
|
|
870
|
+
const mjModel = loadModelFromPath(mujoco, `/working/${config.sceneFile}`);
|
|
871
|
+
const mjData = new mujoco.MjData(mjModel);
|
|
872
|
+
applyInitialPose(mjModel, mjData, config);
|
|
873
|
+
mujoco.mj_forward(mjModel, mjData);
|
|
874
|
+
return { mjModel, mjData };
|
|
875
|
+
}
|
|
876
|
+
function applyInitialPose(mjModel, mjData, config) {
|
|
877
|
+
if (!config.homeJoints) return;
|
|
878
|
+
const homeCount = Math.min(config.homeJoints.length, Math.max(mjModel.nu, mjModel.nq));
|
|
879
|
+
for (let i = 0; i < homeCount; i++) {
|
|
880
|
+
if (i < mjModel.nu) mjData.ctrl[i] = config.homeJoints[i];
|
|
881
|
+
if (i < mjModel.nq) {
|
|
882
|
+
const qposAdr = i < mjModel.nu ? getActuatedScalarQposAdr(mjModel, i) : -1;
|
|
883
|
+
mjData.qpos[qposAdr !== -1 ? qposAdr : i] = config.homeJoints[i];
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
745
887
|
async function loadScene(mujoco, config, onProgress) {
|
|
888
|
+
if (config.files?.length) {
|
|
889
|
+
return loadSceneFromFiles(mujoco, config, onProgress);
|
|
890
|
+
}
|
|
746
891
|
try {
|
|
747
892
|
mujoco.FS.unmount("/working");
|
|
748
893
|
} catch {
|
|
@@ -760,7 +905,7 @@ async function loadScene(mujoco, config, onProgress) {
|
|
|
760
905
|
const fname = xmlQueue.shift();
|
|
761
906
|
if (downloaded.has(fname)) continue;
|
|
762
907
|
downloaded.add(fname);
|
|
763
|
-
if (!fname
|
|
908
|
+
if (!isModelTextFile(fname)) {
|
|
764
909
|
assetFiles.push(fname);
|
|
765
910
|
continue;
|
|
766
911
|
}
|
|
@@ -770,38 +915,7 @@ async function loadScene(mujoco, config, onProgress) {
|
|
|
770
915
|
console.warn(`Failed to fetch ${fname}: ${res.status} ${res.statusText}`);
|
|
771
916
|
continue;
|
|
772
917
|
}
|
|
773
|
-
|
|
774
|
-
for (const patch of config.xmlPatches ?? []) {
|
|
775
|
-
if (fname.endsWith(patch.target) || fname === patch.target) {
|
|
776
|
-
if (patch.replace) {
|
|
777
|
-
const [from, to] = patch.replace;
|
|
778
|
-
if (text.includes(from)) {
|
|
779
|
-
text = text.replace(from, to);
|
|
780
|
-
} else {
|
|
781
|
-
const preview = from.length > 80 ? `${from.slice(0, 80)}...` : from;
|
|
782
|
-
console.warn(`XML patch replace pattern not found in ${fname}: "${preview}"`);
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
if (patch.inject && patch.injectAfter) {
|
|
786
|
-
const idx = text.indexOf(patch.injectAfter);
|
|
787
|
-
if (idx !== -1) {
|
|
788
|
-
const tagEnd = text.indexOf(">", idx + patch.injectAfter.length);
|
|
789
|
-
if (tagEnd !== -1) {
|
|
790
|
-
text = text.slice(0, tagEnd + 1) + patch.inject + text.slice(tagEnd + 1);
|
|
791
|
-
} else {
|
|
792
|
-
console.warn(`XML patch inject failed in ${fname}: could not find tag end after "${patch.injectAfter}"`);
|
|
793
|
-
}
|
|
794
|
-
} else {
|
|
795
|
-
const preview = patch.injectAfter.length > 80 ? `${patch.injectAfter.slice(0, 80)}...` : patch.injectAfter;
|
|
796
|
-
console.warn(`XML patch inject anchor not found in ${fname}: "${preview}"`);
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
if (fname === config.sceneFile && config.sceneObjects?.length) {
|
|
802
|
-
const xml = config.sceneObjects.map((obj) => sceneObjectToXml(obj)).join("");
|
|
803
|
-
text = text.replace("</worldbody>", xml + "</worldbody>");
|
|
804
|
-
}
|
|
918
|
+
const text = applyXmlPatches(await res.text(), fname, config);
|
|
805
919
|
ensureDir(mujoco, fname);
|
|
806
920
|
mujoco.FS.writeFile(`/working/${fname}`, text);
|
|
807
921
|
scanDependencies(text, fname, parser, downloaded, xmlQueue);
|
|
@@ -827,48 +941,49 @@ async function loadScene(mujoco, config, onProgress) {
|
|
|
827
941
|
onProgress?.("Loading model...");
|
|
828
942
|
const mjModel = loadModelFromPath(mujoco, `/working/${config.sceneFile}`);
|
|
829
943
|
const mjData = new mujoco.MjData(mjModel);
|
|
830
|
-
|
|
831
|
-
const homeCount = Math.min(config.homeJoints.length, mjModel.nu);
|
|
832
|
-
for (let i = 0; i < homeCount; i++) {
|
|
833
|
-
mjData.ctrl[i] = config.homeJoints[i];
|
|
834
|
-
const qposAdr = getActuatedScalarQposAdr(mjModel, i);
|
|
835
|
-
if (qposAdr !== -1) {
|
|
836
|
-
mjData.qpos[qposAdr] = config.homeJoints[i];
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
}
|
|
944
|
+
applyInitialPose(mjModel, mjData, config);
|
|
840
945
|
mujoco.mj_forward(mjModel, mjData);
|
|
841
946
|
return { mjModel, mjData };
|
|
842
947
|
}
|
|
843
948
|
function scanDependencies(xmlString, currentFile, parser, downloaded, queue) {
|
|
949
|
+
for (const fullPath of collectDependencyPaths(xmlString, currentFile, parser)) {
|
|
950
|
+
if (!downloaded.has(fullPath)) queue.push(fullPath);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
function collectDependencyPaths(xmlString, currentFile, parser) {
|
|
844
954
|
const xmlDoc = parser.parseFromString(xmlString, "text/xml");
|
|
845
955
|
const compiler = xmlDoc.querySelector("compiler");
|
|
846
956
|
const assetDir = compiler?.getAttribute("assetdir") || "";
|
|
847
957
|
const meshDir = compiler?.getAttribute("meshdir") || assetDir;
|
|
848
958
|
const textureDir = compiler?.getAttribute("texturedir") || assetDir;
|
|
849
959
|
const currentDir = currentFile.includes("/") ? currentFile.substring(0, currentFile.lastIndexOf("/") + 1) : "";
|
|
850
|
-
|
|
851
|
-
|
|
960
|
+
const paths = [];
|
|
961
|
+
xmlDoc.querySelectorAll("[file], [filename]").forEach((el) => {
|
|
962
|
+
const fileAttr = el.getAttribute("file") ?? el.getAttribute("filename");
|
|
852
963
|
if (!fileAttr) return;
|
|
964
|
+
if (/^[a-z]+:\/\//i.test(fileAttr) || fileAttr.startsWith("package://")) return;
|
|
853
965
|
let prefix = "";
|
|
854
966
|
if (el.tagName.toLowerCase() === "mesh") {
|
|
855
967
|
prefix = meshDir ? meshDir + "/" : "";
|
|
856
968
|
} else if (["texture", "hfield"].includes(el.tagName.toLowerCase())) {
|
|
857
969
|
prefix = textureDir ? textureDir + "/" : "";
|
|
858
970
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
const norm = [];
|
|
862
|
-
for (const p of parts) {
|
|
863
|
-
if (p === "..") norm.pop();
|
|
864
|
-
else if (p !== ".") norm.push(p);
|
|
865
|
-
}
|
|
866
|
-
fullPath = norm.join("/");
|
|
867
|
-
if (!downloaded.has(fullPath)) queue.push(fullPath);
|
|
971
|
+
const fullPath = normalizeVfsPath(currentDir + prefix + fileAttr);
|
|
972
|
+
paths.push(fullPath);
|
|
868
973
|
});
|
|
974
|
+
return paths;
|
|
869
975
|
}
|
|
870
976
|
function SceneRenderer(props) {
|
|
871
|
-
const {
|
|
977
|
+
const {
|
|
978
|
+
mjModelRef,
|
|
979
|
+
mjDataRef,
|
|
980
|
+
mujocoRef,
|
|
981
|
+
onSelectionRef,
|
|
982
|
+
hiddenBodiesRef,
|
|
983
|
+
interpolateRef,
|
|
984
|
+
interpolationStateRef,
|
|
985
|
+
status
|
|
986
|
+
} = useMujocoContext();
|
|
872
987
|
const groupRef = useRef(null);
|
|
873
988
|
const bodyRefs = useRef([]);
|
|
874
989
|
const prevModelRef = useRef(null);
|
|
@@ -908,20 +1023,46 @@ function SceneRenderer(props) {
|
|
|
908
1023
|
const data = mjDataRef.current;
|
|
909
1024
|
if (!data) return;
|
|
910
1025
|
const bodies = bodyRefs.current;
|
|
1026
|
+
const interpolation = interpolationStateRef.current;
|
|
1027
|
+
const useInterpolation = interpolateRef.current && interpolation.valid;
|
|
911
1028
|
for (let i = 0; i < bodies.length; i++) {
|
|
912
1029
|
const ref = bodies[i];
|
|
913
1030
|
if (!ref) continue;
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1031
|
+
if (useInterpolation) {
|
|
1032
|
+
const alpha = interpolation.alpha;
|
|
1033
|
+
const i3 = i * 3;
|
|
1034
|
+
ref.position.set(
|
|
1035
|
+
THREE11.MathUtils.lerp(interpolation.previousXpos[i3], interpolation.currentXpos[i3], alpha),
|
|
1036
|
+
THREE11.MathUtils.lerp(interpolation.previousXpos[i3 + 1], interpolation.currentXpos[i3 + 1], alpha),
|
|
1037
|
+
THREE11.MathUtils.lerp(interpolation.previousXpos[i3 + 2], interpolation.currentXpos[i3 + 2], alpha)
|
|
1038
|
+
);
|
|
1039
|
+
const i4 = i * 4;
|
|
1040
|
+
_previousQuat.set(
|
|
1041
|
+
interpolation.previousXquat[i4 + 1],
|
|
1042
|
+
interpolation.previousXquat[i4 + 2],
|
|
1043
|
+
interpolation.previousXquat[i4 + 3],
|
|
1044
|
+
interpolation.previousXquat[i4]
|
|
1045
|
+
);
|
|
1046
|
+
_currentQuat.set(
|
|
1047
|
+
interpolation.currentXquat[i4 + 1],
|
|
1048
|
+
interpolation.currentXquat[i4 + 2],
|
|
1049
|
+
interpolation.currentXquat[i4 + 3],
|
|
1050
|
+
interpolation.currentXquat[i4]
|
|
1051
|
+
);
|
|
1052
|
+
ref.quaternion.copy(_previousQuat).slerp(_currentQuat, alpha);
|
|
1053
|
+
} else {
|
|
1054
|
+
ref.position.set(
|
|
1055
|
+
data.xpos[i * 3],
|
|
1056
|
+
data.xpos[i * 3 + 1],
|
|
1057
|
+
data.xpos[i * 3 + 2]
|
|
1058
|
+
);
|
|
1059
|
+
ref.quaternion.set(
|
|
1060
|
+
data.xquat[i * 4 + 1],
|
|
1061
|
+
data.xquat[i * 4 + 2],
|
|
1062
|
+
data.xquat[i * 4 + 3],
|
|
1063
|
+
data.xquat[i * 4]
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
925
1066
|
}
|
|
926
1067
|
});
|
|
927
1068
|
return /* @__PURE__ */ jsx(
|
|
@@ -948,6 +1089,8 @@ function SceneRenderer(props) {
|
|
|
948
1089
|
}
|
|
949
1090
|
);
|
|
950
1091
|
}
|
|
1092
|
+
var _previousQuat = new THREE11.Quaternion();
|
|
1093
|
+
var _currentQuat = new THREE11.Quaternion();
|
|
951
1094
|
var JOINT_TYPE_NAMES2 = ["free", "ball", "slide", "hinge"];
|
|
952
1095
|
var GEOM_TYPE_NAMES = ["plane", "hfield", "sphere", "capsule", "ellipsoid", "cylinder", "box", "mesh"];
|
|
953
1096
|
var SENSOR_TYPE_NAMES = {
|
|
@@ -1106,6 +1249,7 @@ function MujocoSimProvider({
|
|
|
1106
1249
|
substeps,
|
|
1107
1250
|
paused,
|
|
1108
1251
|
speed,
|
|
1252
|
+
interpolate,
|
|
1109
1253
|
children
|
|
1110
1254
|
}) {
|
|
1111
1255
|
const { gl, camera } = useThree();
|
|
@@ -1117,6 +1261,16 @@ function MujocoSimProvider({
|
|
|
1117
1261
|
const pausedRef = useRef(paused ?? false);
|
|
1118
1262
|
const speedRef = useRef(speed ?? 1);
|
|
1119
1263
|
const substepsRef = useRef(substeps ?? 1);
|
|
1264
|
+
const interpolateRef = useRef(interpolate ?? false);
|
|
1265
|
+
const interpolationStateRef = useRef({
|
|
1266
|
+
alpha: 1,
|
|
1267
|
+
previousXpos: new Float64Array(0),
|
|
1268
|
+
previousXquat: new Float64Array(0),
|
|
1269
|
+
currentXpos: new Float64Array(0),
|
|
1270
|
+
currentXquat: new Float64Array(0),
|
|
1271
|
+
valid: false
|
|
1272
|
+
});
|
|
1273
|
+
const physicsAccumulatorRef = useRef(0);
|
|
1120
1274
|
const stepsToRunRef = useRef(0);
|
|
1121
1275
|
const loadGenRef = useRef(0);
|
|
1122
1276
|
const onSelectionRef = useRef(onSelection);
|
|
@@ -1130,7 +1284,9 @@ function MujocoSimProvider({
|
|
|
1130
1284
|
const bodyRegistryRef = useRef(/* @__PURE__ */ new Map());
|
|
1131
1285
|
const hiddenBodiesRef = useRef(/* @__PURE__ */ new Set());
|
|
1132
1286
|
const bodyReloadTimerRef = useRef(null);
|
|
1133
|
-
|
|
1287
|
+
useEffect(() => {
|
|
1288
|
+
configRef.current = config;
|
|
1289
|
+
}, [config]);
|
|
1134
1290
|
useEffect(() => {
|
|
1135
1291
|
pausedRef.current = paused ?? false;
|
|
1136
1292
|
}, [paused]);
|
|
@@ -1140,6 +1296,9 @@ function MujocoSimProvider({
|
|
|
1140
1296
|
useEffect(() => {
|
|
1141
1297
|
substepsRef.current = substeps ?? 1;
|
|
1142
1298
|
}, [substeps]);
|
|
1299
|
+
useEffect(() => {
|
|
1300
|
+
interpolateRef.current = interpolate ?? false;
|
|
1301
|
+
}, [interpolate]);
|
|
1143
1302
|
useEffect(() => {
|
|
1144
1303
|
if (!gravity) return;
|
|
1145
1304
|
const model = mjModelRef.current;
|
|
@@ -1177,6 +1336,8 @@ function MujocoSimProvider({
|
|
|
1177
1336
|
}
|
|
1178
1337
|
mjModelRef.current = result.mjModel;
|
|
1179
1338
|
mjDataRef.current = result.mjData;
|
|
1339
|
+
physicsAccumulatorRef.current = 0;
|
|
1340
|
+
interpolationStateRef.current.valid = false;
|
|
1180
1341
|
if (gravity && result.mjModel.opt?.gravity) {
|
|
1181
1342
|
result.mjModel.opt.gravity[0] = gravity[0];
|
|
1182
1343
|
result.mjModel.opt.gravity[1] = gravity[1];
|
|
@@ -1201,6 +1362,8 @@ function MujocoSimProvider({
|
|
|
1201
1362
|
mjDataRef.current?.delete();
|
|
1202
1363
|
mjModelRef.current = null;
|
|
1203
1364
|
mjDataRef.current = null;
|
|
1365
|
+
physicsAccumulatorRef.current = 0;
|
|
1366
|
+
interpolationStateRef.current.valid = false;
|
|
1204
1367
|
try {
|
|
1205
1368
|
mujoco.FS.unmount("/working");
|
|
1206
1369
|
} catch {
|
|
@@ -1233,19 +1396,56 @@ function MujocoSimProvider({
|
|
|
1233
1396
|
cb(model, data);
|
|
1234
1397
|
}
|
|
1235
1398
|
const numSubsteps = substepsRef.current;
|
|
1236
|
-
if (
|
|
1399
|
+
if (!interpolateRef.current) {
|
|
1400
|
+
if (stepsToRunRef.current > 0) {
|
|
1401
|
+
for (let s = 0; s < stepsToRunRef.current; s++) {
|
|
1402
|
+
mujoco.mj_step(model, data);
|
|
1403
|
+
}
|
|
1404
|
+
stepsToRunRef.current = 0;
|
|
1405
|
+
} else {
|
|
1406
|
+
const startSimTime = data.time;
|
|
1407
|
+
const clampedDelta = Math.min(delta, 1 / 15);
|
|
1408
|
+
const frameTime = clampedDelta * speedRef.current;
|
|
1409
|
+
while (data.time - startSimTime < frameTime) {
|
|
1410
|
+
for (let s = 0; s < numSubsteps; s++) {
|
|
1411
|
+
mujoco.mj_step(model, data);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
} else if (stepsToRunRef.current > 0) {
|
|
1416
|
+
ensureInterpolationBuffers(model);
|
|
1417
|
+
copyBodyPose(data, interpolationStateRef.current.previousXpos, interpolationStateRef.current.previousXquat);
|
|
1237
1418
|
for (let s = 0; s < stepsToRunRef.current; s++) {
|
|
1238
1419
|
mujoco.mj_step(model, data);
|
|
1239
1420
|
}
|
|
1421
|
+
copyBodyPose(data, interpolationStateRef.current.currentXpos, interpolationStateRef.current.currentXquat);
|
|
1422
|
+
interpolationStateRef.current.alpha = 1;
|
|
1423
|
+
interpolationStateRef.current.valid = true;
|
|
1240
1424
|
stepsToRunRef.current = 0;
|
|
1241
1425
|
} else {
|
|
1242
|
-
|
|
1426
|
+
ensureInterpolationBuffers(model);
|
|
1243
1427
|
const clampedDelta = Math.min(delta, 1 / 15);
|
|
1244
|
-
|
|
1245
|
-
|
|
1428
|
+
physicsAccumulatorRef.current += clampedDelta * speedRef.current;
|
|
1429
|
+
const stepDt = Math.max((model.opt?.timestep ?? 2e-3) * Math.max(1, numSubsteps), 1e-6);
|
|
1430
|
+
let stepped = false;
|
|
1431
|
+
while (physicsAccumulatorRef.current >= stepDt) {
|
|
1432
|
+
copyBodyPose(data, interpolationStateRef.current.previousXpos, interpolationStateRef.current.previousXquat);
|
|
1246
1433
|
for (let s = 0; s < numSubsteps; s++) {
|
|
1247
1434
|
mujoco.mj_step(model, data);
|
|
1248
1435
|
}
|
|
1436
|
+
copyBodyPose(data, interpolationStateRef.current.currentXpos, interpolationStateRef.current.currentXquat);
|
|
1437
|
+
physicsAccumulatorRef.current -= stepDt;
|
|
1438
|
+
stepped = true;
|
|
1439
|
+
}
|
|
1440
|
+
if (!interpolationStateRef.current.valid) {
|
|
1441
|
+
copyBodyPose(data, interpolationStateRef.current.previousXpos, interpolationStateRef.current.previousXquat);
|
|
1442
|
+
copyBodyPose(data, interpolationStateRef.current.currentXpos, interpolationStateRef.current.currentXquat);
|
|
1443
|
+
}
|
|
1444
|
+
interpolationStateRef.current.alpha = Math.min(Math.max(physicsAccumulatorRef.current / stepDt, 0), 1);
|
|
1445
|
+
interpolationStateRef.current.valid = true;
|
|
1446
|
+
if (!stepped) {
|
|
1447
|
+
onStepRef.current?.(data.time);
|
|
1448
|
+
return;
|
|
1249
1449
|
}
|
|
1250
1450
|
}
|
|
1251
1451
|
for (const cb of afterStepCallbacks.current) {
|
|
@@ -1253,6 +1453,19 @@ function MujocoSimProvider({
|
|
|
1253
1453
|
}
|
|
1254
1454
|
onStepRef.current?.(data.time);
|
|
1255
1455
|
}, -1);
|
|
1456
|
+
function ensureInterpolationBuffers(model) {
|
|
1457
|
+
const state = interpolationStateRef.current;
|
|
1458
|
+
const xposLength = model.nbody * 3;
|
|
1459
|
+
const xquatLength = model.nbody * 4;
|
|
1460
|
+
if (state.previousXpos.length !== xposLength) state.previousXpos = new Float64Array(xposLength);
|
|
1461
|
+
if (state.currentXpos.length !== xposLength) state.currentXpos = new Float64Array(xposLength);
|
|
1462
|
+
if (state.previousXquat.length !== xquatLength) state.previousXquat = new Float64Array(xquatLength);
|
|
1463
|
+
if (state.currentXquat.length !== xquatLength) state.currentXquat = new Float64Array(xquatLength);
|
|
1464
|
+
}
|
|
1465
|
+
function copyBodyPose(data, xpos, xquat) {
|
|
1466
|
+
xpos.set(data.xpos.subarray(0, xpos.length));
|
|
1467
|
+
xquat.set(data.xquat.subarray(0, xquat.length));
|
|
1468
|
+
}
|
|
1256
1469
|
const reset = useCallback(() => {
|
|
1257
1470
|
const model = mjModelRef.current;
|
|
1258
1471
|
const data = mjDataRef.current;
|
|
@@ -1660,7 +1873,7 @@ function MujocoSimProvider({
|
|
|
1660
1873
|
mjModelRef.current = null;
|
|
1661
1874
|
mjDataRef.current = null;
|
|
1662
1875
|
setStatus("loading");
|
|
1663
|
-
const result = await loadScene(mujoco, newConfig);
|
|
1876
|
+
const result = await loadScene(mujoco, buildMergedConfig(newConfig));
|
|
1664
1877
|
if (gen !== loadGenRef.current) {
|
|
1665
1878
|
result.mjModel.delete();
|
|
1666
1879
|
result.mjData.delete();
|
|
@@ -1668,6 +1881,8 @@ function MujocoSimProvider({
|
|
|
1668
1881
|
}
|
|
1669
1882
|
mjModelRef.current = result.mjModel;
|
|
1670
1883
|
mjDataRef.current = result.mjData;
|
|
1884
|
+
physicsAccumulatorRef.current = 0;
|
|
1885
|
+
interpolationStateRef.current.valid = false;
|
|
1671
1886
|
configRef.current = newConfig;
|
|
1672
1887
|
setStatus("ready");
|
|
1673
1888
|
} catch (e) {
|
|
@@ -1681,9 +1896,36 @@ function MujocoSimProvider({
|
|
|
1681
1896
|
if (bodyReloadTimerRef.current) clearTimeout(bodyReloadTimerRef.current);
|
|
1682
1897
|
bodyReloadTimerRef.current = setTimeout(() => {
|
|
1683
1898
|
bodyReloadTimerRef.current = null;
|
|
1684
|
-
loadSceneApi(
|
|
1899
|
+
loadSceneApi(configRef.current);
|
|
1685
1900
|
}, 0);
|
|
1686
1901
|
}, [loadSceneApi]);
|
|
1902
|
+
const loadFromFilesApi = useCallback(
|
|
1903
|
+
async (files, options) => {
|
|
1904
|
+
await loadSceneApi(createSceneConfigFromFiles(files, options));
|
|
1905
|
+
},
|
|
1906
|
+
[loadSceneApi]
|
|
1907
|
+
);
|
|
1908
|
+
const addBodyApi = useCallback(async (body) => {
|
|
1909
|
+
const current = configRef.current;
|
|
1910
|
+
const sceneObjects = [
|
|
1911
|
+
...(current.sceneObjects ?? []).filter((obj) => obj.name !== body.name),
|
|
1912
|
+
body
|
|
1913
|
+
];
|
|
1914
|
+
await loadSceneApi({ ...current, sceneObjects });
|
|
1915
|
+
}, [loadSceneApi]);
|
|
1916
|
+
const removeBodyApi = useCallback(async (name) => {
|
|
1917
|
+
const current = configRef.current;
|
|
1918
|
+
bodyRegistryRef.current.delete(name);
|
|
1919
|
+
const sceneObjects = (current.sceneObjects ?? []).filter((obj) => obj.name !== name);
|
|
1920
|
+
await loadSceneApi({ ...current, sceneObjects });
|
|
1921
|
+
}, [loadSceneApi]);
|
|
1922
|
+
const recompileApi = useCallback(async (patches = []) => {
|
|
1923
|
+
const current = configRef.current;
|
|
1924
|
+
await loadSceneApi({
|
|
1925
|
+
...current,
|
|
1926
|
+
xmlPatches: patches.length ? [...current.xmlPatches ?? [], ...patches] : current.xmlPatches
|
|
1927
|
+
});
|
|
1928
|
+
}, [loadSceneApi]);
|
|
1687
1929
|
const getCanvasSnapshot = useCallback(
|
|
1688
1930
|
(width, height, mimeType = "image/jpeg") => {
|
|
1689
1931
|
if (width && height) {
|
|
@@ -1761,7 +2003,9 @@ function MujocoSimProvider({
|
|
|
1761
2003
|
get status() {
|
|
1762
2004
|
return status;
|
|
1763
2005
|
},
|
|
1764
|
-
config
|
|
2006
|
+
get config() {
|
|
2007
|
+
return configRef.current;
|
|
2008
|
+
},
|
|
1765
2009
|
reset,
|
|
1766
2010
|
setSpeed,
|
|
1767
2011
|
togglePause,
|
|
@@ -1800,6 +2044,10 @@ function MujocoSimProvider({
|
|
|
1800
2044
|
getKeyframeNames,
|
|
1801
2045
|
getKeyframeCount,
|
|
1802
2046
|
loadScene: loadSceneApi,
|
|
2047
|
+
loadFromFiles: loadFromFilesApi,
|
|
2048
|
+
addBody: addBodyApi,
|
|
2049
|
+
removeBody: removeBodyApi,
|
|
2050
|
+
recompile: recompileApi,
|
|
1803
2051
|
getCanvasSnapshot,
|
|
1804
2052
|
project2DTo3D,
|
|
1805
2053
|
setBodyMass,
|
|
@@ -1810,7 +2058,6 @@ function MujocoSimProvider({
|
|
|
1810
2058
|
}),
|
|
1811
2059
|
[
|
|
1812
2060
|
status,
|
|
1813
|
-
config,
|
|
1814
2061
|
reset,
|
|
1815
2062
|
setSpeed,
|
|
1816
2063
|
togglePause,
|
|
@@ -1849,6 +2096,10 @@ function MujocoSimProvider({
|
|
|
1849
2096
|
getKeyframeNames,
|
|
1850
2097
|
getKeyframeCount,
|
|
1851
2098
|
loadSceneApi,
|
|
2099
|
+
loadFromFilesApi,
|
|
2100
|
+
addBodyApi,
|
|
2101
|
+
removeBodyApi,
|
|
2102
|
+
recompileApi,
|
|
1852
2103
|
getCanvasSnapshot,
|
|
1853
2104
|
project2DTo3D,
|
|
1854
2105
|
setBodyMass,
|
|
@@ -1868,6 +2119,8 @@ function MujocoSimProvider({
|
|
|
1868
2119
|
pausedRef,
|
|
1869
2120
|
speedRef,
|
|
1870
2121
|
substepsRef,
|
|
2122
|
+
interpolateRef,
|
|
2123
|
+
interpolationStateRef,
|
|
1871
2124
|
onSelectionRef,
|
|
1872
2125
|
beforeStepCallbacks,
|
|
1873
2126
|
afterStepCallbacks,
|
|
@@ -1898,6 +2151,7 @@ var MujocoCanvas = forwardRef(
|
|
|
1898
2151
|
substeps,
|
|
1899
2152
|
paused,
|
|
1900
2153
|
speed,
|
|
2154
|
+
interpolate,
|
|
1901
2155
|
children,
|
|
1902
2156
|
...canvasProps
|
|
1903
2157
|
}, ref) {
|
|
@@ -1925,6 +2179,7 @@ var MujocoCanvas = forwardRef(
|
|
|
1925
2179
|
substeps,
|
|
1926
2180
|
paused,
|
|
1927
2181
|
speed,
|
|
2182
|
+
interpolate,
|
|
1928
2183
|
children
|
|
1929
2184
|
}
|
|
1930
2185
|
) });
|
|
@@ -3476,6 +3731,128 @@ function FlexRenderer(props) {
|
|
|
3476
3731
|
if (status !== "ready") return null;
|
|
3477
3732
|
return /* @__PURE__ */ jsx("group", { ...props, ref: groupRef });
|
|
3478
3733
|
}
|
|
3734
|
+
var GEOM_TYPE_NAMES2 = ["plane", "hfield", "sphere", "capsule", "ellipsoid", "cylinder", "box", "mesh"];
|
|
3735
|
+
var _matrix = new THREE11.Matrix4();
|
|
3736
|
+
function getGeomInfo(model, geomId) {
|
|
3737
|
+
const size = model.geom_size.subarray(geomId * 3, geomId * 3 + 3);
|
|
3738
|
+
const type = model.geom_type[geomId];
|
|
3739
|
+
return {
|
|
3740
|
+
id: geomId,
|
|
3741
|
+
name: getName(model, model.name_geomadr[geomId]),
|
|
3742
|
+
type,
|
|
3743
|
+
typeName: GEOM_TYPE_NAMES2[type] ?? `type-${type}`,
|
|
3744
|
+
size: [size[0], size[1], size[2]],
|
|
3745
|
+
bodyId: model.geom_bodyid[geomId]
|
|
3746
|
+
};
|
|
3747
|
+
}
|
|
3748
|
+
function geomSignature(model, geomId) {
|
|
3749
|
+
const type = model.geom_type[geomId];
|
|
3750
|
+
const size = Array.from(model.geom_size.subarray(geomId * 3, geomId * 3 + 3)).join(",");
|
|
3751
|
+
const mat = model.geom_matid[geomId];
|
|
3752
|
+
const data = model.geom_dataid[geomId];
|
|
3753
|
+
const rgba = Array.from(model.geom_rgba.subarray(geomId * 4, geomId * 4 + 4)).join(",");
|
|
3754
|
+
return [type, size, mat, data, rgba].join("|");
|
|
3755
|
+
}
|
|
3756
|
+
function firstMesh(object) {
|
|
3757
|
+
if (object instanceof THREE11.Mesh) return object;
|
|
3758
|
+
let mesh = null;
|
|
3759
|
+
object.traverse((child) => {
|
|
3760
|
+
if (!mesh && child instanceof THREE11.Mesh) mesh = child;
|
|
3761
|
+
});
|
|
3762
|
+
return mesh;
|
|
3763
|
+
}
|
|
3764
|
+
function InstancedGeomRenderer({
|
|
3765
|
+
geomGroup,
|
|
3766
|
+
filter,
|
|
3767
|
+
material,
|
|
3768
|
+
visible = true,
|
|
3769
|
+
castShadow = true,
|
|
3770
|
+
receiveShadow = true,
|
|
3771
|
+
...groupProps
|
|
3772
|
+
} = {}) {
|
|
3773
|
+
const { mjModelRef, mjDataRef, mujocoRef, status } = useMujocoContext();
|
|
3774
|
+
const meshRefs = useRef([]);
|
|
3775
|
+
const batches = useMemo(() => {
|
|
3776
|
+
if (status !== "ready") return [];
|
|
3777
|
+
const model = mjModelRef.current;
|
|
3778
|
+
if (!model) return [];
|
|
3779
|
+
const builder = new GeomBuilder(mujocoRef.current);
|
|
3780
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
3781
|
+
for (let geomId = 0; geomId < model.ngeom; geomId++) {
|
|
3782
|
+
if (model.geom_group[geomId] === 3) continue;
|
|
3783
|
+
if (geomGroup !== void 0 && model.geom_group[geomId] !== geomGroup) continue;
|
|
3784
|
+
const info = getGeomInfo(model, geomId);
|
|
3785
|
+
if (filter && !filter(info)) continue;
|
|
3786
|
+
const key = geomSignature(model, geomId);
|
|
3787
|
+
const ids = grouped.get(key);
|
|
3788
|
+
if (ids) ids.push(geomId);
|
|
3789
|
+
else grouped.set(key, [geomId]);
|
|
3790
|
+
}
|
|
3791
|
+
const next = [];
|
|
3792
|
+
for (const [key, geomIds] of grouped) {
|
|
3793
|
+
if (geomIds.length < 2) continue;
|
|
3794
|
+
const object = builder.create(model, geomIds[0]);
|
|
3795
|
+
if (!object) continue;
|
|
3796
|
+
const mesh = firstMesh(object);
|
|
3797
|
+
if (!mesh) continue;
|
|
3798
|
+
const sourceMaterial = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
|
|
3799
|
+
next.push({
|
|
3800
|
+
key,
|
|
3801
|
+
geomIds,
|
|
3802
|
+
geometry: mesh.geometry.clone(),
|
|
3803
|
+
material: material ?? sourceMaterial.clone()
|
|
3804
|
+
});
|
|
3805
|
+
}
|
|
3806
|
+
return next;
|
|
3807
|
+
}, [filter, geomGroup, material, mjModelRef, mujocoRef, status]);
|
|
3808
|
+
useFrame(() => {
|
|
3809
|
+
const data = mjDataRef.current;
|
|
3810
|
+
if (!data || !visible) return;
|
|
3811
|
+
batches.forEach((batch, batchIndex) => {
|
|
3812
|
+
const mesh = meshRefs.current[batchIndex];
|
|
3813
|
+
if (!mesh) return;
|
|
3814
|
+
batch.geomIds.forEach((geomId, instanceId) => {
|
|
3815
|
+
const p = geomId * 3;
|
|
3816
|
+
const r = geomId * 9;
|
|
3817
|
+
_matrix.set(
|
|
3818
|
+
data.geom_xmat[r],
|
|
3819
|
+
data.geom_xmat[r + 1],
|
|
3820
|
+
data.geom_xmat[r + 2],
|
|
3821
|
+
data.geom_xpos[p],
|
|
3822
|
+
data.geom_xmat[r + 3],
|
|
3823
|
+
data.geom_xmat[r + 4],
|
|
3824
|
+
data.geom_xmat[r + 5],
|
|
3825
|
+
data.geom_xpos[p + 1],
|
|
3826
|
+
data.geom_xmat[r + 6],
|
|
3827
|
+
data.geom_xmat[r + 7],
|
|
3828
|
+
data.geom_xmat[r + 8],
|
|
3829
|
+
data.geom_xpos[p + 2],
|
|
3830
|
+
0,
|
|
3831
|
+
0,
|
|
3832
|
+
0,
|
|
3833
|
+
1
|
|
3834
|
+
);
|
|
3835
|
+
mesh.setMatrixAt(instanceId, _matrix);
|
|
3836
|
+
});
|
|
3837
|
+
mesh.count = batch.geomIds.length;
|
|
3838
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
3839
|
+
});
|
|
3840
|
+
});
|
|
3841
|
+
if (status !== "ready" || batches.length === 0) return null;
|
|
3842
|
+
return /* @__PURE__ */ jsx("group", { ...groupProps, visible, children: batches.map((batch, index) => /* @__PURE__ */ jsx(
|
|
3843
|
+
"instancedMesh",
|
|
3844
|
+
{
|
|
3845
|
+
ref: (mesh) => {
|
|
3846
|
+
meshRefs.current[index] = mesh;
|
|
3847
|
+
},
|
|
3848
|
+
args: [batch.geometry, batch.material, batch.geomIds.length],
|
|
3849
|
+
castShadow,
|
|
3850
|
+
receiveShadow,
|
|
3851
|
+
frustumCulled: false
|
|
3852
|
+
},
|
|
3853
|
+
batch.key
|
|
3854
|
+
)) });
|
|
3855
|
+
}
|
|
3479
3856
|
var geomNameCacheByModel = /* @__PURE__ */ new WeakMap();
|
|
3480
3857
|
function getGeomNameCached(model, geomId) {
|
|
3481
3858
|
let perModel = geomNameCacheByModel.get(model);
|
|
@@ -4668,6 +5045,6 @@ function useCameraAnimation() {
|
|
|
4668
5045
|
* useCameraAnimation — composable camera animation hook.
|
|
4669
5046
|
*/
|
|
4670
5047
|
|
|
4671
|
-
export { Body, ContactListener, ContactMarkers, Debug, DragInteraction, FlexRenderer, IkGizmo, MujocoCanvas, MujocoPhysics, MujocoProvider, MujocoSimProvider, SceneLights, TendonRenderer, TrajectoryPlayer, buildObservation, createContiguousControlGroup, createController, createControllerHook, findActuatorByName, findBodyByName, findGeomByName, findJointByName, findKeyframeByName, findSensorByName, findSiteByName, findTendonByName, getActuatedJoints, getContact, getControlMap, getName, loadScene, resolveControlGroup, useActuators, useAfterPhysicsStep, useBeforePhysicsStep, useBodyMeshes, useBodyState, useCameraAnimation, useContactEvents, useContacts, useCtrl, useCtrlNoise, useGamepad, useGravityCompensation, useIkController, useJointState, useKeyboardTeleop, useMujoco, useMujocoWasm, useObservation, usePolicy, useSceneLights, useSelectionHighlight, useSensor, useSensors, useSitePosition, useTrajectoryPlayer, useTrajectoryRecorder, useVideoRecorder };
|
|
5048
|
+
export { Body, ContactListener, ContactMarkers, Debug, DragInteraction, FlexRenderer, IkGizmo, InstancedGeomRenderer, MujocoCanvas, MujocoPhysics, MujocoProvider, MujocoSimProvider, SceneLights, TendonRenderer, TrajectoryPlayer, buildObservation, createContiguousControlGroup, createController, createControllerHook, findActuatorByName, findBodyByName, findGeomByName, findJointByName, findKeyframeByName, findSensorByName, findSiteByName, findTendonByName, getActuatedJoints, getContact, getControlMap, getName, loadScene, resolveControlGroup, useActuators, useAfterPhysicsStep, useBeforePhysicsStep, useBodyMeshes, useBodyState, useCameraAnimation, useContactEvents, useContacts, useCtrl, useCtrlNoise, useGamepad, useGravityCompensation, useIkController, useJointState, useKeyboardTeleop, useMujoco, useMujocoWasm, useObservation, usePolicy, useSceneLights, useSelectionHighlight, useSensor, useSensors, useSitePosition, useTrajectoryPlayer, useTrajectoryRecorder, useVideoRecorder };
|
|
4672
5049
|
//# sourceMappingURL=index.js.map
|
|
4673
5050
|
//# sourceMappingURL=index.js.map
|