tfrere's picture
tfrere HF Staff
feat: standalone Microduck RL playground (MuJoCo WASM + ONNX policies)
c8f39b4
Raw History Blame
8.78 kB
// Colour variants of the real Microduck robots (see the reference photo of
// the four printed units). Each variant fills the same semantic slots; the
// mesh-filename -> slot mapping lives in meshMaterialsFor(). All colours are
// linear-space RGB and slightly oversaturated: the renderer tone-maps with
// ACESFilmicToneMapping under warm key/rim lights, which desaturates and
// shifts hues toward gold, so the base colours lean past the target to land
// right on screen.
import * as THREE from "three";
// ── Shared slot specs (identical on all four robots) ───────────────────
// sRGB targets from the reference photo, converted to linear with a
// slight saturation boost (x1.12) so ACES lands on the clean tint.
const AMBER_YELLOW = { color: [1.0, 0.413, 0.007], roughness: 0.4, metalness: 0.0 }; // #ffb52e
const BRIGHT_ORANGE = { color: [1.0, 0.144, 0.008], roughness: 0.45, metalness: 0.0 }; // #ff7a2f
const AMBER = { color: [0.847, 0.339, 0.022], roughness: 0.45, metalness: 0.0 }; // #eda63e
const CLEAN_YELLOW = { color: [1.0, 0.608, 0.021], roughness: 0.4, metalness: 0.0 }; // #ffd23f
const CREAM = { color: [0.888, 0.86, 0.798], roughness: 0.35, metalness: 0.0 }; // #f2efe8
const DARK = { color: [0.012, 0.012, 0.014], roughness: 0.55, metalness: 0.3 }; // #1d1d1f
const GRAY = { color: [0.256, 0.256, 0.279], roughness: 0.5, metalness: 0.35 }; // #8b8b90
// Camera-lens eye: very dark blue-black, glossy like coated glass.
const LENS = { color: [0.01, 0.012, 0.02], roughness: 0.05, metalness: 0.0 };
// Per-variant colours.
const WARM_GRAY = { color: [0.328, 0.312, 0.283], roughness: 0.35, metalness: 0.0 }; // #9b9892
const AMBER_ORANGE = { color: [0.815, 0.321, 0.019], roughness: 0.5, metalness: 0.0 }; // #e9a23b
const CHARCOAL = { color: [0.028, 0.028, 0.033], roughness: 0.5, metalness: 0.0 }; // #2f2f33
const CHARCOAL_FACE = { color: [0.044, 0.044, 0.051], roughness: 0.5, metalness: 0.0 }; // #3c3c40
const CHARCOAL_BODY = { color: [0.017, 0.017, 0.019], roughness: 0.5, metalness: 0.0 }; // #232326
const LIGHT_WARM_GRAY = { color: [0.485, 0.459, 0.416], roughness: 0.35, metalness: 0.0 };// #b9b5ae
const PURPLE_RING = { color: [0.114, 0.058, 0.578], roughness: 0.4, metalness: 0.0 }; // #6a52c8
const PURPLE_FEET = { color: [0.164, 0.085, 0.584], roughness: 0.45, metalness: 0.0 }; // #7a5fc9
const PALE_BLUE = { color: [0.441, 0.613, 0.723], roughness: 0.35, metalness: 0.0 }; // #b6cfdd
const MEDIUM_BLUE = { color: [0.13, 0.321, 0.552], roughness: 0.35, metalness: 0.0 }; // #6f9ec4
const BLUE_GRAY = { color: [0.183, 0.379, 0.565], roughness: 0.35, metalness: 0.0 }; // #7fa9c6
const FEET_YELLOW = { color: [0.784, 0.455, 0.023], roughness: 0.5, metalness: 0.0 }; // #e5b93e
// Slots (validated against the four-robot reference photo): headDome
// (top shell), facePlate (around the eye), trim (band under the shell +
// upper beak base), beakUpper (mouth-roof plate), beakLower (jaw plates),
// eyeRing, lens, bodyShell (trunk shells), legShells (upper-leg shells),
// feet (foot blocks + ankle brackets), soles, mechDark, mechGray.
export const VARIANTS = {
// Front-center robot: warm gray head, cream body, amber eye, orange
// trim/upper beak over a lighter amber lower beak, amber-orange feet.
classic: {
headDome: WARM_GRAY,
facePlate: WARM_GRAY,
trim: BRIGHT_ORANGE,
beakUpper: BRIGHT_ORANGE,
beakLower: AMBER,
eyeRing: AMBER_YELLOW,
lens: LENS,
bodyShell: CREAM,
legShells: CREAM,
feet: AMBER_ORANGE,
soles: AMBER_ORANGE,
mechDark: DARK,
mechGray: GRAY,
},
// Front-left robot: charcoal shells, slightly lighter face plate,
// amber eye, orange/amber beak, yellow soles peeking under black feet.
charcoal: {
headDome: CHARCOAL,
facePlate: CHARCOAL_FACE,
trim: BRIGHT_ORANGE,
beakUpper: BRIGHT_ORANGE,
beakLower: AMBER,
eyeRing: AMBER_YELLOW,
lens: LENS,
bodyShell: CHARCOAL_BODY,
legShells: CHARCOAL_BODY,
feet: CHARCOAL_BODY,
soles: CLEAN_YELLOW,
mechDark: DARK,
mechGray: GRAY,
},
// Back-center robot: light warm gray head, purple eye, YELLOW trim and
// beak (both plates), cream body, purple feet.
purple: {
headDome: LIGHT_WARM_GRAY,
facePlate: LIGHT_WARM_GRAY,
trim: CLEAN_YELLOW,
beakUpper: CLEAN_YELLOW,
beakLower: CLEAN_YELLOW,
eyeRing: PURPLE_RING,
lens: LENS,
bodyShell: CREAM,
legShells: CREAM,
feet: PURPLE_FEET,
soles: PURPLE_FEET,
mechDark: DARK,
mechGray: GRAY,
},
// Right robot: pale blue dome over a medium blue face plate (two
// different blues), amber eye, orange/amber beak, blue-gray body,
// clean yellow feet.
blue: {
headDome: PALE_BLUE,
facePlate: MEDIUM_BLUE,
trim: BRIGHT_ORANGE,
beakUpper: BRIGHT_ORANGE,
beakLower: AMBER,
eyeRing: AMBER_YELLOW,
lens: LENS,
bodyShell: BLUE_GRAY,
legShells: BLUE_GRAY,
feet: FEET_YELLOW,
soles: FEET_YELLOW,
mechDark: DARK,
mechGray: GRAY,
},
};
export const VARIANT_NAMES = Object.keys(VARIANTS);
export const DEFAULT_VARIANT = "classic";
export const randomVariantName = () =>
VARIANT_NAMES[Math.floor(Math.random() * VARIANT_NAMES.length)];
// Mesh filename -> material spec for one variant (mjlab model meshes).
// Anything not listed is small mechanical hardware and falls back to
// mechGray. Verified visually against the four-robot reference photo.
export function meshMaterialsFor(v) {
return {
// Head: dome, band under it, face plate around the eye.
"top_head_shell.stl": v.headDome,
"bottom_head_shell.stl": v.trim,
"face_part.stl": v.facePlate,
// The eye: printed ring + camera lens behind it.
"noenoeil.stl": v.eyeRing,
"lens.stl": v.lens,
"m12_lens_holder.stl": v.mechDark,
// Beak: mouth-roof plate on top, rigid jaw + soft pad below.
"soft_mouth_top.stl": v.beakUpper,
"jaw.stl": v.beakLower,
"jaw_soft.stl": v.beakLower,
// Body shells
"trunk_base.stl": v.bodyShell,
"left_shell.stl": v.bodyShell,
"right_shell.stl": v.bodyShell,
"upper_leg_left.stl": v.legShells,
"upper_leg_right.stl": v.legShells,
"hip_l.stl": v.trim,
// Feet: foot block + ankle bracket, soft sole pad below.
"foot_left.stl": v.feet,
"foot_right.stl": v.feet,
"ankle_left.stl": v.feet,
"ankle_right.stl": v.feet,
"sole_left.stl": v.soles,
"sole_right.stl": v.soles,
// Dark mechanics
"xl330.stl": v.mechDark,
"neck.stl": v.mechDark,
"np_f970.stl": v.mechDark,
"pcb__raspberry_pi_zero_2_w.stl": v.mechDark,
"elec_rpi_robot_hat_pcb.stl": v.mechDark,
"banana_pcb_locker.stl": v.mechDark,
"speaker.stl": v.mechDark,
// Gray mechanics
"upper_leg_rigidity_plate.stl": v.mechGray,
"leg.stl": v.mechGray,
"yaw2roll.stl": v.mechGray,
"yaw_roll_motion.stl": v.mechGray,
"neck_pitch.stl": v.mechGray,
"bearing_roll.stl": v.mechGray,
"motor_support.stl": v.mechGray,
"power_support.stl": v.mechGray,
"seeed_bearing__configuration__22x16x4.stl": v.mechGray,
"seeed_bearing__configuration_default.stl": v.mechGray,
};
}
// buildRig materialForMesh hook for one variant.
export const materialHookFor = (v) => {
const map = meshMaterialsFor(v);
return (mesh) => map[mesh] ?? v.mechGray;
};
// ── Live re-skin ────────────────────────────────────────────────────────
// Swap materials on an already-built rig without reloading any STL.
// Meshes are identified via userData.meshName (set by duck.js buildRig and
// preserved by cloneRig). Materials are cached per resolved spec so clones
// and repeated switches share GPU material instances.
const matCache = new Map();
function matFor(spec) {
const key = `${spec.color.join(",")}|${spec.roughness ?? 0.5}|${spec.metalness ?? 0}`;
let m = matCache.get(key);
if (!m) {
m = new THREE.MeshStandardMaterial({
color: new THREE.Color(...spec.color),
roughness: spec.roughness ?? 0.5,
metalness: spec.metalness ?? 0.0,
});
matCache.set(key, m);
}
return m;
}
export function applyVariant(rig, variant) {
const v = typeof variant === "string" ? VARIANTS[variant] : variant;
const map = meshMaterialsFor(v);
rig.root.traverse((o) => {
if (!o.isMesh || !o.userData.meshName) return;
o.material = matFor(map[o.userData.meshName] ?? v.mechGray);
});
}
// Linear-space spec colour -> sRGB CSS hex, for swatch UI elements.
export function specToHex(spec) {
return `#${new THREE.Color(...spec.color).getHexString()}`;
}