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/vite.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
type ViteConfig = {
|
|
6
|
+
root: string;
|
|
7
|
+
};
|
|
8
|
+
type ViteServer = {
|
|
9
|
+
watcher: {
|
|
10
|
+
add(paths: string | string[]): void;
|
|
11
|
+
on(event: 'add' | 'change' | 'unlink', listener: (file: string) => void): void;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
interface MujocoReactPluginOptions {
|
|
15
|
+
/** Entry MJCF/URDF files to scan. */
|
|
16
|
+
models: string | readonly string[];
|
|
17
|
+
/** Generated declaration file. Defaults to `src/mujoco-register.gen.d.ts`. */
|
|
18
|
+
generatedRegister?: string;
|
|
19
|
+
/** Module name to augment. Defaults to `mujoco-react`. */
|
|
20
|
+
moduleName?: string;
|
|
21
|
+
/** Disable console output. */
|
|
22
|
+
disableLogging?: boolean;
|
|
23
|
+
}
|
|
24
|
+
interface MujocoRegisterCodegenOptions {
|
|
25
|
+
models: readonly string[];
|
|
26
|
+
out: string;
|
|
27
|
+
moduleName?: string;
|
|
28
|
+
root?: string;
|
|
29
|
+
}
|
|
30
|
+
interface MujocoRegisterCodegenResult {
|
|
31
|
+
out: string;
|
|
32
|
+
files: string[];
|
|
33
|
+
counts: Record<RegisterKey, number>;
|
|
34
|
+
}
|
|
35
|
+
type RegisterKey = 'actuators' | 'sensors' | 'bodies' | 'joints' | 'sites' | 'geoms' | 'keyframes';
|
|
36
|
+
declare function mujocoReact(options: MujocoReactPluginOptions): {
|
|
37
|
+
name: string;
|
|
38
|
+
enforce: "pre";
|
|
39
|
+
configResolved(config: ViteConfig): void;
|
|
40
|
+
buildStart(this: {
|
|
41
|
+
addWatchFile?: (file: string) => void;
|
|
42
|
+
}): Promise<void>;
|
|
43
|
+
configureServer(server: ViteServer): void;
|
|
44
|
+
};
|
|
45
|
+
declare function generateMujocoRegister(options: MujocoRegisterCodegenOptions): Promise<MujocoRegisterCodegenResult>;
|
|
46
|
+
|
|
47
|
+
export { type MujocoReactPluginOptions, type MujocoRegisterCodegenOptions, type MujocoRegisterCodegenResult, generateMujocoRegister, mujocoReact };
|
package/dist/vite.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { mkdir, writeFile, readFile } from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
// src/vite.ts
|
|
5
|
+
var REGISTER_KEYS = ["actuators", "sensors", "bodies", "joints", "sites", "geoms", "keyframes"];
|
|
6
|
+
var MODEL_EXTENSIONS = /* @__PURE__ */ new Set([".xml", ".mjcf", ".urdf"]);
|
|
7
|
+
function createEmptyNames() {
|
|
8
|
+
return {
|
|
9
|
+
actuators: /* @__PURE__ */ new Set(),
|
|
10
|
+
sensors: /* @__PURE__ */ new Set(),
|
|
11
|
+
bodies: /* @__PURE__ */ new Set(),
|
|
12
|
+
joints: /* @__PURE__ */ new Set(),
|
|
13
|
+
sites: /* @__PURE__ */ new Set(),
|
|
14
|
+
geoms: /* @__PURE__ */ new Set(),
|
|
15
|
+
keyframes: /* @__PURE__ */ new Set()
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function mujocoReact(options) {
|
|
19
|
+
const models = Array.isArray(options.models) ? options.models : [options.models];
|
|
20
|
+
let root = process.cwd();
|
|
21
|
+
let generatedRegister = options.generatedRegister ?? "src/mujoco-register.gen.d.ts";
|
|
22
|
+
let watchedFiles = [];
|
|
23
|
+
async function generate() {
|
|
24
|
+
const result = await generateMujocoRegister({
|
|
25
|
+
models,
|
|
26
|
+
out: generatedRegister,
|
|
27
|
+
moduleName: options.moduleName,
|
|
28
|
+
root
|
|
29
|
+
});
|
|
30
|
+
watchedFiles = result.files;
|
|
31
|
+
if (!options.disableLogging) {
|
|
32
|
+
const total = Object.values(result.counts).reduce((sum, count) => sum + count, 0);
|
|
33
|
+
console.log(`[mujoco-react] generated ${path.relative(root, result.out)} (${total} names)`);
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
name: "mujoco-react",
|
|
39
|
+
enforce: "pre",
|
|
40
|
+
configResolved(config) {
|
|
41
|
+
root = config.root;
|
|
42
|
+
generatedRegister = path.resolve(root, generatedRegister);
|
|
43
|
+
},
|
|
44
|
+
async buildStart() {
|
|
45
|
+
const result = await generate();
|
|
46
|
+
for (const file of result.files) this.addWatchFile?.(file);
|
|
47
|
+
},
|
|
48
|
+
configureServer(server) {
|
|
49
|
+
generate().then((result) => server.watcher.add(result.files)).catch((error) => {
|
|
50
|
+
console.error("[mujoco-react] register generation failed", error);
|
|
51
|
+
});
|
|
52
|
+
server.watcher.on("add", regenerateIfModelFile);
|
|
53
|
+
server.watcher.on("change", regenerateIfModelFile);
|
|
54
|
+
server.watcher.on("unlink", regenerateIfModelFile);
|
|
55
|
+
function regenerateIfModelFile(file) {
|
|
56
|
+
if (!shouldRegenerate(file, watchedFiles, models, root)) return;
|
|
57
|
+
generate().then((result) => server.watcher.add(result.files)).catch((error) => {
|
|
58
|
+
console.error("[mujoco-react] register generation failed", error);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async function generateMujocoRegister(options) {
|
|
65
|
+
const root = path.resolve(options.root ?? process.cwd());
|
|
66
|
+
const out = path.resolve(root, options.out);
|
|
67
|
+
const moduleName = options.moduleName ?? "mujoco-react";
|
|
68
|
+
const names = createEmptyNames();
|
|
69
|
+
const seen = /* @__PURE__ */ new Set();
|
|
70
|
+
for (const model of options.models) {
|
|
71
|
+
await scanModel(path.resolve(root, model), root, seen, names);
|
|
72
|
+
}
|
|
73
|
+
await mkdir(path.dirname(out), { recursive: true });
|
|
74
|
+
await writeFile(out, renderRegister(moduleName, names), "utf8");
|
|
75
|
+
return {
|
|
76
|
+
out,
|
|
77
|
+
files: [...seen].sort((a, b) => a.localeCompare(b)),
|
|
78
|
+
counts: Object.fromEntries(REGISTER_KEYS.map((key) => [key, names[key].size]))
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
async function scanModel(filePath, root, seen, names) {
|
|
82
|
+
const normalized = path.normalize(filePath);
|
|
83
|
+
if (seen.has(normalized)) return;
|
|
84
|
+
seen.add(normalized);
|
|
85
|
+
const xml = await readFile(normalized, "utf8");
|
|
86
|
+
collectSimpleTagNames(xml, "body", names.bodies);
|
|
87
|
+
collectSimpleTagNames(xml, "joint", names.joints);
|
|
88
|
+
collectSimpleTagNames(xml, "site", names.sites);
|
|
89
|
+
collectSimpleTagNames(xml, "geom", names.geoms);
|
|
90
|
+
collectSimpleTagNames(xml, "key", names.keyframes);
|
|
91
|
+
collectSectionNames(xml, "actuator", names.actuators);
|
|
92
|
+
collectSectionNames(xml, "sensor", names.sensors);
|
|
93
|
+
for (const includePath of collectIncludePaths(xml)) {
|
|
94
|
+
const next = path.resolve(path.dirname(normalized), includePath);
|
|
95
|
+
if (next.startsWith(root)) await scanModel(next, root, seen, names);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function collectSimpleTagNames(xml, tag, target) {
|
|
99
|
+
const pattern = new RegExp(`<\\s*${tag}\\b([^>]*)>`, "gi");
|
|
100
|
+
for (const match of xml.matchAll(pattern)) {
|
|
101
|
+
const name = readAttr(match[1], "name");
|
|
102
|
+
if (name) target.add(name);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function collectSectionNames(xml, section, target) {
|
|
106
|
+
const sectionPattern = new RegExp(`<\\s*${section}\\b[^>]*>([\\s\\S]*?)<\\s*/\\s*${section}\\s*>`, "gi");
|
|
107
|
+
for (const sectionMatch of xml.matchAll(sectionPattern)) {
|
|
108
|
+
const tagPattern = /<\s*[a-zA-Z0-9_:-]+\b([^>]*)>/g;
|
|
109
|
+
for (const tagMatch of sectionMatch[1].matchAll(tagPattern)) {
|
|
110
|
+
const name = readAttr(tagMatch[1], "name");
|
|
111
|
+
if (name) target.add(name);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function collectIncludePaths(xml) {
|
|
116
|
+
const result = [];
|
|
117
|
+
const pattern = /<\s*include\b([^>]*)>/gi;
|
|
118
|
+
for (const match of xml.matchAll(pattern)) {
|
|
119
|
+
const file = readAttr(match[1], "file");
|
|
120
|
+
if (file && !file.includes("://") && !file.startsWith("/")) result.push(file);
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
function readAttr(attrs, attr) {
|
|
125
|
+
const pattern = new RegExp(`\\b${attr}\\s*=\\s*(['"])(.*?)\\1`, "i");
|
|
126
|
+
return attrs.match(pattern)?.[2];
|
|
127
|
+
}
|
|
128
|
+
function renderRegister(moduleName, names) {
|
|
129
|
+
const fields = REGISTER_KEYS.filter((key) => names[key].size > 0).map((key) => ` ${key}: ${renderUnion(names[key])};`);
|
|
130
|
+
return `// Auto-generated by mujoco-react. Do not edit.
|
|
131
|
+
// Regenerate by running Vite with the mujocoReact() plugin or \`mujoco-react codegen\`.
|
|
132
|
+
|
|
133
|
+
import 'mujoco-react';
|
|
134
|
+
|
|
135
|
+
declare module '${moduleName}' {
|
|
136
|
+
interface Register {
|
|
137
|
+
${fields.join("\n")}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
`;
|
|
141
|
+
}
|
|
142
|
+
function renderUnion(values) {
|
|
143
|
+
return [...values].sort((a, b) => a.localeCompare(b)).map((value) => `'${escapeTs(value)}'`).join(" | ");
|
|
144
|
+
}
|
|
145
|
+
function escapeTs(value) {
|
|
146
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
147
|
+
}
|
|
148
|
+
function shouldRegenerate(file, watchedFiles, models, root) {
|
|
149
|
+
const absolute = path.resolve(file);
|
|
150
|
+
if (watchedFiles.includes(absolute)) return true;
|
|
151
|
+
if (!MODEL_EXTENSIONS.has(path.extname(absolute).toLowerCase())) return false;
|
|
152
|
+
const modelDirs = models.map((model) => path.dirname(path.resolve(root, model)));
|
|
153
|
+
return modelDirs.some((dir) => absolute.startsWith(dir));
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* @license
|
|
157
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
158
|
+
*/
|
|
159
|
+
|
|
160
|
+
export { generateMujocoRegister, mujocoReact };
|
|
161
|
+
//# sourceMappingURL=vite.js.map
|
|
162
|
+
//# sourceMappingURL=vite.js.map
|
package/dist/vite.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/vite.ts"],"names":[],"mappings":"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","file":"vite.js","sourcesContent":["/**\n * @license\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\n\ntype ViteConfig = { root: string };\ntype ViteServer = {\n watcher: {\n add(paths: string | string[]): void;\n on(event: 'add' | 'change' | 'unlink', listener: (file: string) => void): void;\n };\n};\n\nexport interface MujocoReactPluginOptions {\n /** Entry MJCF/URDF files to scan. */\n models: string | readonly string[];\n /** Generated declaration file. Defaults to `src/mujoco-register.gen.d.ts`. */\n generatedRegister?: string;\n /** Module name to augment. Defaults to `mujoco-react`. */\n moduleName?: string;\n /** Disable console output. */\n disableLogging?: boolean;\n}\n\nexport interface MujocoRegisterCodegenOptions {\n models: readonly string[];\n out: string;\n moduleName?: string;\n root?: string;\n}\n\nexport interface MujocoRegisterCodegenResult {\n out: string;\n files: string[];\n counts: Record<RegisterKey, number>;\n}\n\ntype RegisterKey = 'actuators' | 'sensors' | 'bodies' | 'joints' | 'sites' | 'geoms' | 'keyframes';\n\nconst REGISTER_KEYS: RegisterKey[] = ['actuators', 'sensors', 'bodies', 'joints', 'sites', 'geoms', 'keyframes'];\nconst MODEL_EXTENSIONS = new Set(['.xml', '.mjcf', '.urdf']);\n\nfunction createEmptyNames(): Record<RegisterKey, Set<string>> {\n return {\n actuators: new Set(),\n sensors: new Set(),\n bodies: new Set(),\n joints: new Set(),\n sites: new Set(),\n geoms: new Set(),\n keyframes: new Set(),\n };\n}\n\nexport function mujocoReact(options: MujocoReactPluginOptions) {\n const models = Array.isArray(options.models) ? options.models : [options.models];\n let root = process.cwd();\n let generatedRegister = options.generatedRegister ?? 'src/mujoco-register.gen.d.ts';\n let watchedFiles: string[] = [];\n\n async function generate() {\n const result = await generateMujocoRegister({\n models,\n out: generatedRegister,\n moduleName: options.moduleName,\n root,\n });\n watchedFiles = result.files;\n if (!options.disableLogging) {\n const total = Object.values(result.counts).reduce((sum, count) => sum + count, 0);\n console.log(`[mujoco-react] generated ${path.relative(root, result.out)} (${total} names)`);\n }\n return result;\n }\n\n return {\n name: 'mujoco-react',\n enforce: 'pre' as const,\n configResolved(config: ViteConfig) {\n root = config.root;\n generatedRegister = path.resolve(root, generatedRegister);\n },\n async buildStart(this: { addWatchFile?: (file: string) => void }) {\n const result = await generate();\n for (const file of result.files) this.addWatchFile?.(file);\n },\n configureServer(server: ViteServer) {\n generate().then((result) => server.watcher.add(result.files)).catch((error: unknown) => {\n console.error('[mujoco-react] register generation failed', error);\n });\n\n server.watcher.on('add', regenerateIfModelFile);\n server.watcher.on('change', regenerateIfModelFile);\n server.watcher.on('unlink', regenerateIfModelFile);\n\n function regenerateIfModelFile(file: string) {\n if (!shouldRegenerate(file, watchedFiles, models, root)) return;\n generate().then((result) => server.watcher.add(result.files)).catch((error: unknown) => {\n console.error('[mujoco-react] register generation failed', error);\n });\n }\n },\n };\n}\n\nexport async function generateMujocoRegister(\n options: MujocoRegisterCodegenOptions\n): Promise<MujocoRegisterCodegenResult> {\n const root = path.resolve(options.root ?? process.cwd());\n const out = path.resolve(root, options.out);\n const moduleName = options.moduleName ?? 'mujoco-react';\n const names = createEmptyNames();\n const seen = new Set<string>();\n\n for (const model of options.models) {\n await scanModel(path.resolve(root, model), root, seen, names);\n }\n\n await mkdir(path.dirname(out), { recursive: true });\n await writeFile(out, renderRegister(moduleName, names), 'utf8');\n\n return {\n out,\n files: [...seen].sort((a, b) => a.localeCompare(b)),\n counts: Object.fromEntries(REGISTER_KEYS.map((key) => [key, names[key].size])) as Record<RegisterKey, number>,\n };\n}\n\nasync function scanModel(\n filePath: string,\n root: string,\n seen: Set<string>,\n names: Record<RegisterKey, Set<string>>\n) {\n const normalized = path.normalize(filePath);\n if (seen.has(normalized)) return;\n seen.add(normalized);\n\n const xml = await readFile(normalized, 'utf8');\n collectSimpleTagNames(xml, 'body', names.bodies);\n collectSimpleTagNames(xml, 'joint', names.joints);\n collectSimpleTagNames(xml, 'site', names.sites);\n collectSimpleTagNames(xml, 'geom', names.geoms);\n collectSimpleTagNames(xml, 'key', names.keyframes);\n collectSectionNames(xml, 'actuator', names.actuators);\n collectSectionNames(xml, 'sensor', names.sensors);\n\n for (const includePath of collectIncludePaths(xml)) {\n const next = path.resolve(path.dirname(normalized), includePath);\n if (next.startsWith(root)) await scanModel(next, root, seen, names);\n }\n}\n\nfunction collectSimpleTagNames(xml: string, tag: string, target: Set<string>) {\n const pattern = new RegExp(`<\\\\s*${tag}\\\\b([^>]*)>`, 'gi');\n for (const match of xml.matchAll(pattern)) {\n const name = readAttr(match[1], 'name');\n if (name) target.add(name);\n }\n}\n\nfunction collectSectionNames(xml: string, section: string, target: Set<string>) {\n const sectionPattern = new RegExp(`<\\\\s*${section}\\\\b[^>]*>([\\\\s\\\\S]*?)<\\\\s*/\\\\s*${section}\\\\s*>`, 'gi');\n for (const sectionMatch of xml.matchAll(sectionPattern)) {\n const tagPattern = /<\\s*[a-zA-Z0-9_:-]+\\b([^>]*)>/g;\n for (const tagMatch of sectionMatch[1].matchAll(tagPattern)) {\n const name = readAttr(tagMatch[1], 'name');\n if (name) target.add(name);\n }\n }\n}\n\nfunction collectIncludePaths(xml: string): string[] {\n const result: string[] = [];\n const pattern = /<\\s*include\\b([^>]*)>/gi;\n for (const match of xml.matchAll(pattern)) {\n const file = readAttr(match[1], 'file');\n if (file && !file.includes('://') && !file.startsWith('/')) result.push(file);\n }\n return result;\n}\n\nfunction readAttr(attrs: string, attr: string): string | undefined {\n const pattern = new RegExp(`\\\\b${attr}\\\\s*=\\\\s*(['\"])(.*?)\\\\1`, 'i');\n return attrs.match(pattern)?.[2];\n}\n\nfunction renderRegister(moduleName: string, names: Record<RegisterKey, Set<string>>): string {\n const fields = REGISTER_KEYS\n .filter((key) => names[key].size > 0)\n .map((key) => ` ${key}: ${renderUnion(names[key])};`);\n\n return `// Auto-generated by mujoco-react. Do not edit.\n// Regenerate by running Vite with the mujocoReact() plugin or \\`mujoco-react codegen\\`.\n\nimport 'mujoco-react';\n\ndeclare module '${moduleName}' {\n interface Register {\n${fields.join('\\n')}\n }\n}\n`;\n}\n\nfunction renderUnion(values: Set<string>): string {\n return [...values].sort((a, b) => a.localeCompare(b)).map((value) => `'${escapeTs(value)}'`).join(' | ');\n}\n\nfunction escapeTs(value: string): string {\n return value.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\");\n}\n\nfunction shouldRegenerate(file: string, watchedFiles: string[], models: readonly string[], root: string): boolean {\n const absolute = path.resolve(file);\n if (watchedFiles.includes(absolute)) return true;\n if (!MODEL_EXTENSIONS.has(path.extname(absolute).toLowerCase())) return false;\n const modelDirs = models.map((model) => path.dirname(path.resolve(root, model)));\n return modelDirs.some((dir) => absolute.startsWith(dir));\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mujoco-react",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.6.0",
|
|
4
4
|
"description": "Composable React Three Fiber building blocks for MuJoCo WASM simulations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -13,9 +13,20 @@
|
|
|
13
13
|
"default": "./dist/index.js"
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
|
+
"./vite": {
|
|
17
|
+
"import": {
|
|
18
|
+
"types": "./dist/vite.d.ts",
|
|
19
|
+
"default": "./dist/vite.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
16
22
|
"./package.json": "./package.json"
|
|
17
23
|
},
|
|
24
|
+
"bin": {
|
|
25
|
+
"mujoco-react": "./bin/mujoco-react.mjs",
|
|
26
|
+
"mujoco-react-codegen": "./bin/mujoco-react-codegen.mjs"
|
|
27
|
+
},
|
|
18
28
|
"files": [
|
|
29
|
+
"bin",
|
|
19
30
|
"dist",
|
|
20
31
|
"src"
|
|
21
32
|
],
|
|
@@ -55,6 +66,7 @@
|
|
|
55
66
|
"@react-three/fiber": "^9.5.0",
|
|
56
67
|
"@semantic-release/changelog": "^6.0.3",
|
|
57
68
|
"@semantic-release/git": "^10.0.1",
|
|
69
|
+
"@types/node": "^25.9.1",
|
|
58
70
|
"@types/react": "^19.0.0",
|
|
59
71
|
"@types/three": "^0.181.0",
|
|
60
72
|
"react": "^19.2.0",
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useFrame } from '@react-three/fiber';
|
|
7
|
+
import type { ThreeElements } from '@react-three/fiber';
|
|
8
|
+
import { useMemo, useRef } from 'react';
|
|
9
|
+
import * as THREE from 'three';
|
|
10
|
+
import { useMujocoContext } from '../core/MujocoSimProvider';
|
|
11
|
+
import { getName } from '../core/SceneLoader';
|
|
12
|
+
import { GeomBuilder } from '../rendering/GeomBuilder';
|
|
13
|
+
import type { GeomInfo, MujocoModel } from '../types';
|
|
14
|
+
|
|
15
|
+
export interface InstancedGeomRendererProps extends Omit<ThreeElements['group'], 'ref'> {
|
|
16
|
+
/** Only render geoms from this MuJoCo geom group. */
|
|
17
|
+
geomGroup?: number;
|
|
18
|
+
/** Predicate for selecting geoms. */
|
|
19
|
+
filter?: (geom: GeomInfo) => boolean;
|
|
20
|
+
/** Optional material override for every instanced batch. */
|
|
21
|
+
material?: THREE.Material;
|
|
22
|
+
/** Hide or show the instanced batches. */
|
|
23
|
+
visible?: boolean;
|
|
24
|
+
castShadow?: boolean;
|
|
25
|
+
receiveShadow?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface GeomBatch {
|
|
29
|
+
key: string;
|
|
30
|
+
geomIds: number[];
|
|
31
|
+
geometry: THREE.BufferGeometry;
|
|
32
|
+
material: THREE.Material;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const GEOM_TYPE_NAMES = ['plane', 'hfield', 'sphere', 'capsule', 'ellipsoid', 'cylinder', 'box', 'mesh'];
|
|
36
|
+
const _matrix = new THREE.Matrix4();
|
|
37
|
+
|
|
38
|
+
function getGeomInfo(model: MujocoModel, geomId: number): GeomInfo {
|
|
39
|
+
const size = model.geom_size.subarray(geomId * 3, geomId * 3 + 3);
|
|
40
|
+
const type = model.geom_type[geomId];
|
|
41
|
+
return {
|
|
42
|
+
id: geomId,
|
|
43
|
+
name: getName(model, model.name_geomadr[geomId]),
|
|
44
|
+
type,
|
|
45
|
+
typeName: GEOM_TYPE_NAMES[type] ?? `type-${type}`,
|
|
46
|
+
size: [size[0], size[1], size[2]],
|
|
47
|
+
bodyId: model.geom_bodyid[geomId],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function geomSignature(model: MujocoModel, geomId: number): string {
|
|
52
|
+
const type = model.geom_type[geomId];
|
|
53
|
+
const size = Array.from(model.geom_size.subarray(geomId * 3, geomId * 3 + 3)).join(',');
|
|
54
|
+
const mat = model.geom_matid[geomId];
|
|
55
|
+
const data = model.geom_dataid[geomId];
|
|
56
|
+
const rgba = Array.from(model.geom_rgba.subarray(geomId * 4, geomId * 4 + 4)).join(',');
|
|
57
|
+
return [type, size, mat, data, rgba].join('|');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function firstMesh(object: THREE.Object3D): THREE.Mesh | null {
|
|
61
|
+
if (object instanceof THREE.Mesh) return object;
|
|
62
|
+
let mesh: THREE.Mesh | null = null;
|
|
63
|
+
object.traverse((child) => {
|
|
64
|
+
if (!mesh && child instanceof THREE.Mesh) mesh = child;
|
|
65
|
+
});
|
|
66
|
+
return mesh;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function InstancedGeomRenderer({
|
|
70
|
+
geomGroup,
|
|
71
|
+
filter,
|
|
72
|
+
material,
|
|
73
|
+
visible = true,
|
|
74
|
+
castShadow = true,
|
|
75
|
+
receiveShadow = true,
|
|
76
|
+
...groupProps
|
|
77
|
+
}: InstancedGeomRendererProps = {}) {
|
|
78
|
+
const { mjModelRef, mjDataRef, mujocoRef, status } = useMujocoContext();
|
|
79
|
+
const meshRefs = useRef<(THREE.InstancedMesh | null)[]>([]);
|
|
80
|
+
|
|
81
|
+
const batches = useMemo<GeomBatch[]>(() => {
|
|
82
|
+
if (status !== 'ready') return [];
|
|
83
|
+
const model = mjModelRef.current;
|
|
84
|
+
if (!model) return [];
|
|
85
|
+
|
|
86
|
+
const builder = new GeomBuilder(mujocoRef.current);
|
|
87
|
+
const grouped = new Map<string, number[]>();
|
|
88
|
+
|
|
89
|
+
for (let geomId = 0; geomId < model.ngeom; geomId++) {
|
|
90
|
+
if (model.geom_group[geomId] === 3) continue;
|
|
91
|
+
if (geomGroup !== undefined && model.geom_group[geomId] !== geomGroup) continue;
|
|
92
|
+
const info = getGeomInfo(model, geomId);
|
|
93
|
+
if (filter && !filter(info)) continue;
|
|
94
|
+
const key = geomSignature(model, geomId);
|
|
95
|
+
const ids = grouped.get(key);
|
|
96
|
+
if (ids) ids.push(geomId);
|
|
97
|
+
else grouped.set(key, [geomId]);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const next: GeomBatch[] = [];
|
|
101
|
+
for (const [key, geomIds] of grouped) {
|
|
102
|
+
if (geomIds.length < 2) continue;
|
|
103
|
+
const object = builder.create(model, geomIds[0]);
|
|
104
|
+
if (!object) continue;
|
|
105
|
+
const mesh = firstMesh(object);
|
|
106
|
+
if (!mesh) continue;
|
|
107
|
+
const sourceMaterial = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
|
|
108
|
+
next.push({
|
|
109
|
+
key,
|
|
110
|
+
geomIds,
|
|
111
|
+
geometry: mesh.geometry.clone(),
|
|
112
|
+
material: material ?? sourceMaterial.clone(),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return next;
|
|
117
|
+
}, [filter, geomGroup, material, mjModelRef, mujocoRef, status]);
|
|
118
|
+
|
|
119
|
+
useFrame(() => {
|
|
120
|
+
const data = mjDataRef.current;
|
|
121
|
+
if (!data || !visible) return;
|
|
122
|
+
|
|
123
|
+
batches.forEach((batch, batchIndex) => {
|
|
124
|
+
const mesh = meshRefs.current[batchIndex];
|
|
125
|
+
if (!mesh) return;
|
|
126
|
+
batch.geomIds.forEach((geomId, instanceId) => {
|
|
127
|
+
const p = geomId * 3;
|
|
128
|
+
const r = geomId * 9;
|
|
129
|
+
_matrix.set(
|
|
130
|
+
data.geom_xmat[r], data.geom_xmat[r + 1], data.geom_xmat[r + 2], data.geom_xpos[p],
|
|
131
|
+
data.geom_xmat[r + 3], data.geom_xmat[r + 4], data.geom_xmat[r + 5], data.geom_xpos[p + 1],
|
|
132
|
+
data.geom_xmat[r + 6], data.geom_xmat[r + 7], data.geom_xmat[r + 8], data.geom_xpos[p + 2],
|
|
133
|
+
0, 0, 0, 1
|
|
134
|
+
);
|
|
135
|
+
mesh.setMatrixAt(instanceId, _matrix);
|
|
136
|
+
});
|
|
137
|
+
mesh.count = batch.geomIds.length;
|
|
138
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (status !== 'ready' || batches.length === 0) return null;
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<group {...groupProps} visible={visible}>
|
|
146
|
+
{batches.map((batch, index) => (
|
|
147
|
+
<instancedMesh
|
|
148
|
+
key={batch.key}
|
|
149
|
+
ref={(mesh) => { meshRefs.current[index] = mesh; }}
|
|
150
|
+
args={[batch.geometry, batch.material, batch.geomIds.length]}
|
|
151
|
+
castShadow={castShadow}
|
|
152
|
+
receiveShadow={receiveShadow}
|
|
153
|
+
frustumCulled={false}
|
|
154
|
+
/>
|
|
155
|
+
))}
|
|
156
|
+
</group>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
@@ -17,7 +17,16 @@ import { useMujocoContext } from '../core/MujocoSimProvider';
|
|
|
17
17
|
* Accepts standard R3F group props (position, rotation, scale, visible, etc.).
|
|
18
18
|
*/
|
|
19
19
|
export function SceneRenderer(props: Omit<ThreeElements['group'], 'ref'>) {
|
|
20
|
-
const {
|
|
20
|
+
const {
|
|
21
|
+
mjModelRef,
|
|
22
|
+
mjDataRef,
|
|
23
|
+
mujocoRef,
|
|
24
|
+
onSelectionRef,
|
|
25
|
+
hiddenBodiesRef,
|
|
26
|
+
interpolateRef,
|
|
27
|
+
interpolationStateRef,
|
|
28
|
+
status,
|
|
29
|
+
} = useMujocoContext();
|
|
21
30
|
const groupRef = useRef<THREE.Group>(null);
|
|
22
31
|
const bodyRefs = useRef<(THREE.Group | null)[]>([]);
|
|
23
32
|
const prevModelRef = useRef<MujocoModel | null>(null);
|
|
@@ -70,20 +79,47 @@ export function SceneRenderer(props: Omit<ThreeElements['group'], 'ref'>) {
|
|
|
70
79
|
const data = mjDataRef.current;
|
|
71
80
|
if (!data) return;
|
|
72
81
|
const bodies = bodyRefs.current;
|
|
82
|
+
const interpolation = interpolationStateRef.current;
|
|
83
|
+
const useInterpolation = interpolateRef.current && interpolation.valid;
|
|
84
|
+
|
|
73
85
|
for (let i = 0; i < bodies.length; i++) {
|
|
74
86
|
const ref = bodies[i];
|
|
75
87
|
if (!ref) continue;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
if (useInterpolation) {
|
|
89
|
+
const alpha = interpolation.alpha;
|
|
90
|
+
const i3 = i * 3;
|
|
91
|
+
ref.position.set(
|
|
92
|
+
THREE.MathUtils.lerp(interpolation.previousXpos[i3], interpolation.currentXpos[i3], alpha),
|
|
93
|
+
THREE.MathUtils.lerp(interpolation.previousXpos[i3 + 1], interpolation.currentXpos[i3 + 1], alpha),
|
|
94
|
+
THREE.MathUtils.lerp(interpolation.previousXpos[i3 + 2], interpolation.currentXpos[i3 + 2], alpha)
|
|
95
|
+
);
|
|
96
|
+
const i4 = i * 4;
|
|
97
|
+
_previousQuat.set(
|
|
98
|
+
interpolation.previousXquat[i4 + 1],
|
|
99
|
+
interpolation.previousXquat[i4 + 2],
|
|
100
|
+
interpolation.previousXquat[i4 + 3],
|
|
101
|
+
interpolation.previousXquat[i4]
|
|
102
|
+
);
|
|
103
|
+
_currentQuat.set(
|
|
104
|
+
interpolation.currentXquat[i4 + 1],
|
|
105
|
+
interpolation.currentXquat[i4 + 2],
|
|
106
|
+
interpolation.currentXquat[i4 + 3],
|
|
107
|
+
interpolation.currentXquat[i4]
|
|
108
|
+
);
|
|
109
|
+
ref.quaternion.copy(_previousQuat).slerp(_currentQuat, alpha);
|
|
110
|
+
} else {
|
|
111
|
+
ref.position.set(
|
|
112
|
+
data.xpos[i * 3],
|
|
113
|
+
data.xpos[i * 3 + 1],
|
|
114
|
+
data.xpos[i * 3 + 2]
|
|
115
|
+
);
|
|
116
|
+
ref.quaternion.set(
|
|
117
|
+
data.xquat[i * 4 + 1],
|
|
118
|
+
data.xquat[i * 4 + 2],
|
|
119
|
+
data.xquat[i * 4 + 3],
|
|
120
|
+
data.xquat[i * 4]
|
|
121
|
+
);
|
|
122
|
+
}
|
|
87
123
|
}
|
|
88
124
|
});
|
|
89
125
|
|
|
@@ -110,3 +146,6 @@ export function SceneRenderer(props: Omit<ThreeElements['group'], 'ref'>) {
|
|
|
110
146
|
/>
|
|
111
147
|
);
|
|
112
148
|
}
|
|
149
|
+
|
|
150
|
+
const _previousQuat = new THREE.Quaternion();
|
|
151
|
+
const _currentQuat = new THREE.Quaternion();
|
|
@@ -30,6 +30,7 @@ export const MujocoCanvas = forwardRef<MujocoSimAPI, MujocoCanvasProps>(
|
|
|
30
30
|
substeps,
|
|
31
31
|
paused,
|
|
32
32
|
speed,
|
|
33
|
+
interpolate,
|
|
33
34
|
children,
|
|
34
35
|
...canvasProps
|
|
35
36
|
},
|
|
@@ -62,6 +63,7 @@ export const MujocoCanvas = forwardRef<MujocoSimAPI, MujocoCanvasProps>(
|
|
|
62
63
|
substeps={substeps}
|
|
63
64
|
paused={paused}
|
|
64
65
|
speed={speed}
|
|
66
|
+
interpolate={interpolate}
|
|
65
67
|
>
|
|
66
68
|
{children}
|
|
67
69
|
</MujocoSimProvider>
|