145 lines
5.0 KiB
JavaScript
145 lines
5.0 KiB
JavaScript
import * as THREE from 'three';
|
|
import { PART } from '../character/body.js';
|
|
|
|
/**
|
|
* Materials for one skater.
|
|
*
|
|
* Placeholder by design: spike 1 renders the bare procedural body from Ludus,
|
|
* team-tinted so three agents can be told apart at a glance. Real gear is a
|
|
* later swap onto the same meshes. The only thing that has to hold now is that
|
|
* every skater owns its own material instances, so recolouring one never
|
|
* touches another.
|
|
*/
|
|
|
|
export const TEAMS = [
|
|
{ name: 'home', jersey: 0xb8342c, accent: 0xf0e6d2 },
|
|
{ name: 'away', jersey: 0x2b5d8f, accent: 0xf0e6d2 },
|
|
{ name: 'third', jersey: 0x3d8c5a, accent: 0xf0e6d2 },
|
|
];
|
|
|
|
const SKIN_TONES = [0xd8a07a, 0xc98d63, 0xa86b45, 0x8a5334, 0xe8bd9a];
|
|
const PANTS = 0x1c1f26;
|
|
|
|
export function buildMaterials(rng, teamIndex = 0) {
|
|
const team = TEAMS[teamIndex % TEAMS.length];
|
|
const skinColor = rng.pick(SKIN_TONES);
|
|
|
|
// One material, vertex-coloured. `paintKit` writes the colours; keeping it to
|
|
// a single material means the skinned body is still one draw call.
|
|
const skin = new THREE.MeshStandardMaterial({
|
|
color: 0xffffff,
|
|
vertexColors: true,
|
|
roughness: 0.68,
|
|
metalness: 0.03,
|
|
});
|
|
skin.userData.skinColor = new THREE.Color(skinColor);
|
|
|
|
return { skin, team, teamIndex: teamIndex % TEAMS.length, skinColor };
|
|
}
|
|
|
|
/**
|
|
* Write the placeholder kit into the geometry's vertex colours.
|
|
*
|
|
* The loft carries `aPart` (which limb) and `aT` (0..1 along it), so the kit
|
|
* can be blocked in without any texture work: sweater over the torso and arms,
|
|
* pants over the hips and thighs, socks in the team colour down the shin.
|
|
*
|
|
* Overwrites the skin-weight heatmap `computeSkin` leaves behind; that array is
|
|
* kept on `userData` so the debug view can still be switched back on.
|
|
*/
|
|
export function paintKit(geo, { jersey, skinColor }) {
|
|
const partAttr = geo.attributes.aPart;
|
|
const tAttr = geo.attributes.aT;
|
|
const existing = geo.attributes.color;
|
|
if (existing && !geo.userData.heatColors) geo.userData.heatColors = existing.array.slice();
|
|
|
|
const n = geo.attributes.position.count;
|
|
const colors = new Float32Array(n * 3);
|
|
const c = new THREE.Color();
|
|
const flesh = new THREE.Color(skinColor);
|
|
const sweater = new THREE.Color(jersey);
|
|
const pants = new THREE.Color(PANTS);
|
|
|
|
for (let i = 0; i < n; i++) {
|
|
const part = partAttr ? partAttr.getX(i) : PART.TORSO;
|
|
const t = tAttr ? tAttr.getX(i) : 0.5;
|
|
if (part === PART.HEAD) {
|
|
// Helmet from the crown down to the brow; face left bare.
|
|
c.copy(t > 0.62 ? sweater : flesh);
|
|
} else if (part === PART.TORSO) {
|
|
c.copy(t < 0.16 ? pants : sweater);
|
|
} else if (part === PART.ARM_L || part === PART.ARM_R) {
|
|
// Sleeve, then a dark glove at the cuff.
|
|
c.copy(t > 0.88 ? pants : sweater);
|
|
} else {
|
|
// Leg: pants to mid-thigh, team sock below, black skate at the ankle.
|
|
c.copy(t < 0.36 ? pants : t > 0.87 ? pants : sweater);
|
|
}
|
|
colors[i * 3] = c.r;
|
|
colors[i * 3 + 1] = c.g;
|
|
colors[i * 3 + 2] = c.b;
|
|
}
|
|
geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
|
}
|
|
|
|
/**
|
|
* Base layer for a skater who is actually wearing gear.
|
|
*
|
|
* `paintKit` draws the kit *onto* the body, which is the right answer while the
|
|
* body is all there is. Once a jersey, pants and socks are real meshes over the
|
|
* top, painting a second jersey underneath only shows up as the wrong colour
|
|
* peeking out at a collar or a cuff. So: face and neck bare, everything else
|
|
* the dark under layer a player has on beneath the pads.
|
|
*/
|
|
export function paintUnderLayer(geo, { skinColor, under = 0x24262c }) {
|
|
const partAttr = geo.attributes.aPart;
|
|
const existing = geo.attributes.color;
|
|
if (existing && !geo.userData.heatColors) geo.userData.heatColors = existing.array.slice();
|
|
|
|
const n = geo.attributes.position.count;
|
|
const colors = new Float32Array(n * 3);
|
|
const flesh = new THREE.Color(skinColor);
|
|
const base = new THREE.Color(under);
|
|
|
|
for (let i = 0; i < n; i++) {
|
|
const part = partAttr ? partAttr.getX(i) : PART.TORSO;
|
|
const c = part === PART.HEAD ? flesh : base;
|
|
colors[i * 3] = c.r;
|
|
colors[i * 3 + 1] = c.g;
|
|
colors[i * 3 + 2] = c.b;
|
|
}
|
|
geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
|
}
|
|
|
|
/** Shared rink materials — one set for the whole scene, not per skater. */
|
|
export function buildRinkMaterials() {
|
|
return {
|
|
ice: new THREE.MeshStandardMaterial({
|
|
color: 0xeaf2fa,
|
|
roughness: 0.16,
|
|
metalness: 0.0,
|
|
}),
|
|
lines: new THREE.MeshBasicMaterial({ color: 0xffffff }),
|
|
boards: new THREE.MeshStandardMaterial({
|
|
color: 0xf2f2f0,
|
|
roughness: 0.5,
|
|
metalness: 0.02,
|
|
side: THREE.DoubleSide,
|
|
}),
|
|
kickplate: new THREE.MeshStandardMaterial({
|
|
color: 0xd6c33c,
|
|
roughness: 0.6,
|
|
side: THREE.DoubleSide,
|
|
}),
|
|
glass: new THREE.MeshStandardMaterial({
|
|
color: 0xc4dcea,
|
|
roughness: 0.06,
|
|
metalness: 0,
|
|
transparent: true,
|
|
opacity: 0.1,
|
|
side: THREE.DoubleSide,
|
|
depthWrite: false,
|
|
}),
|
|
};
|
|
}
|