Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion wasm/codegen/generated/bindings.h
Original file line number Diff line number Diff line change
Expand Up @@ -5263,7 +5263,12 @@ struct MjModel {
return emscripten::val(emscripten::typed_memory_view(ptr_->ntex, ptr_->tex_nchannel));
}
emscripten::val tex_adr() const {
return emscripten::val(emscripten::typed_memory_view(ptr_->ntex, ptr_->tex_adr));
int size = ptr_->ntex;
emscripten::val result = emscripten::val::global("Int32Array").new_(size);
for (int i = 0; i < size; ++i) {
result.set(i, static_cast<int>(ptr_->tex_adr[i]));
}
return result;
}
emscripten::val tex_data() const {
return emscripten::val(emscripten::typed_memory_view(ptr_->ntexdata, ptr_->tex_data));
Expand Down
29 changes: 23 additions & 6 deletions wasm/codegen/generators/structs.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,12 +295,29 @@ def _generate_field_data(
array_size_str = parse_array_extent(extent, w, f.name)

builder = code_builder.CodeBuilder()
with builder.function(f"emscripten::val {f.name}() const"):
builder.line(
"return"
f" emscripten::val(emscripten::typed_memory_view({array_size_str},"
f" {ptr_field_expr}));"
)
if inner_type_name == "mjtSize":
# A typed_memory_view over `mjtSize` (int64_t / size_t) would surface as
# a BigInt64Array in JS, whose elements cannot be mixed with JS numbers.
# The WASM heap is 32-bit addressed so values always fit an int; return an
# Int32Array copy, consistent with scalar mjtSize field handling above.
with builder.function(f"emscripten::val {f.name}() const"):
builder.line(f"int size = {array_size_str};")
builder.line(
"emscripten::val result ="
' emscripten::val::global("Int32Array").new_(size);'
)
with builder.block("for (int i = 0; i < size; ++i)"):
builder.line(
f"result.set(i, static_cast<int>({ptr_field_expr}[i]));"
)
builder.line("return result;")
else:
with builder.function(f"emscripten::val {f.name}() const"):
builder.line(
"return"
f" emscripten::val(emscripten::typed_memory_view({array_size_str},"
f" {ptr_field_expr}));"
)
return WrappedFieldData(
declaration=builder.to_string(),
typename=_get_field_struct_type(f, s), # pyrefly: ignore[bad-argument-type]
Expand Down
64 changes: 38 additions & 26 deletions wasm/demo_app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,40 +303,44 @@ class App {
throw new Error('mjvGeom is not an instance of mujoco.MjvGeom');
}

const geomType = Number(mjvGeom.type);
const geomDataId = Number(mjvGeom.dataid);
const size = Array.from(mjvGeom.size).map((x: any) => Number(x));

// Lookup the geometry and return it if found
const key = JSON.stringify([mjvGeom.type, mjvGeom.size, mjvGeom.dataid]);
const key = JSON.stringify([geomType, size, geomDataId]);
const found = this.bufferGeometryCache.get(key);
if (found) {
return [false, found];
}

// Create geometry
let geom: THREE.BufferGeometry;
if (mjvGeom.type === mujoco.mjtGeom.mjGEOM_PLANE.value) {
if (geomType === mujoco.mjtGeom.mjGEOM_PLANE.value) {
geom = new THREE.PlaneGeometry(
2 * (mjvGeom.size[0] ? mjvGeom.size[0] : 10000),
2 * (mjvGeom.size[1] ? mjvGeom.size[1] : 10000));
2 * (size[0] ? size[0] : 10000),
2 * (size[1] ? size[1] : 10000));
const uv = geom.getAttribute('uv');
for (let i = 0; i < uv.count; ++i) {
uv.setY(i, 1 - uv.getY(i));
}
} else if (mjvGeom.type === mujoco.mjtGeom.mjGEOM_SPHERE.value) {
geom = new THREE.SphereGeometry(mjvGeom.size[0]);
} else if (mjvGeom.type === mujoco.mjtGeom.mjGEOM_CAPSULE.value) {
geom = new CapsuleGeometry(mjvGeom.size[0], 2 * mjvGeom.size[2], 32, 16);
} else if (geomType === mujoco.mjtGeom.mjGEOM_SPHERE.value) {
geom = new THREE.SphereGeometry(size[0]);
} else if (geomType === mujoco.mjtGeom.mjGEOM_CAPSULE.value) {
geom = new CapsuleGeometry(size[0], 2 * size[2], 32, 16);
geom.rotateX(0.5 * Math.PI);
} else if (mjvGeom.type === mujoco.mjtGeom.mjGEOM_BOX.value) {
} else if (geomType === mujoco.mjtGeom.mjGEOM_BOX.value) {
geom = new THREE.BoxGeometry(
2 * mjvGeom.size[0], 2 * mjvGeom.size[1], 2 * mjvGeom.size[2]);
} else if (mjvGeom.type === mujoco.mjtGeom.mjGEOM_CYLINDER.value) {
2 * size[0], 2 * size[1], 2 * size[2]);
} else if (geomType === mujoco.mjtGeom.mjGEOM_CYLINDER.value) {
geom = new THREE.CylinderGeometry(
mjvGeom.size[0], mjvGeom.size[1], 2 * mjvGeom.size[2], 32);
size[0], size[1], 2 * size[2], 32);
geom.rotateX(0.5 * Math.PI);
} else if (mjvGeom.type === mujoco.mjtGeom.mjGEOM_ELLIPSOID.value) {
} else if (geomType === mujoco.mjtGeom.mjGEOM_ELLIPSOID.value) {
geom = new THREE.SphereGeometry(1);
geom.scale(mjvGeom.size[0], mjvGeom.size[1], mjvGeom.size[2]);
geom.scale(size[0], size[1], size[2]);
} else {
console.log('Unsupported geom type: ', mjvGeom.type);
console.log('Unsupported geom type: ', geomType);
geom = new THREE.BufferGeometry();
}

Expand All @@ -353,8 +357,8 @@ class App {

// Simulate physics for 1/60 sec
if (!app.paused) {
let sim_start = app.mjData.time;
while (app.mjData.time - sim_start < 1. / 60.) {
let sim_start = Number(app.mjData.time);
while (Number(app.mjData.time) - sim_start < 1. / 60.) {
mujoco.mj_step(app.mjModel, app.mjData);
}
}
Expand All @@ -365,22 +369,30 @@ class App {
this.mjvCamera, mujoco.mjtCatBit.mjCAT_ALL.value, this.mjvScene);

const geoms = this.mjvScene.geoms;
for (let i = 0; i < geoms.size(); i++) {
const geomCount = Number(geoms.size());
for (let i = 0; i < geomCount; i++) {
const mjvGeom = geoms.get(i);

let mesh: THREE.Mesh;
const rgba = Array.from(mjvGeom.rgba).map((x: any) => Number(x));
const mat = Array.from(mjvGeom.mat).map((x: any) => Number(x));
const pos = Array.from(mjvGeom.pos).map((x: any) => Number(x));

if (i < this.meshes.length) {
mesh = this.meshes[i];
if (mesh.material instanceof THREE.MeshPhongMaterial) {
mesh.material.color.setRGB(rgba[0], rgba[1], rgba[2]);
mesh.material.opacity = rgba[3];
mesh.material.transparent = rgba[3] < 1.0;
}
} else {
const mjvGeom = geoms.get(i);
const [added, geom] = this.getBufferGeometry(mjvGeom);

// Create material
let material = new THREE.MeshPhongMaterial();
material.color.setRGB(
mjvGeom.rgba[0], mjvGeom.rgba[1], mjvGeom.rgba[2]);
material.opacity = mjvGeom.rgba[3];
material.transparent = mjvGeom.rgba[3] !== 0;
material.color.setRGB(rgba[0], rgba[1], rgba[2]);
material.opacity = rgba[3];
material.transparent = rgba[3] < 1.0;

// Create mesh
mesh = new THREE.Mesh(geom, material);
Expand All @@ -394,9 +406,9 @@ class App {
mesh.matrixAutoUpdate = false;
const sz = 1;
mesh.matrix.set(
mjvGeom.mat[0], mjvGeom.mat[1], mjvGeom.mat[2] * sz, mjvGeom.pos[0],
mjvGeom.mat[3], mjvGeom.mat[4], mjvGeom.mat[5] * sz, mjvGeom.pos[1],
mjvGeom.mat[6], mjvGeom.mat[7], mjvGeom.mat[8] * sz, mjvGeom.pos[2],
mat[0], mat[1], mat[2] * sz, pos[0],
mat[3], mat[4], mat[5] * sz, pos[1],
mat[6], mat[7], mat[8] * sz, pos[2],
0, 0, 0, 1);
mesh.matrixWorldNeedsUpdate = true;

Expand Down
5 changes: 5 additions & 0 deletions wasm/tests/bindings_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1640,6 +1640,11 @@ describe('MuJoCo WASM Bindings', () => {
expect(model).toBeDefined();
expect(model!.tex_height).toEqual(new Int32Array([512]));
expect(model!.tex_width).toEqual(new Int32Array([512]));
// tex_adr is mjtSize* (int64_t); must surface as Int32Array, not
// BigInt64Array, so that arithmetic like tex_adr[0] * 3 works without
// "can't convert BigInt to number" (fixes #3496).
expect(model!.tex_adr).toEqual(new Int32Array([0]));
expect(() => model!.tex_adr[0] * 3).not.toThrow();
} finally {
model?.delete();
unlinkXMLFile(texFilename);
Expand Down
Loading