Render with three.js
@imvu/chkn assembles a product's Cal3D meshes and materials, and @imvu/cal3d turns each submesh into typed arrays. That's everything a WebGL engine needs. This page draws a product's static (bind-pose) geometry with three.js.
sh
npm install @imvu/chkn threeLoad the product
ts
import { CdnProductSource, Product } from '@imvu/chkn';
// Revisions from the XML-RPC gateway, resolved server-side (see Download products).
const source = new CdnProductSource({ revisions });
const product = await Product.load(productId, source);
const { parts } = product.loadModel();loadModel() returns one part per mesh slot in the product's template. Each part has the parsed mesh and a map from material slot to its parsed material. A submesh's materialId picks its slot.
If this runs in a browser, read Running in the browser first: the API and CDN may need a proxy.
Build three.js meshes
ts
import * as THREE from 'three';
import type { Material as Cal3DMaterial } from '@imvu/cal3d';
const group = new THREE.Group();
for (const part of parts) {
for (const submesh of part.mesh?.submeshes ?? []) {
const buffers = submesh.toBuffers();
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(buffers.positions, 3));
geometry.setAttribute('normal', new THREE.BufferAttribute(buffers.normals, 3));
if (buffers.texCoords[0]) {
geometry.setAttribute('uv', new THREE.BufferAttribute(buffers.texCoords[0], 2));
}
geometry.setIndex(new THREE.BufferAttribute(buffers.indices, 1));
const slot = part.materials.get(submesh.materialId);
group.add(new THREE.Mesh(geometry, await toThreeMaterial(slot?.material ?? null)));
}
}
scene.add(group);Textures
Materials name their textures. product.fetchAsset() finds each one in the product or its parents and downloads the full-size version:
ts
const textures = new Map<string, Promise<THREE.Texture>>();
function loadTexture(filename: string): Promise<THREE.Texture> {
let texture = textures.get(filename);
if (!texture) {
texture = product.fetchAsset(filename).then(async (bytes) => {
const bitmap = await createImageBitmap(new Blob([bytes]));
const result = new THREE.Texture(bitmap);
result.colorSpace = THREE.SRGBColorSpace;
result.needsUpdate = true;
return result;
});
textures.set(filename, texture);
}
return texture;
}
async function toThreeMaterial(material: Cal3DMaterial | null): Promise<THREE.Material> {
const diffuse = material?.diffuseMap?.filename;
const opacity = material?.opacityMap?.filename;
return new THREE.MeshStandardMaterial({
map: diffuse ? await loadTexture(diffuse) : null,
alphaMap: opacity ? await loadTexture(opacity) : null,
transparent: Boolean(opacity),
});
}Textures are shared between submeshes, so cache them by name as shown.
Things to adjust
- Image formats.
createImageBitmapdecodes PNG, JPEG, BMP and the like, but some products ship.tgatextures, which browsers can't decode. Use a TGA decoder for those, such as three'sTGALoader. - Orientation. If the model lies on its side or its textures appear upside down, rotate the group (for example
group.rotation.x = -Math.PI / 2) or flip thevtexture coordinate (1 - v). - Render settings. Each material slot's
spec.rawholds IMVU's settings for it (two-sided, blend mode, and so on) if you want to match IMVU's look more closely. - Animation.
buffers.skinIndicesandbuffers.skinWeightscarry up to four bone influences per vertex by default (toBuffers({ maxInfluences })). Combined withproduct.loadModel().skeletonand anAnimation, that's enough to build aTHREE.SkinnedMesh, but bone and quaternion conventions need care (see Cal3D notes).