zen: UB is now the Queen angelfish (real model) + fix admin lockout
- Admin lockout: /zen checked blockedForViewer() before auth loaded, so a hard-refresh/
direct-link bounced admins to /play. Now revalidate auth (await refresh if !ready)
BEFORE the gate check.
- UB swap: retired the two-tail koi (ub.glb/ub-split.glb) for the vetted Queen angelfish.
Trimmed the 75.67s baked Take down to just the Idle loop (tools/glb-split/trim-idle.mjs
→ 16MB → 6.9MB) → static/models/ub-angelfish.glb. aquarium.js reworked for the pack's
ONE-mesh/TWO-material layout (…_body opaque single-sided; …_fins opaque alpha-tested,
tunable); animation is the trimmed Idle. Debug tuner (/zen?debug=1) updated: yaw/pitch/
scale + one fins&tail section. Still devgate IN_DEV={'zen'} — admin-only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -16,22 +16,20 @@
|
||||
import * as THREE from 'three';
|
||||
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||
|
||||
const MODEL_URL = '/models/ub-split.glb';
|
||||
const MODEL_URL = '/models/ub-angelfish.glb';
|
||||
|
||||
// Converged Phase-A starting point (Claude + Codex): a hair off strict profile so
|
||||
// the mouth/chin reads naturally; tail OPAQUE alpha-tested (one coherent tail, no
|
||||
// blend bleed); fins translucent + single-sided (kills the double-sided ghosting
|
||||
// that read as an off-center dorsal). All live-tunable at /zen?debug=1.
|
||||
// UB is now a marine angelfish (Queen): ONE mesh, TWO materials (…_body, …_fins — fins
|
||||
// covers the caudal tail too), and a single trimmed Idle loop. Body renders OPAQUE +
|
||||
// single-sided; fins default to OPAQUE alpha-tested (clean silhouette, no blend bleed —
|
||||
// the koi's lesson) but can go translucent. All live-tunable at /zen?debug=1.
|
||||
export const DEFAULTS = {
|
||||
yaw: Math.PI / 2 + 0.10, // ub.rotation.y — slight 3/4 view
|
||||
yaw: Math.PI / 2, // ub.rotation.y — side-on-ish (tune live; head faces ±Z)
|
||||
pitch: 0, // ub.rotation.x — nose up/down
|
||||
tailTranslucent: false, // false = opaque alpha-tested (coherent); true = blended
|
||||
tailSide: 'front', // front | back | double
|
||||
tailAlphaTest: 0.035,
|
||||
tailOpacity: 1.0, // only when translucent
|
||||
finSide: 'front', // front | back | double
|
||||
finOpacity: 0.75,
|
||||
finAlphaTest: 0.02,
|
||||
scale: 1, // multiplier on the auto-fit size
|
||||
finTranslucent: false, // false = opaque alpha-tested (coherent); true = blended
|
||||
finSide: 'double', // front | back | double (fins are thin → double reads fuller)
|
||||
finOpacity: 0.9, // only when translucent
|
||||
finAlphaTest: 0.5, // clip the fin-edge alpha
|
||||
paused: false,
|
||||
frame: 0, // 0..1 scrub position when paused
|
||||
};
|
||||
@@ -76,43 +74,41 @@ export async function createAquarium(canvas, initial = {}) {
|
||||
const center = box.getCenter(new THREE.Vector3());
|
||||
ub.position.sub(center);
|
||||
const maxDim = Math.max(size.x, size.y, size.z) || 1;
|
||||
ub.scale.setScalar(2.6 / maxDim);
|
||||
const baseScale = 2.6 / maxDim; // auto-fit; params.scale fine-tunes it live
|
||||
|
||||
// Collect the three named materials/meshes so applyMaterials can retarget them
|
||||
// live without re-traversing semantics.
|
||||
// Classify the two meshes by material-name substring (…_body / …_fins) so
|
||||
// applyMaterials can retarget them live regardless of the species prefix.
|
||||
const part = {};
|
||||
ub.traverse((o) => { if (o.isMesh && o.material?.name) part[o.material.name] = o; });
|
||||
ub.traverse((o) => {
|
||||
if (!o.isMesh || !o.material?.name) return;
|
||||
const n = o.material.name.toLowerCase();
|
||||
if (n.includes('body')) part.body = o; else if (n.includes('fin')) part.fins = o;
|
||||
});
|
||||
|
||||
function applyMaterials() {
|
||||
const body = part.UB_Body, fins = part.UB_Fins, tail = part.UB_Tail;
|
||||
if (body) {
|
||||
const body = part.body, fins = part.fins;
|
||||
if (body) { // body: always OPAQUE, single-sided
|
||||
const m = body.material;
|
||||
m.transparent = false; m.opacity = 1; m.alphaTest = 0;
|
||||
m.depthWrite = true; m.depthTest = true; m.side = THREE.FrontSide;
|
||||
body.renderOrder = 1; m.needsUpdate = true;
|
||||
}
|
||||
if (fins) {
|
||||
if (fins) { // fins (+ tail): opaque alpha-tested by default
|
||||
const m = fins.material;
|
||||
m.transparent = true; m.opacity = params.finOpacity; m.alphaTest = params.finAlphaTest;
|
||||
m.alphaToCoverage = true; m.depthWrite = false; m.depthTest = true;
|
||||
m.side = SIDE[params.finSide] ?? THREE.FrontSide;
|
||||
fins.renderOrder = 3; m.needsUpdate = true;
|
||||
}
|
||||
if (tail) {
|
||||
const m = tail.material;
|
||||
m.alphaTest = params.tailAlphaTest; m.alphaToCoverage = true; m.depthTest = true;
|
||||
m.side = SIDE[params.tailSide] ?? THREE.FrontSide;
|
||||
if (params.tailTranslucent) {
|
||||
m.transparent = true; m.opacity = params.tailOpacity; m.depthWrite = false;
|
||||
m.alphaTest = params.finAlphaTest; m.alphaToCoverage = true; m.depthTest = true;
|
||||
m.side = SIDE[params.finSide] ?? THREE.DoubleSide;
|
||||
if (params.finTranslucent) {
|
||||
m.transparent = true; m.opacity = params.finOpacity; m.depthWrite = false;
|
||||
} else {
|
||||
m.transparent = false; m.opacity = 1; m.depthWrite = true; // opaque, coherent
|
||||
m.transparent = false; m.opacity = 1; m.depthWrite = true; // clean, coherent
|
||||
}
|
||||
tail.renderOrder = 2; m.needsUpdate = true;
|
||||
fins.renderOrder = 2; m.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
function applyTransform() {
|
||||
ub.rotation.set(params.pitch, params.yaw, 0);
|
||||
ub.scale.setScalar(baseScale * (params.scale ?? 1));
|
||||
}
|
||||
|
||||
applyMaterials();
|
||||
@@ -124,7 +120,7 @@ export async function createAquarium(canvas, initial = {}) {
|
||||
const mixer = new THREE.AnimationMixer(ub);
|
||||
const actions = {};
|
||||
for (const clip of gltf.animations) actions[clip.name] = mixer.clipAction(clip);
|
||||
const baseClip = (actions.Idle_swim ? actions.Idle_swim : mixer.clipAction(gltf.animations[0]));
|
||||
const baseClip = mixer.clipAction(gltf.animations[0]); // the trimmed Idle loop
|
||||
baseClip.play();
|
||||
const baseDuration = baseClip.getClip().duration || 1;
|
||||
mixer.timeScale = reduced ? 0.6 : 1; // calmer when reduced-motion
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { auth } from '$lib/auth.svelte.js';
|
||||
import { auth, refresh as refreshAuth } from '$lib/auth.svelte.js';
|
||||
import { isDevGated, blockedForViewer } from '$lib/devgate.js';
|
||||
import Footer from '$lib/components/Footer.svelte';
|
||||
|
||||
@@ -25,12 +25,17 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Dev-gated while UB is being ironed out: non-admins (no preview token) bounce.
|
||||
if (blockedForViewer('zen', auth.user, $page.url)) { goto('/play'); return; }
|
||||
debug = $page.url.searchParams.get('debug') === '1';
|
||||
let h;
|
||||
let cancelled = false; // guard the async load against an early unmount
|
||||
(async () => {
|
||||
// Don't decide the gate until we actually KNOW who's here. A cold load / hard
|
||||
// refresh / direct link mounts before auth has revalidated, so auth.user is still
|
||||
// null and an admin would be wrongly bounced. Revalidate first, THEN gate.
|
||||
if (!auth.ready) { try { await refreshAuth(); } catch { /* offline → treat as gated */ } }
|
||||
if (cancelled) return;
|
||||
// Dev-gated while UB is being ironed out: non-admins (no preview token) bounce.
|
||||
if (blockedForViewer('zen', auth.user, $page.url)) { goto('/play'); return; }
|
||||
debug = $page.url.searchParams.get('debug') === '1';
|
||||
try {
|
||||
// WebGL guard — fall back to a warm card rather than a blank canvas.
|
||||
const t = document.createElement('canvas');
|
||||
@@ -88,27 +93,20 @@
|
||||
<input type="range" min="-3.15" max="3.15" step="0.01" bind:value={dbg.yaw} oninput={apply} /></label>
|
||||
<label>pitch <span>{dbg.pitch.toFixed(2)}</span>
|
||||
<input type="range" min="-0.6" max="0.6" step="0.01" bind:value={dbg.pitch} oninput={apply} /></label>
|
||||
<label>scale <span>{dbg.scale.toFixed(2)}</span>
|
||||
<input type="range" min="0.3" max="2.5" step="0.05" bind:value={dbg.scale} oninput={apply} /></label>
|
||||
|
||||
<hr />
|
||||
<div class="ph">Tail</div>
|
||||
<label class="chk"><input type="checkbox" bind:checked={dbg.tailTranslucent} onchange={apply} /> translucent (off = opaque, coherent)</label>
|
||||
<label>side
|
||||
<select bind:value={dbg.tailSide} onchange={apply}><option>front</option><option>back</option><option>double</option></select></label>
|
||||
<label>alphaTest <span>{dbg.tailAlphaTest.toFixed(3)}</span>
|
||||
<input type="range" min="0" max="0.2" step="0.005" bind:value={dbg.tailAlphaTest} oninput={apply} /></label>
|
||||
{#if dbg.tailTranslucent}
|
||||
<label>opacity <span>{dbg.tailOpacity.toFixed(2)}</span>
|
||||
<input type="range" min="0" max="1" step="0.05" bind:value={dbg.tailOpacity} oninput={apply} /></label>
|
||||
{/if}
|
||||
|
||||
<hr />
|
||||
<div class="ph">Fins</div>
|
||||
<div class="ph">Fins & tail</div>
|
||||
<label class="chk"><input type="checkbox" bind:checked={dbg.finTranslucent} onchange={apply} /> translucent (off = opaque, coherent)</label>
|
||||
<label>side
|
||||
<select bind:value={dbg.finSide} onchange={apply}><option>front</option><option>back</option><option>double</option></select></label>
|
||||
<label>opacity <span>{dbg.finOpacity.toFixed(2)}</span>
|
||||
<input type="range" min="0" max="1" step="0.05" bind:value={dbg.finOpacity} oninput={apply} /></label>
|
||||
<label>alphaTest <span>{dbg.finAlphaTest.toFixed(3)}</span>
|
||||
<input type="range" min="0" max="0.2" step="0.005" bind:value={dbg.finAlphaTest} oninput={apply} /></label>
|
||||
<input type="range" min="0" max="0.9" step="0.01" bind:value={dbg.finAlphaTest} oninput={apply} /></label>
|
||||
{#if dbg.finTranslucent}
|
||||
<label>opacity <span>{dbg.finOpacity.toFixed(2)}</span>
|
||||
<input type="range" min="0" max="1" step="0.05" bind:value={dbg.finOpacity} oninput={apply} /></label>
|
||||
{/if}
|
||||
|
||||
<hr />
|
||||
<label class="chk"><input type="checkbox" bind:checked={dbg.paused} onchange={apply} /> freeze frame</label>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user