clone

an endless marble-race generator in the browser; the url is the seed, so every track can be shared

M13: follow-cam island occlusion + banking lean

Dami committed 23e0277
Occluders option on the follow cam (island instanced meshes): desired
position pulled in front of the first marble->camera hit, so the cam
slides over shoulders instead of clipping through mass. Banking lean:
camera rolls into the upcoming curve (signed three-sample turn, speed-
scaled, smoothed; rotateZ(+phi) banks right). Both modes.
commit
23e0277b92cebe4ef13b790d02a38708b368ac47
parent
521b01343d3614f26dacaf98667b2fe43c294157
author
Dami <dami.etoile@gmail.com>
2 files changed, +98 -2
Msrc/client/camera.ts82++++++++++++++++++++++++++++++
Msrc/client/endless.ts18++++++-
M src/client/camera.ts +82 -0
@@ -46,6 +46,11 @@ export interface CameraOptions {
4646 * farther back (slalom-scale track pieces would swallow the default). */4747 trail?: number;4848 height?: number;49+ /** Objects the follow cam must not clip through (island masses): the50+ * desired position is pulled in front of the first hit along the51+ * marble → camera ray. Resolved per frame — pass a closure over the52+ * live scene. */53+ occluders?: () => readonly THREE.Object3D[];4954 }5055 5156 export function createCameras(
@@ -75,6 +80,12 @@ export function createCameras(
7580 const desired = new THREE.Vector3();7681 const lookTarget = new THREE.Vector3();7782 const lookNow = new THREE.Vector3();83+ const raycaster = new THREE.Raycaster();84+ const rayDir = new THREE.Vector3();85+ const bankA = new THREE.Vector3();86+ const bankB = new THREE.Vector3();87+ const bankC = new THREE.Vector3();88+ let bankNow = 0;7889 let lookInit = false;7990 8091 const api: Cameras = {
@@ -160,6 +171,33 @@ export function createCameras(
160171 lookTarget.copy(activeMarble.obj.position);161172 lookTarget.y += LOOK_UP;162173 174+ // Occlusion: never let island mass sit between marble and camera —175+ // pull the desired position in front of the first hit (small176+ // margin), so the cam slides over shoulders instead of clipping177+ // through them.178+ if (opts.occluders) {179+ const obs = opts.occluders();180+ if (obs.length > 0) {181+ rayDir.copy(desired).sub(lookTarget);182+ const dist = rayDir.length();183+ if (dist > 1e-4) {184+ rayDir.divideScalar(dist);185+ raycaster.set(lookTarget, rayDir);186+ raycaster.far = dist;187+ const hits = raycaster.intersectObjects(188+ obs as THREE.Object3D[],189+ true,190+ );191+ if (hits.length > 0) {192+ desired.copy(lookTarget).addScaledVector(193+ rayDir,194+ Math.max(0.8, hits[0].distance - 0.35),195+ );196+ }197+ }198+ }199+ }200+163201 // Frame-rate independent smoothing of the nominal 0.08/frame at 60fps.164202 const a = 1 - Math.pow(1 - LERP, Math.max(dt, 1e-4) * 60);165203 camera.position.lerp(desired, a);
@@ -170,6 +208,50 @@ export function createCameras(
170208 lookNow.lerp(lookTarget, a);171209 }172210 camera.lookAt(lookNow);211+212+ // Banking lean (M13): roll into the upcoming curve — signed turn213+ // from three path samples around the marble; rotateZ(+φ) banks214+ // right, a right turn has (d1×d2).y < 0, so φ = −θ·gain.215+ let bankTarget = 0;216+ const SPAN = 1.5;217+ const s = activeMarble.s;218+ const sampled = pathSampler219+ ? pathSampler(s - SPAN, bankA) && pathSampler(s, bankB) &&220+ pathSampler(s + SPAN, bankC)221+ : activeCurve222+ ? (() => {223+ const total = activeCurve.getLength();224+ if (total <= SPAN * 2) return false;225+ const at = (ss: number, out: THREE.Vector3) =>226+ activeCurve!.getPointAt(227+ (((ss % total) + total) % total) / total,228+ out,229+ );230+ at(s - SPAN, bankA);231+ at(s, bankB);232+ at(s + SPAN, bankC);233+ return true;234+ })()235+ : false;236+ if (sampled) {237+ const d1x = bankB.x - bankA.x;238+ const d1z = bankB.z - bankA.z;239+ const d2x = bankC.x - bankB.x;240+ const d2z = bankC.z - bankB.z;241+ const crossY = d1z * d2x - d1x * d2z;242+ const dot = d1x * d2x + d1z * d2z;243+ const turn = Math.atan2(crossY, dot);244+ bankTarget = Math.max(245+ -0.32,246+ Math.min(247+ 0.32,248+ -turn * (0.1 + activeMarble.speed * 0.035),249+ ),250+ );251+ }252+ bankNow += (bankTarget - bankNow) *253+ (1 - Math.pow(1 - 0.05, Math.max(dt, 1e-4) * 60));254+ camera.rotateZ(bankNow);173255 },174256 175257 dispose() {
M src/client/endless.ts +16 -2