125 lines
4.6 KiB
JavaScript
125 lines
4.6 KiB
JavaScript
import * as THREE from 'three';
|
|
import { NET, goalLineX } from '../../shared/net.js';
|
|
import { CAT, KIND, makeTag, xyz } from './bridge.js';
|
|
|
|
/**
|
|
* The goal frame: posts, crossbar, and a mesh back that stops the puck.
|
|
*
|
|
* Static bodies, because a net that moves is a rule (it comes off its moorings)
|
|
* rather than a feature, and not one worth having before there is a game.
|
|
*
|
|
* The back and sides are solid boxes rather than a real mesh. A puck that goes
|
|
* in should stay in and settle, and modelling twine is a lot of work to make a
|
|
* puck stop moving.
|
|
*/
|
|
export function createNet(physics, end) {
|
|
const { api, world } = physics;
|
|
const line = goalLineX(end);
|
|
const halfW = NET.width / 2;
|
|
const r = NET.postRadius;
|
|
|
|
const sd = api.b3DefaultShapeDef();
|
|
sd.baseMaterial.friction = 0.4;
|
|
// Posts ring; the back eats everything so the puck settles in the net.
|
|
sd.baseMaterial.restitution = 0.35;
|
|
sd.baseMaterial.userMaterialId = makeTag(KIND.RINK, 0xff, end > 0 ? 10 : 11);
|
|
sd.filter.categoryBits = CAT.RINK;
|
|
sd.filter.maskBits = 0xffffffffffffffffn;
|
|
|
|
const bodies = [];
|
|
const box = (x, y, z, hx, hy, hz, restitution = null) => {
|
|
const bd = api.b3DefaultBodyDef();
|
|
bd.position = xyz(x, y, z);
|
|
const b = api.b3CreateBody(world, bd);
|
|
if (restitution !== null) sd.baseMaterial.restitution = restitution;
|
|
api.b3CreateBoxShape(b, sd, hx, hy, hz);
|
|
sd.baseMaterial.restitution = 0.35;
|
|
bodies.push(b);
|
|
return b;
|
|
};
|
|
|
|
// Posts, on the line.
|
|
box(line, NET.height / 2, halfW, r, NET.height / 2, r);
|
|
box(line, NET.height / 2, -halfW, r, NET.height / 2, r);
|
|
// Crossbar.
|
|
box(line, NET.height, 0, r, r, halfW);
|
|
// Back and sides, deadened so the puck does not fire back out.
|
|
//
|
|
// The net extends *away* from centre ice, `line + end * depth`. Getting this
|
|
// sign backwards put the back panel a metre in front of the goal line — a
|
|
// solid wall across the mouth — and every shot in the game bounced off it
|
|
// before it could cross. Nothing ever scored, and the symptom looked like a
|
|
// goalie problem.
|
|
box(line + end * NET.depth, NET.height / 2, 0, 0.04, NET.height / 2, halfW, 0.02);
|
|
box(line + end * NET.depth * 0.5, NET.height / 2, halfW, NET.depth / 2, NET.height / 2, 0.03, 0.05);
|
|
box(line + end * NET.depth * 0.5, NET.height / 2, -halfW, NET.depth / 2, NET.height / 2, 0.03, 0.05);
|
|
|
|
return {
|
|
end,
|
|
bodies,
|
|
destroy() {
|
|
for (const b of bodies) api.b3DestroyBody(b);
|
|
},
|
|
};
|
|
}
|
|
|
|
/** The rendered net: frame tubes plus a translucent mesh bag. */
|
|
export function buildNetMesh(scene, end) {
|
|
const line = goalLineX(end);
|
|
const halfW = NET.width / 2;
|
|
const group = new THREE.Group();
|
|
group.name = 'net:' + end;
|
|
|
|
const frame = new THREE.MeshStandardMaterial({ color: 0xc0332c, roughness: 0.45, metalness: 0.25 });
|
|
const mesh = new THREE.MeshStandardMaterial({
|
|
color: 0xf2f4f8,
|
|
roughness: 0.9,
|
|
transparent: true,
|
|
opacity: 0.28,
|
|
side: THREE.DoubleSide,
|
|
depthWrite: false,
|
|
});
|
|
|
|
const tube = (len, x, y, z, axis) => {
|
|
const g = new THREE.CylinderGeometry(NET.postRadius, NET.postRadius, len, 10);
|
|
const m = new THREE.Mesh(g, frame);
|
|
if (axis === 'z') m.rotation.x = Math.PI / 2;
|
|
if (axis === 'x') m.rotation.z = Math.PI / 2;
|
|
m.position.set(x, y, z);
|
|
m.castShadow = true;
|
|
group.add(m);
|
|
};
|
|
tube(NET.height, line, NET.height / 2, halfW, 'y');
|
|
tube(NET.height, line, NET.height / 2, -halfW, 'y');
|
|
tube(NET.width, line, NET.height, 0, 'z');
|
|
// Back frame, so the net reads as a box rather than as a doorway.
|
|
tube(NET.depth, line + end * NET.depth / 2, 0.06, halfW, 'x');
|
|
tube(NET.depth, line + end * NET.depth / 2, 0.06, -halfW, 'x');
|
|
|
|
const back = new THREE.Mesh(new THREE.PlaneGeometry(NET.width, NET.height), mesh);
|
|
back.position.set(line + end * NET.depth, NET.height / 2, 0);
|
|
back.rotation.y = Math.PI / 2;
|
|
group.add(back);
|
|
for (const s of [1, -1]) {
|
|
const side = new THREE.Mesh(new THREE.PlaneGeometry(NET.depth, NET.height), mesh);
|
|
side.position.set(line + end * NET.depth / 2, NET.height / 2, s * halfW);
|
|
group.add(side);
|
|
}
|
|
const top = new THREE.Mesh(new THREE.PlaneGeometry(NET.depth, NET.width), mesh);
|
|
top.rotation.x = -Math.PI / 2;
|
|
top.position.set(line + end * NET.depth / 2, NET.height, 0);
|
|
group.add(top);
|
|
|
|
// Crease paint.
|
|
const crease = new THREE.Mesh(
|
|
new THREE.CircleGeometry(NET.creaseRadius, 24, end > 0 ? -Math.PI / 2 : Math.PI / 2, Math.PI),
|
|
new THREE.MeshBasicMaterial({ color: 0x77b3e0, transparent: true, opacity: 0.45, depthWrite: false }),
|
|
);
|
|
crease.rotation.x = -Math.PI / 2;
|
|
crease.position.set(line, 0.004, 0);
|
|
group.add(crease);
|
|
|
|
scene.add(group);
|
|
return group;
|
|
}
|