feat: add factory sample machines and robot pricing conversations
This commit is contained in:
+538
-390
@@ -2,310 +2,410 @@ import * as THREE from 'three';
|
||||
|
||||
// Lightweight articulated characters; all coordinates below are in the group.
|
||||
// createFamily() builds the father and child; createFamily({robot: true}) builds the Robot Market guide alone.
|
||||
/**
|
||||
* Creates family resources for the game.
|
||||
* @param {*} options - Operation options.
|
||||
* @returns {*} Operation result, when applicable.
|
||||
*/
|
||||
export function createFamily({ robot: robotOnly = false } = {}) {
|
||||
const group = new THREE.Group();
|
||||
const mat = (color) => new THREE.MeshStandardMaterial({ color, roughness: 0.82 });
|
||||
const skin = mat('#edbd98'),
|
||||
hair = mat('#49382d'),
|
||||
shoes = mat('#273c36');
|
||||
const build = {
|
||||
robotOnly,
|
||||
};
|
||||
for (const [name, method] of Object.entries(FAMILY_METHODS))
|
||||
build[name] = (...args) => method(build, ...args);
|
||||
for (const initialize of FAMILY_STEPS) initialize(build);
|
||||
return build.result;
|
||||
}
|
||||
/**
|
||||
* Runs the family shape operation for this subsystem.
|
||||
* @param {*} build - build.
|
||||
* @param {*} parent - parent.
|
||||
* @param {*} geometry - geometry.
|
||||
* @param {*} material - material.
|
||||
* @param {*} position - position.
|
||||
* @returns {*} Operation result, when applicable.
|
||||
*/
|
||||
function familyShape(build, parent, geometry, material, position) {
|
||||
const m = new THREE.Mesh(geometry, material);
|
||||
m.position.set(...position);
|
||||
m.castShadow = true;
|
||||
m.receiveShadow = true;
|
||||
parent.add(m);
|
||||
return m;
|
||||
}
|
||||
/**
|
||||
* Runs the family sphere operation for this subsystem.
|
||||
* @param {*} build - build.
|
||||
* @param {*} parent - parent.
|
||||
* @param {*} r - r.
|
||||
* @param {*} material - material.
|
||||
* @param {*} pos - pos.
|
||||
* @param {*} scale - scale.
|
||||
* @returns {*} Operation result, when applicable.
|
||||
*/
|
||||
function familySphere(build, parent, r, material, pos, scale = [1, 1, 1]) {
|
||||
const m = build.shape(parent, new THREE.SphereGeometry(r, 16, 12), material, pos);
|
||||
m.scale.set(...scale);
|
||||
return m;
|
||||
}
|
||||
/**
|
||||
* Runs the family segment operation for this subsystem.
|
||||
* @param {*} build - build.
|
||||
* @param {*} parent - parent.
|
||||
* @param {*} a - a.
|
||||
* @param {*} b - b.
|
||||
* @param {*} r - r.
|
||||
* @param {*} material - material.
|
||||
* @returns {*} Operation result, when applicable.
|
||||
*/
|
||||
function familySegment(build, parent, a, b, r, material) {
|
||||
const mesh = build.shape(parent, new THREE.CylinderGeometry(r, r, 1, 10), material, [0, 0, 0]);
|
||||
return {
|
||||
mesh,
|
||||
a,
|
||||
b,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Runs the family update segment operation for this subsystem.
|
||||
* @param {*} build - build.
|
||||
* @param {*} bone - bone.
|
||||
* @param {*} a - a.
|
||||
* @param {*} b - b.
|
||||
* @returns {*} Operation result, when applicable.
|
||||
*/
|
||||
function familyUpdateSegment(build, bone, a, b) {
|
||||
const delta = b.clone().sub(a);
|
||||
bone.mesh.position.copy(a).add(b).multiplyScalar(0.5);
|
||||
bone.mesh.scale.y = delta.length();
|
||||
bone.mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), delta.normalize());
|
||||
}
|
||||
|
||||
function shape(parent, geometry, material, position) {
|
||||
const m = new THREE.Mesh(geometry, material);
|
||||
m.position.set(...position);
|
||||
m.castShadow = true;
|
||||
m.receiveShadow = true;
|
||||
parent.add(m);
|
||||
return m;
|
||||
}
|
||||
|
||||
function sphere(parent, r, material, pos, scale = [1, 1, 1]) {
|
||||
const m = shape(parent, new THREE.SphereGeometry(r, 16, 12), material, pos);
|
||||
m.scale.set(...scale);
|
||||
return m;
|
||||
}
|
||||
|
||||
function segment(parent, a, b, r, material) {
|
||||
const mesh = shape(parent, new THREE.CylinderGeometry(r, r, 1, 10), material, [0, 0, 0]);
|
||||
return { mesh, a, b };
|
||||
}
|
||||
|
||||
function updateSegment(bone, a, b) {
|
||||
const delta = b.clone().sub(a);
|
||||
bone.mesh.position.copy(a).add(b).multiplyScalar(0.5);
|
||||
bone.mesh.scale.y = delta.length();
|
||||
bone.mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), delta.normalize());
|
||||
}
|
||||
|
||||
// Procedural Robot Market mascot: a hovering guide that follows the father and child.
|
||||
function makeRobot(z) {
|
||||
const scale = 0.69,
|
||||
root = new THREE.Group(),
|
||||
torso = new THREE.Group();
|
||||
root.name = 'robot-market-mascot';
|
||||
root.position.z = z;
|
||||
group.add(root);
|
||||
root.add(torso);
|
||||
const teal = new THREE.MeshStandardMaterial({
|
||||
color: '#079f98',
|
||||
metalness: 0.4,
|
||||
roughness: 0.28,
|
||||
});
|
||||
const white = new THREE.MeshStandardMaterial({
|
||||
color: '#edf6f4',
|
||||
metalness: 0.22,
|
||||
roughness: 0.32,
|
||||
});
|
||||
const joint = new THREE.MeshStandardMaterial({
|
||||
color: '#102b30',
|
||||
metalness: 0.5,
|
||||
roughness: 0.35,
|
||||
});
|
||||
const glow = new THREE.MeshStandardMaterial({
|
||||
color: '#c6f7ff',
|
||||
emissive: '#23bcff',
|
||||
emissiveIntensity: 2.5,
|
||||
roughness: 0.2,
|
||||
});
|
||||
const visor = new THREE.MeshStandardMaterial({
|
||||
color: '#020d16',
|
||||
metalness: 0.5,
|
||||
roughness: 0.16,
|
||||
});
|
||||
sphere(torso, 0.25, teal, [0, 0.76, 0], [1, 0.65, 0.75]);
|
||||
sphere(torso, 0.31, white, [0, 0.97, 0], [1, 1.05, 0.72]);
|
||||
for (const side of [-1, 1]) sphere(torso, 0.17, teal, [side * 0.24, 1.03, -0.04], [0.55, 1, 1]);
|
||||
shape(torso, new THREE.CylinderGeometry(0.09, 0.09, 0.12, 12), joint, [0, 1.27, 0]);
|
||||
const head = new THREE.Group();
|
||||
head.position.y = 1.61;
|
||||
torso.add(head);
|
||||
sphere(head, 0.43, teal, [0, 0, 0], [1.12, 0.91, 0.88]);
|
||||
sphere(head, 0.4, white, [0, -0.015, 0.12], [1.12, 0.86, 0.77]);
|
||||
sphere(head, 0.35, visor, [0, -0.015, 0.255], [1.13, 0.83, 0.48]);
|
||||
const eyes = [];
|
||||
function arc(parent, radius, thickness, pos, rotation = 0, material = glow) {
|
||||
const line = shape(
|
||||
parent,
|
||||
new THREE.TorusGeometry(radius, thickness, 8, 24, Math.PI),
|
||||
material,
|
||||
pos,
|
||||
);
|
||||
line.rotation.z = rotation;
|
||||
return line;
|
||||
}
|
||||
for (const side of [-1, 1]) {
|
||||
eyes.push(arc(head, 0.065, 0.016, [side * 0.16, 0.025, 0.423]));
|
||||
const ear = shape(head, new THREE.CylinderGeometry(0.14, 0.14, 0.085, 24), joint, [
|
||||
side * 0.47,
|
||||
0,
|
||||
0,
|
||||
]);
|
||||
ear.rotation.z = Math.PI / 2;
|
||||
const ring = shape(head, new THREE.TorusGeometry(0.105, 0.012, 8, 24), glow, [
|
||||
side * 0.518,
|
||||
0,
|
||||
0,
|
||||
]);
|
||||
ring.rotation.y = Math.PI / 2;
|
||||
sphere(head, 0.12, teal, [side * 0.49, 0, 0], [0.4, 1, 1]);
|
||||
}
|
||||
arc(head, 0.075, 0.016, [0, -0.085, 0.425], Math.PI);
|
||||
sphere(head, 0.1, teal, [0, 0.38, 0], [1, 0.55, 1]);
|
||||
shape(head, new THREE.CylinderGeometry(0.017, 0.022, 0.18, 10), teal, [0, 0.49, 0]);
|
||||
sphere(head, 0.055, teal, [0, 0.6, 0]);
|
||||
// Draw the chest mark as a texture so it remains attached to the 3D body.
|
||||
if (typeof document !== 'undefined') {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 256;
|
||||
canvas.height = 256;
|
||||
const c = canvas.getContext('2d');
|
||||
if (c) {
|
||||
c.strokeStyle = '#102b30';
|
||||
c.fillStyle = '#102b30';
|
||||
c.lineWidth = 12;
|
||||
c.lineJoin = 'round';
|
||||
c.beginPath();
|
||||
c.moveTo(65, 40);
|
||||
c.lineTo(82, 40);
|
||||
c.lineTo(99, 110);
|
||||
c.lineTo(170, 110);
|
||||
c.lineTo(186, 58);
|
||||
c.lineTo(89, 58);
|
||||
c.stroke();
|
||||
for (const x of [111, 162]) {
|
||||
c.beginPath();
|
||||
c.arc(x, 130, 9, 0, Math.PI * 2);
|
||||
c.fill();
|
||||
}
|
||||
c.textAlign = 'center';
|
||||
c.font = 'bold 34px sans-serif';
|
||||
c.fillText('ROBOT', 128, 181);
|
||||
c.fillText('MARKET', 128, 218);
|
||||
shape(
|
||||
torso,
|
||||
new THREE.PlaneGeometry(0.31, 0.31),
|
||||
new THREE.MeshBasicMaterial({
|
||||
map: new THREE.CanvasTexture(canvas),
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
}),
|
||||
[0, 0.99, 0.224],
|
||||
);
|
||||
}
|
||||
}
|
||||
const legs = [-1, 1].map((side) => {
|
||||
const hip = new THREE.Group();
|
||||
hip.position.set(side * 0.15, 0.65, 0);
|
||||
root.add(hip);
|
||||
sphere(hip, 0.09, joint, [0, 0, 0]);
|
||||
shape(hip, new THREE.CapsuleGeometry(0.095, 0.12, 4, 12), white, [0, -0.12, 0]);
|
||||
const knee = new THREE.Group();
|
||||
knee.position.y = -0.25;
|
||||
hip.add(knee);
|
||||
sphere(knee, 0.105, teal, [0, 0, 0.035]);
|
||||
shape(knee, new THREE.CapsuleGeometry(0.1, 0.12, 4, 12), white, [0, -0.13, 0]);
|
||||
const foot = sphere(knee, 0.13, teal, [0, -0.29, 0.07], [1, 0.65, 1.6]);
|
||||
sphere(knee, 0.13, joint, [0, -0.34, 0.07], [1.06, 0.22, 1.65]);
|
||||
return { hip, knee, foot };
|
||||
});
|
||||
const arms = [-1, 1].map(() => ({
|
||||
upper: segment(group, null, null, 0.09, teal),
|
||||
lower: segment(group, null, null, 0.085, white),
|
||||
hand: sphere(group, 0.09, joint, [0, 0, 0], [1, 1.1, 1]),
|
||||
}));
|
||||
return { root, torso, head, legs, arms, scale, z, eyes };
|
||||
}
|
||||
|
||||
function makePerson(isChild, z) {
|
||||
const scale = isChild ? 0.69 : 1.05,
|
||||
root = new THREE.Group();
|
||||
root.position.z = z;
|
||||
group.add(root);
|
||||
const torso = new THREE.Group();
|
||||
root.add(torso);
|
||||
const cloth = mat(isChild ? '#d98150' : '#e4c695'),
|
||||
pants = mat(isChild ? '#527463' : '#496474');
|
||||
// Tapered jacket, hips and neck replace the single rounded toy-like body.
|
||||
shape(torso, new THREE.CylinderGeometry(0.255 * scale, 0.22 * scale, 0.62 * scale, 16), cloth, [
|
||||
0,
|
||||
1.2 * scale,
|
||||
0,
|
||||
]).scale.z = 0.72;
|
||||
sphere(torso, 0.23, pants, [0, 0.88 * scale, 0], [scale, 0.55 * scale, 0.72 * scale]);
|
||||
shape(torso, new THREE.CylinderGeometry(0.075 * scale, 0.09 * scale, 0.18 * scale, 12), skin, [
|
||||
0,
|
||||
1.59 * scale,
|
||||
0,
|
||||
]);
|
||||
shape(torso, new THREE.BoxGeometry(0.014 * scale, 0.5 * scale, 0.014 * scale), mat('#bda77f'), [
|
||||
0,
|
||||
1.2 * scale,
|
||||
0.19 * scale,
|
||||
]);
|
||||
for (const side of [-1, 1]) {
|
||||
const collar = shape(
|
||||
torso,
|
||||
new THREE.BoxGeometry(0.12 * scale, 0.09 * scale, 0.025 * scale),
|
||||
cloth,
|
||||
[side * 0.07 * scale, 1.49 * scale, 0.15 * scale],
|
||||
);
|
||||
collar.rotation.z = side * 0.4;
|
||||
}
|
||||
const head = new THREE.Group();
|
||||
head.position.y = 1.77 * scale;
|
||||
torso.add(head);
|
||||
head.scale.setScalar(isChild ? 0.9 : 0.77);
|
||||
const eyes = [];
|
||||
sphere(head, 0.27 * scale, skin, [0, 0, 0]);
|
||||
sphere(head, 0.275 * scale, hair, [0, 0.11 * scale, -0.035 * scale], [1, 0.65, 1]);
|
||||
sphere(head, 0.058 * scale, skin, [0, -0.02 * scale, 0.25 * scale]);
|
||||
for (const side of [-1, 1]) {
|
||||
sphere(head, 0.06 * scale, skin, [side * 0.26 * scale, 0, 0]);
|
||||
sphere(
|
||||
head,
|
||||
0.037 * scale,
|
||||
mat('#f6f1e7'),
|
||||
[side * 0.09 * scale, 0.03 * scale, 0.241 * scale],
|
||||
[1, 0.65, 0.5],
|
||||
);
|
||||
const eye = sphere(
|
||||
head,
|
||||
0.022 * scale,
|
||||
shoes,
|
||||
[side * 0.09 * scale, 0.03 * scale, 0.258 * scale],
|
||||
[0.8, 1, 0.45],
|
||||
);
|
||||
eyes.push(eye);
|
||||
const brow = shape(
|
||||
head,
|
||||
new THREE.BoxGeometry(0.075 * scale, 0.015 * scale, 0.015 * scale),
|
||||
hair,
|
||||
[side * 0.09 * scale, 0.087 * scale, 0.237 * scale],
|
||||
);
|
||||
brow.rotation.z = side * -0.08;
|
||||
sphere(head, 0.04 * scale, mat('#d79379'), [
|
||||
side * 0.15 * scale,
|
||||
-0.055 * scale,
|
||||
0.215 * scale,
|
||||
]);
|
||||
}
|
||||
const smile = shape(
|
||||
head,
|
||||
new THREE.TorusGeometry(0.068 * scale, 0.012 * scale, 6, 14, Math.PI),
|
||||
shoes,
|
||||
[0, -0.075 * scale, 0.252 * scale],
|
||||
// Procedural Robot Market mascot: a hovering guide that follows the father and child.
|
||||
/**
|
||||
* Runs the family make robot operation for this subsystem.
|
||||
* @param {*} build - build.
|
||||
* @param {*} z - z.
|
||||
* @returns {*} Operation result, when applicable.
|
||||
*/
|
||||
function familyMakeRobot(build, z) {
|
||||
const scale = 0.69,
|
||||
root = new THREE.Group(),
|
||||
torso = new THREE.Group();
|
||||
root.name = 'robot-market-mascot';
|
||||
root.position.z = z;
|
||||
build.group.add(root);
|
||||
root.add(torso);
|
||||
const teal = new THREE.MeshStandardMaterial({
|
||||
color: '#079f98',
|
||||
metalness: 0.4,
|
||||
roughness: 0.28,
|
||||
});
|
||||
const white = new THREE.MeshStandardMaterial({
|
||||
color: '#edf6f4',
|
||||
metalness: 0.22,
|
||||
roughness: 0.32,
|
||||
});
|
||||
const joint = new THREE.MeshStandardMaterial({
|
||||
color: '#102b30',
|
||||
metalness: 0.5,
|
||||
roughness: 0.35,
|
||||
});
|
||||
const glow = new THREE.MeshStandardMaterial({
|
||||
color: '#c6f7ff',
|
||||
emissive: '#23bcff',
|
||||
emissiveIntensity: 2.5,
|
||||
roughness: 0.2,
|
||||
});
|
||||
const visor = new THREE.MeshStandardMaterial({
|
||||
color: '#020d16',
|
||||
metalness: 0.5,
|
||||
roughness: 0.16,
|
||||
});
|
||||
build.sphere(torso, 0.25, teal, [0, 0.76, 0], [1, 0.65, 0.75]);
|
||||
build.sphere(torso, 0.31, white, [0, 0.97, 0], [1, 1.05, 0.72]);
|
||||
for (const side of [-1, 1])
|
||||
build.sphere(torso, 0.17, teal, [side * 0.24, 1.03, -0.04], [0.55, 1, 1]);
|
||||
build.shape(torso, new THREE.CylinderGeometry(0.09, 0.09, 0.12, 12), joint, [0, 1.27, 0]);
|
||||
const head = new THREE.Group();
|
||||
head.position.y = 1.61;
|
||||
torso.add(head);
|
||||
build.sphere(head, 0.43, teal, [0, 0, 0], [1.12, 0.91, 0.88]);
|
||||
build.sphere(head, 0.4, white, [0, -0.015, 0.12], [1.12, 0.86, 0.77]);
|
||||
build.sphere(head, 0.35, visor, [0, -0.015, 0.255], [1.13, 0.83, 0.48]);
|
||||
const eyes = [];
|
||||
/**
|
||||
* Runs the arc operation for this subsystem.
|
||||
* @param {*} parent - parent.
|
||||
* @param {*} radius - radius.
|
||||
* @param {*} thickness - thickness.
|
||||
* @param {*} pos - pos.
|
||||
* @param {*} rotation - rotation.
|
||||
* @param {*} material - material.
|
||||
* @returns {*} Operation result, when applicable.
|
||||
*/
|
||||
function arc(parent, radius, thickness, pos, rotation = 0, material = glow) {
|
||||
const line = build.shape(
|
||||
parent,
|
||||
new THREE.TorusGeometry(radius, thickness, 8, 24, Math.PI),
|
||||
material,
|
||||
pos,
|
||||
);
|
||||
smile.rotation.z = Math.PI;
|
||||
if (isChild) {
|
||||
sphere(head, 0.29 * scale, mat('#e8b857'), [0, 0.18 * scale, 0], [1, 0.5, 1]);
|
||||
shape(head, new THREE.BoxGeometry(0.55 * scale, 0.04 * scale, 0.34 * scale), mat('#e8b857'), [
|
||||
0,
|
||||
0.13 * scale,
|
||||
0.12 * scale,
|
||||
]);
|
||||
} else {
|
||||
sphere(torso, 0.22, mat('#a77950'), [0, 1.2 * scale, -0.24 * scale], [1, 1.2, 0.5]);
|
||||
}
|
||||
const legs = [-1, 1].map((side) => {
|
||||
const hip = new THREE.Group();
|
||||
hip.position.set(side * 0.15 * scale, 0.83 * scale, 0);
|
||||
root.add(hip);
|
||||
shape(hip, new THREE.CapsuleGeometry(0.09 * scale, 0.23 * scale, 4, 10), pants, [
|
||||
0,
|
||||
-0.16 * scale,
|
||||
0,
|
||||
]);
|
||||
const knee = new THREE.Group();
|
||||
knee.position.y = -0.34 * scale;
|
||||
hip.add(knee);
|
||||
shape(knee, new THREE.CapsuleGeometry(0.08 * scale, 0.25 * scale, 4, 10), pants, [
|
||||
0,
|
||||
-0.16 * scale,
|
||||
0,
|
||||
]);
|
||||
const foot = shape(knee, new THREE.SphereGeometry(0.12 * scale, 12, 8), shoes, [
|
||||
0,
|
||||
-0.32 * scale,
|
||||
0.065 * scale,
|
||||
]);
|
||||
foot.scale.set(0.85, 0.65, 1.5);
|
||||
return { hip, knee, foot };
|
||||
});
|
||||
const arms = [-1, 1].map(() => ({
|
||||
upper: segment(group, null, null, 0.072 * scale, cloth),
|
||||
lower: segment(group, null, null, 0.061 * scale, skin),
|
||||
hand: sphere(group, 0.075 * scale, skin, [0, 0, 0]),
|
||||
}));
|
||||
return { root, torso, head, legs, arms, scale, z, eyes };
|
||||
line.rotation.z = rotation;
|
||||
return line;
|
||||
}
|
||||
|
||||
const father = robotOnly ? null : makePerson(false, -0.45),
|
||||
child = robotOnly ? null : makePerson(true, 0.45);
|
||||
const robot = robotOnly ? makeRobot(0) : null;
|
||||
const actors = robotOnly ? [robot] : [father, child];
|
||||
|
||||
// The group faces +X; `facing` is the actors' own yaw inside it (π/2 = along +X).
|
||||
// `wave` raises the robot's right hand, `point` stretches it forward.
|
||||
function update({
|
||||
for (const side of [-1, 1]) {
|
||||
eyes.push(arc(head, 0.065, 0.016, [side * 0.16, 0.025, 0.423]));
|
||||
const ear = build.shape(head, new THREE.CylinderGeometry(0.14, 0.14, 0.085, 24), joint, [
|
||||
side * 0.47,
|
||||
0,
|
||||
0,
|
||||
]);
|
||||
ear.rotation.z = Math.PI / 2;
|
||||
const ring = build.shape(head, new THREE.TorusGeometry(0.105, 0.012, 8, 24), glow, [
|
||||
side * 0.518,
|
||||
0,
|
||||
0,
|
||||
]);
|
||||
ring.rotation.y = Math.PI / 2;
|
||||
build.sphere(head, 0.12, teal, [side * 0.49, 0, 0], [0.4, 1, 1]);
|
||||
}
|
||||
arc(head, 0.075, 0.016, [0, -0.085, 0.425], Math.PI);
|
||||
build.sphere(head, 0.1, teal, [0, 0.38, 0], [1, 0.55, 1]);
|
||||
build.shape(head, new THREE.CylinderGeometry(0.017, 0.022, 0.18, 10), teal, [0, 0.49, 0]);
|
||||
build.sphere(head, 0.055, teal, [0, 0.6, 0]);
|
||||
// Draw the chest mark as a texture so it remains attached to the 3D body.
|
||||
if (typeof document !== 'undefined') {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 256;
|
||||
canvas.height = 256;
|
||||
const c = canvas.getContext('2d');
|
||||
if (c) {
|
||||
c.strokeStyle = '#102b30';
|
||||
c.fillStyle = '#102b30';
|
||||
c.lineWidth = 12;
|
||||
c.lineJoin = 'round';
|
||||
c.beginPath();
|
||||
c.moveTo(65, 40);
|
||||
c.lineTo(82, 40);
|
||||
c.lineTo(99, 110);
|
||||
c.lineTo(170, 110);
|
||||
c.lineTo(186, 58);
|
||||
c.lineTo(89, 58);
|
||||
c.stroke();
|
||||
for (const x of [111, 162]) {
|
||||
c.beginPath();
|
||||
c.arc(x, 130, 9, 0, Math.PI * 2);
|
||||
c.fill();
|
||||
}
|
||||
c.textAlign = 'center';
|
||||
c.font = 'bold 34px sans-serif';
|
||||
c.fillText('ROBOT', 128, 181);
|
||||
c.fillText('MARKET', 128, 218);
|
||||
build.shape(
|
||||
torso,
|
||||
new THREE.PlaneGeometry(0.31, 0.31),
|
||||
new THREE.MeshBasicMaterial({
|
||||
map: new THREE.CanvasTexture(canvas),
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
}),
|
||||
[0, 0.99, 0.224],
|
||||
);
|
||||
}
|
||||
}
|
||||
const legs = [-1, 1].map((side) => {
|
||||
const hip = new THREE.Group();
|
||||
hip.position.set(side * 0.15, 0.65, 0);
|
||||
root.add(hip);
|
||||
build.sphere(hip, 0.09, joint, [0, 0, 0]);
|
||||
build.shape(hip, new THREE.CapsuleGeometry(0.095, 0.12, 4, 12), white, [0, -0.12, 0]);
|
||||
const knee = new THREE.Group();
|
||||
knee.position.y = -0.25;
|
||||
hip.add(knee);
|
||||
build.sphere(knee, 0.105, teal, [0, 0, 0.035]);
|
||||
build.shape(knee, new THREE.CapsuleGeometry(0.1, 0.12, 4, 12), white, [0, -0.13, 0]);
|
||||
const foot = build.sphere(knee, 0.13, teal, [0, -0.29, 0.07], [1, 0.65, 1.6]);
|
||||
build.sphere(knee, 0.13, joint, [0, -0.34, 0.07], [1.06, 0.22, 1.65]);
|
||||
return {
|
||||
hip,
|
||||
knee,
|
||||
foot,
|
||||
};
|
||||
});
|
||||
const arms = [-1, 1].map(() => ({
|
||||
upper: build.segment(build.group, null, null, 0.09, teal),
|
||||
lower: build.segment(build.group, null, null, 0.085, white),
|
||||
hand: build.sphere(build.group, 0.09, joint, [0, 0, 0], [1, 1.1, 1]),
|
||||
}));
|
||||
return {
|
||||
root,
|
||||
torso,
|
||||
head,
|
||||
legs,
|
||||
arms,
|
||||
scale,
|
||||
z,
|
||||
eyes,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Runs the family make person operation for this subsystem.
|
||||
* @param {*} build - build.
|
||||
* @param {*} isChild - is Child.
|
||||
* @param {*} z - z.
|
||||
* @returns {*} Operation result, when applicable.
|
||||
*/
|
||||
function familyMakePerson(build, isChild, z) {
|
||||
const scale = isChild ? 0.69 : 1.05,
|
||||
root = new THREE.Group();
|
||||
root.position.z = z;
|
||||
build.group.add(root);
|
||||
const torso = new THREE.Group();
|
||||
root.add(torso);
|
||||
const cloth = build.mat(isChild ? '#d98150' : '#e4c695'),
|
||||
pants = build.mat(isChild ? '#527463' : '#496474');
|
||||
// Tapered jacket, hips and neck replace the single rounded toy-like body.
|
||||
build.shape(
|
||||
torso,
|
||||
new THREE.CylinderGeometry(0.255 * scale, 0.22 * scale, 0.62 * scale, 16),
|
||||
cloth,
|
||||
[0, 1.2 * scale, 0],
|
||||
).scale.z = 0.72;
|
||||
build.sphere(torso, 0.23, pants, [0, 0.88 * scale, 0], [scale, 0.55 * scale, 0.72 * scale]);
|
||||
build.shape(
|
||||
torso,
|
||||
new THREE.CylinderGeometry(0.075 * scale, 0.09 * scale, 0.18 * scale, 12),
|
||||
build.skin,
|
||||
[0, 1.59 * scale, 0],
|
||||
);
|
||||
build.shape(
|
||||
torso,
|
||||
new THREE.BoxGeometry(0.014 * scale, 0.5 * scale, 0.014 * scale),
|
||||
build.mat('#bda77f'),
|
||||
[0, 1.2 * scale, 0.19 * scale],
|
||||
);
|
||||
for (const side of [-1, 1]) {
|
||||
const collar = build.shape(
|
||||
torso,
|
||||
new THREE.BoxGeometry(0.12 * scale, 0.09 * scale, 0.025 * scale),
|
||||
cloth,
|
||||
[side * 0.07 * scale, 1.49 * scale, 0.15 * scale],
|
||||
);
|
||||
collar.rotation.z = side * 0.4;
|
||||
}
|
||||
const head = new THREE.Group();
|
||||
head.position.y = 1.77 * scale;
|
||||
torso.add(head);
|
||||
head.scale.setScalar(isChild ? 0.9 : 0.77);
|
||||
const eyes = [];
|
||||
build.sphere(head, 0.27 * scale, build.skin, [0, 0, 0]);
|
||||
build.sphere(head, 0.275 * scale, build.hair, [0, 0.11 * scale, -0.035 * scale], [1, 0.65, 1]);
|
||||
build.sphere(head, 0.058 * scale, build.skin, [0, -0.02 * scale, 0.25 * scale]);
|
||||
for (const side of [-1, 1]) {
|
||||
build.sphere(head, 0.06 * scale, build.skin, [side * 0.26 * scale, 0, 0]);
|
||||
build.sphere(
|
||||
head,
|
||||
0.037 * scale,
|
||||
build.mat('#f6f1e7'),
|
||||
[side * 0.09 * scale, 0.03 * scale, 0.241 * scale],
|
||||
[1, 0.65, 0.5],
|
||||
);
|
||||
const eye = build.sphere(
|
||||
head,
|
||||
0.022 * scale,
|
||||
build.shoes,
|
||||
[side * 0.09 * scale, 0.03 * scale, 0.258 * scale],
|
||||
[0.8, 1, 0.45],
|
||||
);
|
||||
eyes.push(eye);
|
||||
const brow = build.shape(
|
||||
head,
|
||||
new THREE.BoxGeometry(0.075 * scale, 0.015 * scale, 0.015 * scale),
|
||||
build.hair,
|
||||
[side * 0.09 * scale, 0.087 * scale, 0.237 * scale],
|
||||
);
|
||||
brow.rotation.z = side * -0.08;
|
||||
build.sphere(head, 0.04 * scale, build.mat('#d79379'), [
|
||||
side * 0.15 * scale,
|
||||
-0.055 * scale,
|
||||
0.215 * scale,
|
||||
]);
|
||||
}
|
||||
const smile = build.shape(
|
||||
head,
|
||||
new THREE.TorusGeometry(0.068 * scale, 0.012 * scale, 6, 14, Math.PI),
|
||||
build.shoes,
|
||||
[0, -0.075 * scale, 0.252 * scale],
|
||||
);
|
||||
smile.rotation.z = Math.PI;
|
||||
if (isChild) {
|
||||
build.sphere(head, 0.29 * scale, build.mat('#e8b857'), [0, 0.18 * scale, 0], [1, 0.5, 1]);
|
||||
build.shape(
|
||||
head,
|
||||
new THREE.BoxGeometry(0.55 * scale, 0.04 * scale, 0.34 * scale),
|
||||
build.mat('#e8b857'),
|
||||
[0, 0.13 * scale, 0.12 * scale],
|
||||
);
|
||||
} else {
|
||||
build.sphere(torso, 0.22, build.mat('#a77950'), [0, 1.2 * scale, -0.24 * scale], [1, 1.2, 0.5]);
|
||||
}
|
||||
const legs = [-1, 1].map((side) => {
|
||||
const hip = new THREE.Group();
|
||||
hip.position.set(side * 0.15 * scale, 0.83 * scale, 0);
|
||||
root.add(hip);
|
||||
build.shape(hip, new THREE.CapsuleGeometry(0.09 * scale, 0.23 * scale, 4, 10), pants, [
|
||||
0,
|
||||
-0.16 * scale,
|
||||
0,
|
||||
]);
|
||||
const knee = new THREE.Group();
|
||||
knee.position.y = -0.34 * scale;
|
||||
hip.add(knee);
|
||||
build.shape(knee, new THREE.CapsuleGeometry(0.08 * scale, 0.25 * scale, 4, 10), pants, [
|
||||
0,
|
||||
-0.16 * scale,
|
||||
0,
|
||||
]);
|
||||
const foot = build.shape(knee, new THREE.SphereGeometry(0.12 * scale, 12, 8), build.shoes, [
|
||||
0,
|
||||
-0.32 * scale,
|
||||
0.065 * scale,
|
||||
]);
|
||||
foot.scale.set(0.85, 0.65, 1.5);
|
||||
return {
|
||||
hip,
|
||||
knee,
|
||||
foot,
|
||||
};
|
||||
});
|
||||
const arms = [-1, 1].map(() => ({
|
||||
upper: build.segment(build.group, null, null, 0.072 * scale, cloth),
|
||||
lower: build.segment(build.group, null, null, 0.061 * scale, build.skin),
|
||||
hand: build.sphere(build.group, 0.075 * scale, build.skin, [0, 0, 0]),
|
||||
}));
|
||||
return {
|
||||
root,
|
||||
torso,
|
||||
head,
|
||||
legs,
|
||||
arms,
|
||||
scale,
|
||||
z,
|
||||
eyes,
|
||||
};
|
||||
}
|
||||
// The group faces +X; `facing` is the actors' own yaw inside it (π/2 = along +X).
|
||||
// `wave` raises the robot's right hand, `point` stretches it forward.
|
||||
/**
|
||||
* Runs the family update operation for this subsystem.
|
||||
* @param {*} build - build.
|
||||
* @param {*} options1 - Operation options.
|
||||
* @returns {*} Operation result, when applicable.
|
||||
*/
|
||||
function familyUpdate(
|
||||
build,
|
||||
{
|
||||
time,
|
||||
gait,
|
||||
moving,
|
||||
@@ -317,95 +417,143 @@ export function createFamily({ robot: robotOnly = false } = {}) {
|
||||
wave = false,
|
||||
point = false,
|
||||
dt = 1 / 60,
|
||||
}) {
|
||||
actors.forEach((actor, index) => {
|
||||
const blend = 1 - Math.exp(-dt * 7),
|
||||
isRobot = actor === robot;
|
||||
actor.root.position.x = THREE.MathUtils.lerp(
|
||||
actor.root.position.x,
|
||||
finished ? (index === 0 ? -0.55 : 0.55) : shopping && !isRobot ? [-0.55, 0.25][index] : 0,
|
||||
blend,
|
||||
);
|
||||
// The robot hovers; it bobs faster while it is speaking.
|
||||
const hover = isRobot ? 0.35 + Math.sin(time * (talking ? 6 : 2.2)) * 0.07 : 0;
|
||||
actor.root.position.y = THREE.MathUtils.lerp(
|
||||
actor.root.position.y,
|
||||
finished ? -0.6 * actor.scale : hover,
|
||||
isRobot ? 1 : blend,
|
||||
);
|
||||
actor.root.position.z = finished ? 0 : actor.z;
|
||||
actor.root.rotation.y = facing;
|
||||
const bob = moving ? Math.abs(Math.sin(gait)) * 0.022 : Math.sin(time * 2) * 0.006;
|
||||
actor.torso.position.y = bob;
|
||||
actor.torso.rotation.z = moving ? Math.sin(gait) * 0.018 : 0;
|
||||
const blinking = Math.sin(time * 1.3 + index) > 0.993;
|
||||
actor.eyes.forEach((eye) => (eye.scale.y = blinking ? 0.12 : 1));
|
||||
actor.head.rotation.y = shopping
|
||||
? -0.65
|
||||
: isRobot
|
||||
? Math.sin(time * 0.9) * 0.35
|
||||
: Math.sin(time * 0.7) * 0.06;
|
||||
actor.torso.rotation.x = delivery && actor === child ? 0.22 : 0;
|
||||
actor.legs.forEach((leg, i) => {
|
||||
const phase = gait + i * Math.PI;
|
||||
const stride = isRobot ? 0.08 : 0.34;
|
||||
leg.hip.rotation.x = finished ? -1.4 : moving ? Math.sin(phase) * stride : 0;
|
||||
leg.foot.rotation.x = moving ? Math.sin(phase + 0.5) * 0.12 : 0;
|
||||
leg.knee.rotation.x = finished
|
||||
? 2.5
|
||||
: isRobot
|
||||
? 0.25
|
||||
: moving
|
||||
? Math.max(0, -Math.sin(phase)) * 0.5
|
||||
: 0.05;
|
||||
});
|
||||
});
|
||||
group.updateMatrixWorld(true);
|
||||
const toFamily = (actor, point) => point.applyMatrix4(actor.root.matrix).clone();
|
||||
const shoulder = (actor, side) =>
|
||||
toFamily(
|
||||
actor,
|
||||
new THREE.Vector3(
|
||||
side * 0.29 * actor.scale,
|
||||
(actor === robot ? 1.12 : 1.43) * actor.scale,
|
||||
0,
|
||||
),
|
||||
);
|
||||
// With characters facing +X, father's left and child's right hands meet.
|
||||
const contact = new THREE.Vector3(0, 1.06, 0);
|
||||
actors.forEach((actor) =>
|
||||
actor.arms.forEach((arm, i) => {
|
||||
const side = i === 0 ? -1 : 1,
|
||||
inside = actor === father ? side === -1 : actor === child && side === 1;
|
||||
const a = shoulder(actor, side);
|
||||
let target;
|
||||
if (inside && !shopping && !finished) target = contact.clone();
|
||||
else if (actor === child && shopping && side === -1)
|
||||
target = new THREE.Vector3(0.5, delivery ? 0.65 : 1.1, -0.15);
|
||||
else if (actor === robot && side === 1 && wave)
|
||||
target = toFamily(actor, new THREE.Vector3(0.42, 1.62 + Math.sin(time * 9) * 0.07, 0.12));
|
||||
else if (actor === robot && side === 1 && point)
|
||||
target = toFamily(actor, new THREE.Vector3(0.3, 1.25, 0.62));
|
||||
else
|
||||
target = toFamily(
|
||||
actor,
|
||||
new THREE.Vector3(
|
||||
side * 0.34 * actor.scale,
|
||||
0.88 * actor.scale,
|
||||
moving ? Math.sin(gait + (i ? Math.PI : 0)) * 0.2 : 0,
|
||||
),
|
||||
);
|
||||
const elbow = a
|
||||
.clone()
|
||||
.lerp(target, 0.5)
|
||||
.add(new THREE.Vector3(0.07, 0, inside ? 0 : 0.07));
|
||||
updateSegment(arm.upper, a, elbow);
|
||||
updateSegment(arm.lower, elbow, target);
|
||||
arm.hand.position.copy(target);
|
||||
}),
|
||||
},
|
||||
) {
|
||||
build.actors.forEach((actor, index) => {
|
||||
const blend = 1 - Math.exp(-dt * 7),
|
||||
isRobot = actor === build.robot;
|
||||
actor.root.position.x = THREE.MathUtils.lerp(
|
||||
actor.root.position.x,
|
||||
finished ? (index === 0 ? -0.55 : 0.55) : shopping && !isRobot ? [-0.55, 0.25][index] : 0,
|
||||
blend,
|
||||
);
|
||||
}
|
||||
|
||||
update({ time: 0, gait: 0, moving: false });
|
||||
return { group, update };
|
||||
// The robot hovers; it bobs faster while it is speaking.
|
||||
const hover = isRobot ? 0.35 + Math.sin(time * (talking ? 6 : 2.2)) * 0.07 : 0;
|
||||
actor.root.position.y = THREE.MathUtils.lerp(
|
||||
actor.root.position.y,
|
||||
finished ? -0.6 * actor.scale : hover,
|
||||
isRobot ? 1 : blend,
|
||||
);
|
||||
actor.root.position.z = finished ? 0 : actor.z;
|
||||
actor.root.rotation.y = facing;
|
||||
const bob = moving ? Math.abs(Math.sin(gait)) * 0.022 : Math.sin(time * 2) * 0.006;
|
||||
actor.torso.position.y = bob;
|
||||
actor.torso.rotation.z = moving ? Math.sin(gait) * 0.018 : 0;
|
||||
const blinking = Math.sin(time * 1.3 + index) > 0.993;
|
||||
actor.eyes.forEach((eye) => (eye.scale.y = blinking ? 0.12 : 1));
|
||||
actor.head.rotation.y = shopping
|
||||
? -0.65
|
||||
: isRobot
|
||||
? Math.sin(time * 0.9) * 0.35
|
||||
: Math.sin(time * 0.7) * 0.06;
|
||||
actor.torso.rotation.x = delivery && actor === build.child ? 0.22 : 0;
|
||||
actor.legs.forEach((leg, i) => {
|
||||
const phase = gait + i * Math.PI;
|
||||
const stride = isRobot ? 0.08 : 0.34;
|
||||
leg.hip.rotation.x = finished ? -1.4 : moving ? Math.sin(phase) * stride : 0;
|
||||
leg.foot.rotation.x = moving ? Math.sin(phase + 0.5) * 0.12 : 0;
|
||||
leg.knee.rotation.x = finished
|
||||
? 2.5
|
||||
: isRobot
|
||||
? 0.25
|
||||
: moving
|
||||
? Math.max(0, -Math.sin(phase)) * 0.5
|
||||
: 0.05;
|
||||
});
|
||||
});
|
||||
build.group.updateMatrixWorld(true);
|
||||
/**
|
||||
* Runs the to family operation for this subsystem.
|
||||
* @param {*} actor - actor.
|
||||
* @param {*} point - point.
|
||||
* @returns {*} Operation result, when applicable.
|
||||
*/
|
||||
const toFamily = (actor, point) => point.applyMatrix4(actor.root.matrix).clone();
|
||||
/**
|
||||
* Runs the shoulder operation for this subsystem.
|
||||
* @param {*} actor - actor.
|
||||
* @param {*} side - side.
|
||||
* @returns {*} Operation result, when applicable.
|
||||
*/
|
||||
const shoulder = (actor, side) =>
|
||||
toFamily(
|
||||
actor,
|
||||
new THREE.Vector3(
|
||||
side * 0.29 * actor.scale,
|
||||
(actor === build.robot ? 1.12 : 1.43) * actor.scale,
|
||||
0,
|
||||
),
|
||||
);
|
||||
// With characters facing +X, father's left and child's right hands meet.
|
||||
const contact = new THREE.Vector3(0, 1.06, 0);
|
||||
build.actors.forEach((actor) =>
|
||||
actor.arms.forEach((arm, i) => {
|
||||
const side = i === 0 ? -1 : 1,
|
||||
inside = actor === build.father ? side === -1 : actor === build.child && side === 1;
|
||||
const a = shoulder(actor, side);
|
||||
let target;
|
||||
if (inside && !shopping && !finished) target = contact.clone();
|
||||
else if (actor === build.child && shopping && side === -1)
|
||||
target = new THREE.Vector3(0.5, delivery ? 0.65 : 1.1, -0.15);
|
||||
else if (actor === build.robot && side === 1 && wave)
|
||||
target = toFamily(actor, new THREE.Vector3(0.42, 1.62 + Math.sin(time * 9) * 0.07, 0.12));
|
||||
else if (actor === build.robot && side === 1 && point)
|
||||
target = toFamily(actor, new THREE.Vector3(0.3, 1.25, 0.62));
|
||||
else
|
||||
target = toFamily(
|
||||
actor,
|
||||
new THREE.Vector3(
|
||||
side * 0.34 * actor.scale,
|
||||
0.88 * actor.scale,
|
||||
moving ? Math.sin(gait + (i ? Math.PI : 0)) * 0.2 : 0,
|
||||
),
|
||||
);
|
||||
const elbow = a
|
||||
.clone()
|
||||
.lerp(target, 0.5)
|
||||
.add(new THREE.Vector3(0.07, 0, inside ? 0 : 0.07));
|
||||
build.updateSegment(arm.upper, a, elbow);
|
||||
build.updateSegment(arm.lower, elbow, target);
|
||||
arm.hand.position.copy(target);
|
||||
}),
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Runs the family build group operation for this subsystem.
|
||||
* @param {*} build - build.
|
||||
* @returns {*} Operation result, when applicable.
|
||||
*/
|
||||
function familyBuildGroup(build) {
|
||||
build.group = new THREE.Group();
|
||||
build.mat = (color) =>
|
||||
new THREE.MeshStandardMaterial({
|
||||
color,
|
||||
roughness: 0.82,
|
||||
});
|
||||
build.skin = build.mat('#edbd98');
|
||||
build.hair = build.mat('#49382d');
|
||||
build.shoes = build.mat('#273c36');
|
||||
build.father = build.robotOnly ? null : build.makePerson(false, -0.45);
|
||||
build.child = build.robotOnly ? null : build.makePerson(true, 0.45);
|
||||
build.robot = build.robotOnly ? build.makeRobot(0) : null;
|
||||
build.actors = build.robotOnly ? [build.robot] : [build.father, build.child];
|
||||
build.update({
|
||||
time: 0,
|
||||
gait: 0,
|
||||
moving: false,
|
||||
});
|
||||
build.result = {
|
||||
group: build.group,
|
||||
update: build.update,
|
||||
};
|
||||
}
|
||||
const FAMILY_METHODS = {
|
||||
shape: familyShape,
|
||||
sphere: familySphere,
|
||||
segment: familySegment,
|
||||
updateSegment: familyUpdateSegment,
|
||||
makeRobot: familyMakeRobot,
|
||||
makePerson: familyMakePerson,
|
||||
update: familyUpdate,
|
||||
};
|
||||
const FAMILY_STEPS = [familyBuildGroup];
|
||||
|
||||
Reference in New Issue
Block a user