diff --git a/.env.example b/.env.example index 6ba0afc..c82c9fc 100644 --- a/.env.example +++ b/.env.example @@ -57,8 +57,11 @@ VIEWS_IP_SALT= # context.dev fallback when Firecrawl is blocked (get key at https://context.dev) # CONTEXT_DEV_API_KEY= -# --- Game reverse 3D hero meshes (Meshy text-to-3D + optional auto-rig) --- +# --- Game reverse 3D hero meshes (Meshy text-to-3D + Quaternius kernel auto-rig) --- # MESHY_API_KEY=msy_... +# After Meshy sculpts a humanoid, gitreverse auto-rigs it onto the vendored +# Quaternius Universal Animation Library skeleton (public/quaternius/) and +# embeds Idle_Loop / Walk_Loop / Sprint_Loop / etc. Preview: /game/kernel # --- Stripe checkout (optional: Premium subscription and pay-per-use credits) --- # Server-side Checkout Session — success URL is set in code (?session_id={CHECKOUT_SESSION_ID}). diff --git a/app/api/game-kernel/fixture/route.ts b/app/api/game-kernel/fixture/route.ts new file mode 100644 index 0000000..36f96af --- /dev/null +++ b/app/api/game-kernel/fixture/route.ts @@ -0,0 +1,42 @@ +import { NextResponse } from "next/server"; +import { autoRigToQuaterniusKernel } from "@/lib/auto-rig-humanoid"; +import { buildTPoseDummyGlb } from "@/lib/tpose-dummy"; + +export const runtime = "nodejs"; + +function corsHeaders(extra: Record = {}): Record { + return { + "Access-Control-Allow-Origin": "*", + ...extra, + }; +} + +export async function GET() { + const dummy = await buildTPoseDummyGlb(); + const rigged = await autoRigToQuaterniusKernel(dummy); + if (!rigged.ok) { + return NextResponse.json( + { error: rigged.error }, + { status: 500, headers: corsHeaders() } + ); + } + + return new NextResponse(new Uint8Array(rigged.glb), { + status: 200, + headers: corsHeaders({ + "Content-Type": "model/gltf-binary", + "Content-Disposition": 'inline; filename="kernel-fixture.glb"', + "Cache-Control": "public, max-age=60", + }), + }); +} + +export async function OPTIONS() { + return new NextResponse(null, { + status: 204, + headers: corsHeaders({ + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", + }), + }); +} diff --git a/app/game/kernel/page.tsx b/app/game/kernel/page.tsx new file mode 100644 index 0000000..5380bd8 --- /dev/null +++ b/app/game/kernel/page.tsx @@ -0,0 +1,11 @@ +import type { Metadata } from "next"; +import { KernelLabPage } from "@/components/kernel-lab-page"; + +export const metadata: Metadata = { + title: "Quaternius movement kernel", + robots: { index: false, follow: false }, +}; + +export default function Page() { + return ; +} diff --git a/app/specs/[slug]/page.tsx b/app/specs/[slug]/page.tsx index 5fcbf49..595de44 100644 --- a/app/specs/[slug]/page.tsx +++ b/app/specs/[slug]/page.tsx @@ -1,6 +1,7 @@ import { notFound } from "next/navigation"; import { Navbar } from "@/components/navbar"; import { PromptMarkdown } from "@/components/prompt-markdown"; +import { HeroKernelPreview } from "@/components/hero-kernel-preview"; import { isValidGameSlug } from "@/lib/parse-game-input"; import { readGameReverse } from "@/lib/game-reverse-storage"; import { readHeroAssetManifest } from "@/lib/game-asset-storage"; @@ -24,6 +25,7 @@ export default async function GameSpecPage({ params }: PageProps) { const downloadHref = `/api/game-spec/${encodeURIComponent(slug)}?download=1`; const heroAssets = (await readHeroAssetManifest(slug))?.assets ?? []; + const heroPreview = heroAssets.find((asset) => asset.kind === "humanoid"); return (
@@ -58,6 +60,20 @@ export default async function GameSpecPage({ params }: PageProps) {
{cached.specMd}
+ {heroPreview ? ( +
+ +
+ ) : null} {heroAssets.length > 0 ? (

@@ -74,7 +90,11 @@ export default async function GameSpecPage({ params }: PageProps) { Download {asset.filename} - {asset.hasWalk ? "rigged walk" : "textured sculpt"} + {asset.kernel + ? "Quaternius kernel" + : asset.hasWalk + ? "rigged walk" + : "textured sculpt"} ))} diff --git a/components/game-reverse-page.tsx b/components/game-reverse-page.tsx index d99ab72..f9bcc44 100644 --- a/components/game-reverse-page.tsx +++ b/components/game-reverse-page.tsx @@ -3,9 +3,11 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { GameSpecFlavorText } from "@/components/game-spec-flavor-text"; +import { HeroKernelPreview } from "@/components/hero-kernel-preview"; import { Navbar } from "@/components/navbar"; import { PromptMarkdown } from "@/components/prompt-markdown"; import { nameToSlug, parseGameInput } from "@/lib/parse-game-input"; +import type { StoredHeroAsset } from "@/lib/game-asset-storage"; type GameReversePageProps = { gameSlug: string; @@ -24,13 +26,33 @@ export function GameReversePage({ gameSlug, gameName }: GameReversePageProps) { const [loading, setLoading] = useState(true); const [statusLine, setStatusLine] = useState("Checking if it's cached…"); const [copied, setCopied] = useState(false); + const [heroAssets, setHeroAssets] = useState< + Array + >([]); const started = useRef(false); const resultsRef = useRef(null); + const loadHeroAssets = useCallback(async (slug: string) => { + try { + const res = await fetch(`/api/game-assets/${encodeURIComponent(slug)}`); + if (!res.ok) { + setHeroAssets([]); + return; + } + const data = (await res.json()) as { + assets?: Array; + }; + setHeroAssets(data.assets ?? []); + } catch { + setHeroAssets([]); + } + }, []); + const run = useCallback(async (slug: string, name: string) => { setLoading(true); setError(null); setPrompt(null); + setHeroAssets([]); setStatusLine("Checking if it's cached…"); try { @@ -57,6 +79,7 @@ export function GameReversePage({ gameSlug, gameName }: GameReversePageProps) { if (data.prompt) { setPrompt(data.prompt); if (data.fromCache) setStatusLine("Loaded from cache"); + void loadHeroAssets(slug); } else { throw new Error("No prompt returned."); } @@ -101,6 +124,7 @@ export function GameReversePage({ gameSlug, gameName }: GameReversePageProps) { if (event === "done" && typeof json.prompt === "string") { setPrompt(json.prompt); if (json.fromCache) setStatusLine("Loaded from cache"); + void loadHeroAssets(slug); } if (event === "error" && typeof json.error === "string") { throw new Error(json.error); @@ -117,7 +141,7 @@ export function GameReversePage({ gameSlug, gameName }: GameReversePageProps) { } finally { setLoading(false); } - }, []); + }, [loadHeroAssets]); useEffect(() => { if (started.current) return; @@ -318,6 +342,35 @@ export function GameReversePage({ gameSlug, gameName }: GameReversePageProps) {

) : null} + + {prompt ? ( +
+
+
+ {heroAssets.find((a) => a.kind === "humanoid") ? ( + a.kind === "humanoid")?.url ?? + undefined + } + title="Generated hero" + subtitle={ + heroAssets.find((a) => a.kernel) + ? "Auto-rigged to the Quaternius Universal skeleton" + : "Textured sculpt (kernel bind pending)" + } + autoClip="Walk_Loop" + /> + ) : ( + + )} +
+
+ ) : null}
); diff --git a/components/hero-kernel-preview.tsx b/components/hero-kernel-preview.tsx new file mode 100644 index 0000000..4dd91de --- /dev/null +++ b/components/hero-kernel-preview.tsx @@ -0,0 +1,244 @@ +"use client"; + +import { useEffect, useMemo, useRef, useState } from "react"; +import { + KERNEL_PREVIEW_CLIPS, + QUATERNIUS_STANDARD_PUBLIC_PATH, + type QuaterniusClip, +} from "@/lib/quaternius-kernel"; + +type HeroKernelPreviewProps = { + modelUrl?: string | null; + title?: string; + subtitle?: string; + autoClip?: QuaterniusClip; +}; + +export function HeroKernelPreview({ + modelUrl, + title = "Quaternius kernel", + subtitle, + autoClip = "Idle_Loop", +}: HeroKernelPreviewProps) { + const canvasRef = useRef(null); + const [clip, setClip] = useState(autoClip); + const [available, setAvailable] = useState([...KERNEL_PREVIEW_CLIPS]); + const [status, setStatus] = useState("Loading kernel…"); + const [error, setError] = useState(null); + const playClipRef = useRef<(name: string) => void>(() => {}); + + const src = modelUrl || QUATERNIUS_STANDARD_PUBLIC_PATH; + const clipButtons = useMemo(() => { + const preferred = KERNEL_PREVIEW_CLIPS.filter((name) => + available.includes(name) + ); + return preferred.length ? preferred : available.slice(0, 12); + }, [available]); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + let disposed = false; + let renderer: import("three").WebGLRenderer | null = null; + let mixer: import("three").AnimationMixer | null = null; + let frame = 0; + let onResize: (() => void) | null = null; + + async function boot() { + setError(null); + setStatus("Loading kernel…"); + const THREE = await import("three"); + const { GLTFLoader } = await import( + "three/examples/jsm/loaders/GLTFLoader.js" + ); + const { OrbitControls } = await import( + "three/examples/jsm/controls/OrbitControls.js" + ); + if (disposed || !canvas) return; + + const scene = new THREE.Scene(); + scene.background = new THREE.Color(0xf6efe2); + + const camera = new THREE.PerspectiveCamera( + 35, + canvas.clientWidth / Math.max(1, canvas.clientHeight), + 0.05, + 50 + ); + camera.position.set(2.4, 1.4, 3.2); + + renderer = new THREE.WebGLRenderer({ + canvas, + antialias: true, + alpha: false, + }); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + renderer.setSize(canvas.clientWidth, canvas.clientHeight, false); + renderer.outputColorSpace = THREE.SRGBColorSpace; + renderer.shadowMap.enabled = true; + + const hemi = new THREE.HemisphereLight(0xfff4da, 0x6b5a44, 1.1); + scene.add(hemi); + const key = new THREE.DirectionalLight(0xffffff, 1.4); + key.position.set(2.2, 4, 3); + key.castShadow = true; + scene.add(key); + scene.add(new THREE.AmbientLight(0xffffff, 0.25)); + + const ground = new THREE.Mesh( + new THREE.CircleGeometry(3.2, 48), + new THREE.MeshStandardMaterial({ + color: 0xe8d7b8, + roughness: 1, + metalness: 0, + }) + ); + ground.rotation.x = -Math.PI / 2; + ground.receiveShadow = true; + scene.add(ground); + + const controls = new OrbitControls(camera, canvas); + controls.target.set(0, 0.95, 0); + controls.enableDamping = true; + controls.maxPolarAngle = Math.PI * 0.49; + controls.minDistance = 1.2; + controls.maxDistance = 7; + + const gltf = await new GLTFLoader().loadAsync(src); + if (disposed) return; + + const root = gltf.scene; + root.traverse((obj) => { + const mesh = obj as import("three").Mesh; + if (mesh.isMesh) { + mesh.castShadow = true; + mesh.receiveShadow = true; + } + }); + scene.add(root); + + const clips = gltf.animations ?? []; + const names = clips.map((c) => c.name).filter(Boolean); + setAvailable(names.length ? names : [...KERNEL_PREVIEW_CLIPS]); + + mixer = new THREE.AnimationMixer(root); + const actions = new Map(); + for (const c of clips) { + const action = mixer.clipAction(c); + action.enabled = true; + actions.set(c.name, action); + } + + playClipRef.current = (name: string) => { + if (!mixer) return; + const next = actions.get(name); + if (!next) { + setStatus(`Missing clip ${name}`); + return; + } + for (const action of actions.values()) { + if (action !== next) action.fadeOut(0.18); + } + next.reset().fadeIn(0.18).play(); + setStatus(name); + }; + + const initial = + actions.get(autoClip)?.getClip().name ?? + names.find((n) => n === "Idle_Loop") ?? + names[0]; + if (initial) { + setClip(initial); + playClipRef.current(initial); + } else { + setStatus("Loaded mesh, no clips"); + } + + const clock = new THREE.Clock(); + onResize = () => { + if (!renderer || !canvas) return; + const w = Math.max(1, canvas.clientWidth); + const h = Math.max(1, canvas.clientHeight); + camera.aspect = w / h; + camera.updateProjectionMatrix(); + renderer.setSize(w, h, false); + }; + onResize(); + window.addEventListener("resize", onResize); + + const tick = () => { + if (disposed) return; + frame = requestAnimationFrame(tick); + const dt = clock.getDelta(); + mixer?.update(dt); + controls.update(); + renderer?.render(scene, camera); + }; + tick(); + } + + const cleanupPromise = boot().catch((e) => { + if (!disposed) { + setError(e instanceof Error ? e.message : String(e)); + setStatus("Failed to load"); + } + }); + + return () => { + disposed = true; + cancelAnimationFrame(frame); + if (onResize) window.removeEventListener("resize", onResize); + playClipRef.current = () => {}; + mixer?.stopAllAction(); + renderer?.dispose(); + void cleanupPromise; + }; + }, [src, autoClip]); + + return ( +
+
+
+

{title}

+

+ {subtitle ?? "Quaternius Universal Animation Library kernel"} +

+
+

+ {error ? error : status} +

+
+
+ +
+ {error ? ( +

{error}

+ ) : ( +
+ {clipButtons.map((name) => ( + + ))} +
+ )} +
+ ); +} diff --git a/components/kernel-lab-page.tsx b/components/kernel-lab-page.tsx new file mode 100644 index 0000000..d1441b5 --- /dev/null +++ b/components/kernel-lab-page.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { Navbar } from "@/components/navbar"; +import { HeroKernelPreview } from "@/components/hero-kernel-preview"; +import { + QUATERNIUS_ROOT_MOTION_PUBLIC_PATH, + QUATERNIUS_STANDARD_PUBLIC_PATH, +} from "@/lib/quaternius-kernel"; + +export function KernelLabPage() { + return ( +
+ +
+
+

+ Quaternius movement kernel +

+

+ Game reverse auto-rigs generated Meshy meshes onto this Universal + skeleton and plays these clips. In-place playback uses{" "} + {QUATERNIUS_STANDARD_PUBLIC_PATH}. + Traveling locomotion is{" "} + + {QUATERNIUS_ROOT_MOTION_PUBLIC_PATH} + + . +

+
+ +
+
+
+ +
+
+ +
+
+
+ +
+
+
+
+ ); +} diff --git a/lib/assets/GAME.template.md b/lib/assets/GAME.template.md index 68a57ae..3a45a25 100644 --- a/lib/assets/GAME.template.md +++ b/lib/assets/GAME.template.md @@ -26,7 +26,7 @@ Define a **buildable** slice, not the full commercial game. - Win / lose / fail states: - Single player only unless evidence says otherwise: -When listing out of scope: ban the full map, licensed music, a GLB per building, and a full animation graph. Do **not** ban the one on-camera hero mesh if the camera is third-person, over-shoulder, or otherwise frames a character. +When listing out of scope: ban the full map, licensed music, a GLB per building, and a full custom animation graph. Do **not** ban the one on-camera hero mesh if the camera is third-person, over-shoulder, or otherwise frames a character. Playing the bundled Quaternius Universal clips on that hero is in v1. ## 3. Frozen Stack @@ -113,7 +113,7 @@ Rules: - "Optional hero mesh later" is forbidden for identity objects. Agents skip later. - First-person / cockpit games can skip a detailed body. - 2D games: the hero is a sprite sheet or texture atlas, not a GLB. -- Simple idle/walk on one hero is in scope. A full animation graph and a GLB-per-building pipeline are not. +- Simple idle/walk/run on one hero is in scope: play the Quaternius Universal clips already bound on the hero GLB (`Idle_Loop`, `Walk_Loop`, `Jog_Fwd_Loop`, `Sprint_Loop`, jump and combat clips). Do not invent keyframes. A full custom animation graph and a GLB-per-building pipeline are not in v1. ### Asset tiers (critical) diff --git a/lib/auto-rig-humanoid.ts b/lib/auto-rig-humanoid.ts new file mode 100644 index 0000000..ca24490 --- /dev/null +++ b/lib/auto-rig-humanoid.ts @@ -0,0 +1,641 @@ +import { readFile } from "node:fs/promises"; +import { + type Accessor, + Document, + Logger, + type Material, + type mat4, + NodeIO, + type Node as GltfNode, +} from "@gltf-transform/core"; +import { ALL_EXTENSIONS } from "@gltf-transform/extensions"; +import { + cloneDocument, + copyToDocument, + prune, +} from "@gltf-transform/functions"; +import { + isDeformJoint, + QUATERNIUS_CLIPS, + QUATERNIUS_JOINT_COUNT, + quaterniusStandardDiskPath, +} from "./quaternius-kernel"; + +export type AutoRigSuccess = { + ok: true; + glb: Buffer; + clips: string[]; + jointCount: number; + vertexCount: number; + scale: number; +}; + +export type AutoRigFailure = { ok: false; error: string }; +export type AutoRigResult = AutoRigSuccess | AutoRigFailure; + +type Vec3 = [number, number, number]; +type Aabb = { min: Vec3; max: Vec3 }; + +type BoneSegment = { + index: number; + name: string; + a: Vec3; + b: Vec3; +}; + +type WorldPrim = { + positions: Float32Array; + normals: Float32Array | null; + uvs: Float32Array | null; + indices: Uint32Array | null; + material: Material | null; +}; + +const MANNEQUIN_HEIGHT = 1.829; +const INFLUENCE_COUNT = 4; +const WEIGHT_POWER = 2.2; + +function io(): NodeIO { + return new NodeIO().registerExtensions(ALL_EXTENSIONS); +} + +function sub(a: Vec3, b: Vec3): Vec3 { + return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; +} + +function add(a: Vec3, b: Vec3): Vec3 { + return [a[0] + b[0], a[1] + b[1], a[2] + b[2]]; +} + +function scaleVec(a: Vec3, s: number): Vec3 { + return [a[0] * s, a[1] * s, a[2] * s]; +} + +function dot(a: Vec3, b: Vec3): number { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; +} + +function lengthSq(a: Vec3): number { + return dot(a, a); +} + +function clamp(n: number, lo: number, hi: number): number { + return Math.min(hi, Math.max(lo, n)); +} + +function transformPoint(m: mat4, p: Vec3): Vec3 { + const x = p[0]; + const y = p[1]; + const z = p[2]; + const w = m[3] * x + m[7] * y + m[11] * z + m[15] || 1; + return [ + (m[0] * x + m[4] * y + m[8] * z + m[12]) / w, + (m[1] * x + m[5] * y + m[9] * z + m[13]) / w, + (m[2] * x + m[6] * y + m[10] * z + m[14]) / w, + ]; +} + +function transformDir(m: mat4, p: Vec3): Vec3 { + return [ + m[0] * p[0] + m[4] * p[1] + m[8] * p[2], + m[1] * p[0] + m[5] * p[1] + m[9] * p[2], + m[2] * p[0] + m[6] * p[1] + m[10] * p[2], + ]; +} + +function emptyAabb(): Aabb { + return { + min: [Infinity, Infinity, Infinity], + max: [-Infinity, -Infinity, -Infinity], + }; +} + +function expandAabb(box: Aabb, p: Vec3): void { + for (let i = 0; i < 3; i++) { + box.min[i] = Math.min(box.min[i], p[i]); + box.max[i] = Math.max(box.max[i], p[i]); + } +} + +function aabbSize(box: Aabb): Vec3 { + return sub(box.max, box.min); +} + +function distPointToSegmentSq(p: Vec3, a: Vec3, b: Vec3): number { + const ab = sub(b, a); + const abLen = lengthSq(ab); + if (abLen < 1e-10) return lengthSq(sub(p, a)); + const t = clamp(dot(sub(p, a), ab) / abLen, 0, 1); + const closest = add(a, scaleVec(ab, t)); + return lengthSq(sub(p, closest)); +} + +function rotateZUpToYUp(p: Vec3): Vec3 { + // (x, y, z) Z-up -> (x, z, -y) Y-up + return [p[0], p[2], -p[1]]; +} + +export function detectUpAxis(box: Aabb): "y" | "z" { + const size = aabbSize(box); + if (size[2] > size[1] * 1.2 && box.max[2] > box.max[1]) return "z"; + return "y"; +} + +export function fitAabbToUniversalHeight(box: Aabb): { + scale: number; + offset: Vec3; + upAxis: "y" | "z"; +} { + const upAxis = detectUpAxis(box); + const corners: Vec3[] = []; + for (const x of [box.min[0], box.max[0]]) { + for (const y of [box.min[1], box.max[1]]) { + for (const z of [box.min[2], box.max[2]]) { + corners.push(upAxis === "z" ? rotateZUpToYUp([x, y, z]) : [x, y, z]); + } + } + } + const world = emptyAabb(); + for (const c of corners) expandAabb(world, c); + + const height = Math.max(1e-4, world.max[1] - world.min[1]); + const scale = MANNEQUIN_HEIGHT / height; + const offset: Vec3 = [ + -((world.min[0] + world.max[0]) / 2) * scale, + -world.min[1] * scale, + -((world.min[2] + world.max[2]) / 2) * scale, + ]; + return { scale, offset, upAxis }; +} + +function applyFit(p: Vec3, scale: number, offset: Vec3, upAxis: "y" | "z"): Vec3 { + const q = upAxis === "z" ? rotateZUpToYUp(p) : p; + return add(scaleVec(q, scale), offset); +} + +function collectWorldPrims(doc: Document): { prims: WorldPrim[]; box: Aabb } { + const box = emptyAabb(); + const prims: WorldPrim[] = []; + + const visit = (node: GltfNode) => { + const mesh = node.getMesh(); + if (mesh) { + const world = node.getWorldMatrix(); + for (const primitive of mesh.listPrimitives()) { + const posAcc = primitive.getAttribute("POSITION"); + if (!posAcc) continue; + const n = posAcc.getCount(); + const positions = new Float32Array(n * 3); + const tmp: number[] = [0, 0, 0]; + for (let i = 0; i < n; i++) { + posAcc.getElement(i, tmp); + const wp = transformPoint(world, tmp as Vec3); + positions[i * 3] = wp[0]; + positions[i * 3 + 1] = wp[1]; + positions[i * 3 + 2] = wp[2]; + expandAabb(box, wp); + } + + const nrmAcc = primitive.getAttribute("NORMAL"); + let normals: Float32Array | null = null; + if (nrmAcc && nrmAcc.getCount() === n) { + normals = new Float32Array(n * 3); + for (let i = 0; i < n; i++) { + nrmAcc.getElement(i, tmp); + const wn = transformDir(world, tmp as Vec3); + normals[i * 3] = wn[0]; + normals[i * 3 + 1] = wn[1]; + normals[i * 3 + 2] = wn[2]; + } + } + + const uvAcc = primitive.getAttribute("TEXCOORD_0"); + let uvs: Float32Array | null = null; + if (uvAcc && uvAcc.getCount() === n) { + uvs = new Float32Array(n * 2); + const uv = [0, 0]; + for (let i = 0; i < n; i++) { + uvAcc.getElement(i, uv); + uvs[i * 2] = uv[0]; + uvs[i * 2 + 1] = uv[1]; + } + } + + const idxAcc = primitive.getIndices(); + let indices: Uint32Array | null = null; + if (idxAcc) { + indices = new Uint32Array(idxAcc.getCount()); + const one = [0]; + for (let i = 0; i < idxAcc.getCount(); i++) { + idxAcc.getElement(i, one); + indices[i] = one[0]; + } + } + + prims.push({ + positions, + normals, + uvs, + indices, + material: primitive.getMaterial(), + }); + } + } + for (const child of node.listChildren()) visit(child); + }; + + const scene = doc.getRoot().getDefaultScene() ?? doc.getRoot().listScenes()[0]; + if (scene) { + for (const child of scene.listChildren()) visit(child); + } else { + for (const node of doc.getRoot().listNodes()) { + if (!node.getParentNode()) visit(node); + } + } + + return { prims, box }; +} + +function buildBoneSegments(joints: GltfNode[]): BoneSegment[] { + const indexByJoint = new Map(); + joints.forEach((j, i) => indexByJoint.set(j, i)); + const segments: BoneSegment[] = []; + + joints.forEach((joint, index) => { + if (!isDeformJoint(joint.getName())) return; + const a = joint.getWorldTranslation() as Vec3; + const childJoints = joint + .listChildren() + .filter((c) => indexByJoint.has(c) && isDeformJoint(c.getName())); + if (childJoints.length === 0) { + const parent = joint.getParentNode(); + const parentPos = parent ? (parent.getWorldTranslation() as Vec3) : a; + const dir = sub(a, parentPos); + const fallback = lengthSq(dir) < 1e-8 ? ([0, 0.08, 0] as Vec3) : scaleVec(dir, 0.35); + segments.push({ index, name: joint.getName(), a, b: add(a, fallback) }); + return; + } + for (const child of childJoints) { + segments.push({ + index, + name: joint.getName(), + a, + b: child.getWorldTranslation() as Vec3, + }); + } + }); + return segments; +} + +function regionAllowsBone(p: Vec3, boneName: string): boolean { + const x = p[0]; + const y = p[1]; + const absX = Math.abs(x); + + if (boneName === "pelvis" && y < 0.82) return false; + + const isHeadBone = boneName === "Head" || boneName === "neck_01"; + if (isHeadBone && y < 1.35) return false; + if (y > 1.55 && absX < 0.22 && !isHeadBone && !boneName.startsWith("spine")) { + return false; + } + + const isLeftArm = + boneName.endsWith("_l") && + (boneName.includes("arm") || + boneName.includes("hand") || + boneName.includes("clavicle") || + boneName.includes("index") || + boneName.includes("middle") || + boneName.includes("ring") || + boneName.includes("pinky") || + boneName.includes("thumb")); + const isRightArm = + boneName.endsWith("_r") && + (boneName.includes("arm") || + boneName.includes("hand") || + boneName.includes("clavicle") || + boneName.includes("index") || + boneName.includes("middle") || + boneName.includes("ring") || + boneName.includes("pinky") || + boneName.includes("thumb")); + + if (absX > 0.28 && y > 1.2 && y < 1.58) { + if (x > 0 && !isLeftArm) return false; + if (x < 0 && !isRightArm) return false; + } + + const isLeftLeg = + boneName.endsWith("_l") && + (boneName.includes("thigh") || + boneName.includes("calf") || + boneName.includes("foot") || + boneName.includes("ball")); + const isRightLeg = + boneName.endsWith("_r") && + (boneName.includes("thigh") || + boneName.includes("calf") || + boneName.includes("foot") || + boneName.includes("ball")); + if (y < 0.85) { + if (x > 0.02 && !isLeftLeg) return false; + if (x < -0.02 && !isRightLeg) return false; + if (Math.abs(x) <= 0.02 && !isLeftLeg && !isRightLeg) return false; + } + + return true; +} + +function skinVertex( + p: Vec3, + segments: BoneSegment[], + jointCount: number +): { indices: number[]; weights: number[] } { + const best: { index: number; distSq: number }[] = []; + const seen = new Set(); + + for (const seg of segments) { + if (!regionAllowsBone(p, seg.name)) continue; + const d = distPointToSegmentSq(p, seg.a, seg.b); + if (seen.has(seg.index) && best.find((b) => b.index === seg.index && b.distSq <= d)) { + continue; + } + let replaced = false; + for (let i = 0; i < best.length; i++) { + if (best[i].index === seg.index && d < best[i].distSq) { + best[i] = { index: seg.index, distSq: d }; + replaced = true; + break; + } + } + if (replaced) continue; + if (seen.has(seg.index)) continue; + best.push({ index: seg.index, distSq: d }); + seen.add(seg.index); + } + + best.sort((a, b) => a.distSq - b.distSq); + const top = best.slice(0, INFLUENCE_COUNT); + if (!top.length) { + return { indices: [1, 0, 0, 0], weights: [1, 0, 0, 0] }; + } + + const raw = top.map((b) => 1 / Math.pow(b.distSq + 1e-6, WEIGHT_POWER / 2)); + const sum = raw.reduce((s, v) => s + v, 0) || 1; + const indices = [0, 0, 0, 0]; + const weights = [0, 0, 0, 0]; + top.forEach((b, i) => { + indices[i] = clamp(b.index, 0, jointCount - 1); + weights[i] = raw[i] / sum; + }); + return { indices, weights }; +} + +function copyAccessorArray( + dest: Document, + buffer: ReturnType, + name: string, + type: "SCALAR" | "VEC2" | "VEC3" | "VEC4", + array: Float32Array | Uint16Array | Uint32Array +): Accessor { + const copy = + array instanceof Uint16Array + ? new Uint16Array(array) + : array instanceof Uint32Array + ? new Uint32Array(array) + : new Float32Array(array); + return dest.createAccessor(name, buffer).setType(type).setArray(copy); +} + +/** + * Bind an unrigged (or differently-rigged) mesh onto the Quaternius Universal + * skeleton and pack the Standard kernel clips into the same GLB. + */ +export async function autoRigToQuaterniusKernel( + meshBytes: Buffer | Uint8Array, + opts?: { kernelPath?: string } +): Promise { + const kernelPath = opts?.kernelPath ?? quaterniusStandardDiskPath(); + let kernelBytes: Buffer; + try { + kernelBytes = await readFile(kernelPath); + } catch { + return { ok: false, error: `Quaternius kernel missing at ${kernelPath}` }; + } + + try { + const reader = io(); + const kernelDoc = await reader.readBinary(new Uint8Array(kernelBytes)); + const meshDoc = await reader.readBinary(new Uint8Array(meshBytes)); + + const skin = kernelDoc.getRoot().listSkins()[0]; + if (!skin) return { ok: false, error: "Quaternius kernel has no skin" }; + const joints = skin.listJoints(); + if (joints.length !== QUATERNIUS_JOINT_COUNT) { + return { + ok: false, + error: `Kernel joint count ${joints.length} != ${QUATERNIUS_JOINT_COUNT}`, + }; + } + + const { prims, box } = collectWorldPrims(meshDoc); + if (!prims.length || !Number.isFinite(box.min[0])) { + return { ok: false, error: "Source mesh has no vertices" }; + } + + const { scale, offset, upAxis } = fitAabbToUniversalHeight(box); + if (!Number.isFinite(scale) || scale <= 0) { + return { ok: false, error: "Could not compute a valid fit scale" }; + } + + for (const prim of prims) { + const n = prim.positions.length / 3; + for (let i = 0; i < n; i++) { + const fitted = applyFit( + [ + prim.positions[i * 3], + prim.positions[i * 3 + 1], + prim.positions[i * 3 + 2], + ], + scale, + offset, + upAxis + ); + prim.positions[i * 3] = fitted[0]; + prim.positions[i * 3 + 1] = fitted[1]; + prim.positions[i * 3 + 2] = fitted[2]; + if (prim.normals && upAxis === "z") { + const nrm = rotateZUpToYUp([ + prim.normals[i * 3], + prim.normals[i * 3 + 1], + prim.normals[i * 3 + 2], + ]); + prim.normals[i * 3] = nrm[0]; + prim.normals[i * 3 + 1] = nrm[1]; + prim.normals[i * 3 + 2] = nrm[2]; + } + } + } + + const dest = cloneDocument(kernelDoc); + dest.setLogger(new Logger(Logger.Verbosity.ERROR)); + dest.getRoot().getAsset().generator = "gitreverse-quaternius-kernel"; + const destSkin = dest.getRoot().listSkins()[0]; + const destJoints = destSkin.listJoints(); + const segments = buildBoneSegments(destJoints); + const destBuffer = + dest.getRoot().listBuffers()[0] ?? dest.createBuffer("kernel"); + + const materialMap = copyToDocument( + dest, + meshDoc, + meshDoc + .getRoot() + .listMaterials() + .filter(Boolean) + ); + + const heroMesh = dest.createMesh("Hero"); + let vertexCount = 0; + + prims.forEach((prim, primIndex) => { + const n = prim.positions.length / 3; + vertexCount += n; + const jointsArr = new Uint16Array(n * 4); + const weightsArr = new Float32Array(n * 4); + for (let i = 0; i < n; i++) { + const p: Vec3 = [ + prim.positions[i * 3], + prim.positions[i * 3 + 1], + prim.positions[i * 3 + 2], + ]; + const skinned = skinVertex(p, segments, destJoints.length); + for (let k = 0; k < 4; k++) { + jointsArr[i * 4 + k] = skinned.indices[k]; + weightsArr[i * 4 + k] = skinned.weights[k]; + } + } + + const destPrim = dest.createPrimitive().setMode(4); + destPrim.setAttribute( + "POSITION", + copyAccessorArray( + dest, + destBuffer, + `hero_${primIndex}_pos`, + "VEC3", + prim.positions + ) + ); + if (prim.normals) { + destPrim.setAttribute( + "NORMAL", + copyAccessorArray( + dest, + destBuffer, + `hero_${primIndex}_nrm`, + "VEC3", + prim.normals + ) + ); + } + if (prim.uvs) { + destPrim.setAttribute( + "TEXCOORD_0", + copyAccessorArray( + dest, + destBuffer, + `hero_${primIndex}_uv`, + "VEC2", + prim.uvs + ) + ); + } + destPrim.setAttribute( + "JOINTS_0", + copyAccessorArray( + dest, + destBuffer, + `hero_${primIndex}_joints`, + "VEC4", + jointsArr + ) + ); + destPrim.setAttribute( + "WEIGHTS_0", + copyAccessorArray( + dest, + destBuffer, + `hero_${primIndex}_weights`, + "VEC4", + weightsArr + ) + ); + if (prim.indices) { + destPrim.setIndices( + copyAccessorArray( + dest, + destBuffer, + `hero_${primIndex}_idx`, + "SCALAR", + prim.indices + ) + ); + } + if (prim.material) { + const copied = materialMap.get(prim.material); + if (copied && copied.propertyType === "Material") { + destPrim.setMaterial(copied as Material); + } + } + heroMesh.addPrimitive(destPrim); + }); + + let bound = false; + for (const node of dest.getRoot().listNodes()) { + const mesh = node.getMesh(); + if (mesh && (mesh.getName() === "Mannequin" || node.getSkin() === destSkin)) { + const old = mesh; + node.setMesh(heroMesh); + node.setSkin(destSkin); + if (old && old !== heroMesh) old.dispose(); + bound = true; + } + } + if (!bound) { + const armature = + dest.getRoot().listNodes().find((n) => n.getName() === "Armature") ?? + dest.getRoot().listNodes()[0]; + const heroNode = dest.createNode("Hero").setMesh(heroMesh).setSkin(destSkin); + if (armature) armature.addChild(heroNode); + else dest.getRoot().listScenes()[0]?.addChild(heroNode); + } + + await dest.transform(prune({ keepLeaves: true })); + + const clips = dest + .getRoot() + .listAnimations() + .map((a) => a.getName()) + .filter(Boolean); + if (clips.length < 10) { + return { ok: false, error: `Kernel clips missing after rig (${clips.length})` }; + } + + const glb = await io().writeBinary(dest); + return { + ok: true, + glb: Buffer.from(glb), + clips: clips.length ? clips : [...QUATERNIUS_CLIPS], + jointCount: destJoints.length, + vertexCount, + scale, + }; + } catch (e) { + return { + ok: false, + error: e instanceof Error ? e.message : String(e), + }; + } +} diff --git a/lib/game-asset-storage.ts b/lib/game-asset-storage.ts index 337defa..7c9ccbe 100644 --- a/lib/game-asset-storage.ts +++ b/lib/game-asset-storage.ts @@ -19,6 +19,8 @@ export type StoredHeroAsset = { rigged: boolean; hasWalk: boolean; prompt: string; + kernel?: "quaternius-ual1" | null; + clips?: string[]; }; export type HeroAssetManifest = { diff --git a/lib/game-hero-assets.ts b/lib/game-hero-assets.ts index 28e879f..874a732 100644 --- a/lib/game-hero-assets.ts +++ b/lib/game-hero-assets.ts @@ -1,16 +1,18 @@ import type { LlmTarget } from "@/lib/quick-llm"; import { callQuickLlm } from "@/lib/quick-llm"; +import { generateTexturedGlb, getMeshyApiKey } from "@/lib/meshy-client"; +import { autoRigToQuaterniusKernel } from "@/lib/auto-rig-humanoid"; import { - generateTexturedGlb, - getMeshyApiKey, - rigHumanoidWalk, -} from "@/lib/meshy-client"; + QUATERNIUS_KERNEL_ID, + QUATERNIUS_ROOT_MOTION_PUBLIC_PATH, + QUATERNIUS_STANDARD_PUBLIC_PATH, +} from "@/lib/quaternius-kernel"; import { type StoredHeroAsset, writeGameAssetFile, writeHeroAssetManifest, } from "@/lib/game-asset-storage"; -import { gameAssetFileUrl } from "@/lib/site-url"; +import { gameAssetFileUrl, getSiteBaseUrl } from "@/lib/site-url"; export type HeroAssetKind = "humanoid" | "vehicle" | "prop"; @@ -58,15 +60,27 @@ export function appendGeneratedAssetsSection( const rows = assets .map((asset) => { - const note = asset.hasWalk - ? "rigged walk clip" - : asset.rigged - ? "rigged" - : "textured sculpt"; + const note = asset.kernel + ? "Quaternius Universal kernel (Idle_Loop, Walk_Loop, Sprint_Loop, …)" + : asset.hasWalk + ? "rigged walk clip" + : asset.rigged + ? "rigged" + : "textured sculpt"; return `| ${asset.id} | \`${asset.filename}\` | ${gameAssetFileUrl(slug, asset.filename)} | ${note} |`; }) .join("\n"); + const kernelHint = assets.some((a) => a.kernel) + ? ` +- Clips are already embedded. Drive them with Three.js \`AnimationMixer\` using these names: \`Idle_Loop\`, \`Walk_Loop\`, \`Jog_Fwd_Loop\`, \`Sprint_Loop\`, \`Jump_Start\` / \`Jump_Loop\` / \`Jump_Land\`, \`Punch_Jab\`, \`Sword_Attack\`, \`Death01\`. +- Do not T-pose, invent keyframes, or replace the hero with boxes. +- In-place locomotion is baked in. For traveling root motion, also load \`${getSiteBaseUrl()}${QUATERNIUS_ROOT_MOTION_PUBLIC_PATH}\` (same bone names). +` + : ` +- Play the walk clip on humanoid heroes while moving. Pause it when idle. +`; + return `${base} ## 10. Generated hero assets @@ -77,7 +91,7 @@ These GLBs were sculpted for this slice. Download them into \`public/models/\` a | --- | --- | --- | --- | ${rows} -- Play the walk clip on humanoid heroes while moving. Pause it when idle. +${kernelHint.trim()} - Keep buildings, roads, and repeating world dressing procedural. `; } @@ -89,12 +103,17 @@ export function appendHeroAssetInstructions( ): string { if (!assets.length) return prompt; const lines = assets.map((asset) => { - const extra = asset.hasWalk - ? " (rigged; play the walk animation while moving)" - : ""; + const extra = asset.kernel + ? " (Quaternius Universal rig; play Idle_Loop / Walk_Loop / Sprint_Loop — do not T-pose)" + : asset.hasWalk + ? " (rigged; play the walk animation while moving)" + : ""; return `- ${asset.filename}${extra}: ${gameAssetFileUrl(slug, asset.filename)}`; }); - const block = `Download these 3D models into public/models/ and load them with GLTFLoader. Do not rebuild these heroes from boxes.\n${lines.join("\n")}`; + const kernelLine = assets.some((a) => a.kernel) + ? `\nClip names: Idle_Loop (stand), Walk_Loop (move), Jog_Fwd_Loop, Sprint_Loop (run), Jump_Start then Jump_Loop then Jump_Land, Punch_Jab / Punch_Cross, Sword_Attack, Death01. Use AnimationMixer. Optional traveling locomotion: ${getSiteBaseUrl()}${QUATERNIUS_ROOT_MOTION_PUBLIC_PATH} (same skeleton). In-place kernel copy: ${getSiteBaseUrl()}${QUATERNIUS_STANDARD_PUBLIC_PATH}.` + : ""; + const block = `Download these 3D models into public/models/ and load them with GLTFLoader. Do not rebuild these heroes from boxes.\n${lines.join("\n")}${kernelLine}`; const stripped = prompt .replace( /\n*Download these 3D models into public\/models\/[\s\S]*?(?=\n\nUse this game spec:|$)/i, @@ -119,7 +138,7 @@ Rules: - 1 or 2 assets max. Prefer the player/army first, then one signature vehicle if the camera sits on it. - kind is humanoid, vehicle, or prop. - id is a short slug like player, car, knight. -- prompt is a Meshy text-to-3D prompt: one object, full body or full vehicle, A-pose if humanoid, no scene, no extra people, readable game-ready sculpt. +- prompt is a Meshy text-to-3D prompt: one object, full body or full vehicle, T-pose if humanoid, no scene, no extra people, readable game-ready sculpt. - Do not request buildings, trees, or a whole city.`, `Game: ${opts.gameName}\n\nGAME.md:\n${opts.specMd.slice(0, 6000)}`, 800 @@ -156,7 +175,6 @@ Rules: } function heuristicPlan(gameName: string, specMd: string): PlannedAsset[] { - const text = specMd.toLowerCase(); const is2d = /\b(canvas 2d|sprite sheet|side scroll|2d platform)/i.test(specMd) && !/three\.js/i.test(specMd); @@ -169,7 +187,7 @@ function heuristicPlan(gameName: string, specMd: string): PlannedAsset[] { { id: "player", kind: "humanoid", - prompt: `A single full-body game character from ${gameName}, standing in A-pose, arms slightly away from the torso, readable silhouette, detailed clothes, no weapons in hand, no background, no other people, game-ready sculpt`, + prompt: `A single full-body game character from ${gameName}, standing in T-pose, arms straight out to the sides, readable silhouette, detailed clothes, no weapons in hand, no background, no other people, game-ready sculpt`, }, ]; } @@ -205,7 +223,7 @@ export async function generateHeroAssets(opts: { const sculpt = await generateTexturedGlb({ apiKey, prompt: item.prompt, - poseMode: item.kind === "humanoid" ? "a-pose" : "", + poseMode: item.kind === "humanoid" ? "t-pose" : "", deadlineAt: opts.deadlineAt, onStatus: opts.onStatus, }); @@ -217,23 +235,20 @@ export async function generateHeroAssets(opts: { let bytes = sculpt.glb; let rigged = false; let hasWalk = false; - if ( - item.kind === "humanoid" && - sculpt.refineTaskId && - (!opts.deadlineAt || opts.deadlineAt - Date.now() > 25_000) - ) { - const rig = await rigHumanoidWalk({ - apiKey, - refineTaskId: sculpt.refineTaskId, - deadlineAt: opts.deadlineAt, - onStatus: opts.onStatus, - }); + let kernel: StoredHeroAsset["kernel"] = null; + let clips: string[] = []; + if (item.kind === "humanoid") { + opts.onStatus?.("Auto-rigging Quaternius kernel"); + const rig = await autoRigToQuaterniusKernel(sculpt.glb); if (rig.ok) { bytes = rig.glb; rigged = true; - hasWalk = true; + hasWalk = rig.clips.includes("Walk_Loop"); + kernel = QUATERNIUS_KERNEL_ID; + clips = rig.clips; + opts.onStatus?.("Quaternius kernel bound (Idle_Loop / Walk_Loop)"); } else { - console.warn(`[game-assets] ${item.id} rig failed: ${rig.error}`); + console.warn(`[game-assets] ${item.id} auto-rig failed: ${rig.error}`); } } @@ -247,6 +262,8 @@ export async function generateHeroAssets(opts: { rigged, hasWalk, prompt: item.prompt, + kernel, + clips, }); } catch (e) { console.warn( diff --git a/lib/game-reverse-system-prompt.ts b/lib/game-reverse-system-prompt.ts index 5bcb61b..d085ba0 100644 --- a/lib/game-reverse-system-prompt.ts +++ b/lib/game-reverse-system-prompt.ts @@ -9,7 +9,7 @@ You are given a **game title**, optional evidence, and a short **GAME.md spec su - **Plain language.** Sounds like a real request ("Build me…", "I want…"), not an architecture doc. - **Outcome focused.** Describe what the game should *feel* like to play, not every system. - **Honest scope.** A browser demo slice, not the full AAA game. One city district, one level, one mechanic loop. -- **Genre appropriate.** Driving games mention feel of the car and camera. Platformers mention jump and level flow. Puzzle games mention the core loop. Third-person or character-led games should say the player (or army, or hero vehicle) should look stylish and readable, not like a placeholder made of boxes. Do not mention Meshy, APIs, or download URLs; those are attached after you write the prompt. +- **Genre appropriate.** Driving games mention feel of the car and camera. Platformers mention jump and level flow. Puzzle games mention the core loop. Third-person or character-led games should say the player (or army, or hero vehicle) should look stylish and readable, not like a placeholder made of boxes, and should actually walk, idle, and run rather than T-pose. Do not mention Meshy, APIs, or download URLs; those are attached after you write the prompt. - **Length:** about **120 to 200 words**, usually one short paragraph or a few tight sentences. Not a bullet list of file paths or package names. - **Tone:** natural and conversational. Use contractions when they fit. No preamble ("Sure, here is…"), no meta ("As an AI…"). NEVER use hyphens or dashes; use commas or shorter sentences instead. diff --git a/lib/game-spec-system-prompt.ts b/lib/game-spec-system-prompt.ts index 842d65c..3c7f592 100644 --- a/lib/game-spec-system-prompt.ts +++ b/lib/game-spec-system-prompt.ts @@ -27,7 +27,7 @@ Given a game title and any available evidence, write a complete GAME.md specific - Architecture must separate simulation core (no renderer imports) from render layer. - Asset tiers are mandatory and based on **camera proximity**, not object type. Procedural world (buildings, roads, terrain, VFX). Distant extras may be primitives. Identity objects the camera inspects every shot (third-person player, signature vehicle, boss, unique NPC, chess army) MUST be a readable hero mesh in v1: one GLB sculpt or a painted texture atlas on smooth capsules. Never a stack of untextured cubes. Never mark that hero as "optional later". First-person games may skip a detailed body. 2D games use sprite sheets, not GLB. - Do **not** invent download URLs or write a "Generated hero assets" section. A later Meshy pipeline attaches real GLB links after you finish GAME.md. -- When listing out of scope, ban "a GLB per building" and "a full animation graph". Do **not** ban the single hero sculpt. Simple idle/walk on one hero is in v1 for third-person games. +- When listing out of scope, ban "a GLB per building" and "a full custom animation graph". Do **not** ban the single hero sculpt. Playing the embedded Quaternius Universal clips (Idle_Loop, Walk_Loop, Sprint_Loop) on one hero is in v1 for third-person games. - Do not billboard a generated 2D photo as a 3D person. Generated images are for textures, HUD, sprites, and atlases. - If evidence is thin (name only), use well known facts about the game and label uncertain items in Evidence Notes. - When external metadata JSON is provided, prefer it over guesses. diff --git a/lib/quaternius-kernel.ts b/lib/quaternius-kernel.ts new file mode 100644 index 0000000..914938c --- /dev/null +++ b/lib/quaternius-kernel.ts @@ -0,0 +1,193 @@ +/** Quaternius Universal Animation Library (Standard) clip names, from UAL1_Standard.glb. */ +export const QUATERNIUS_CLIPS = [ + "A_TPose", + "Crouch_Fwd_Loop", + "Crouch_Idle_Loop", + "Dance_Loop", + "Death01", + "Driving_Loop", + "Fixing_Kneeling", + "Hit_Chest", + "Hit_Head", + "Idle_Loop", + "Idle_Talking_Loop", + "Idle_Torch_Loop", + "Interact", + "Jog_Fwd_Loop", + "Jump_Land", + "Jump_Loop", + "Jump_Start", + "PickUp_Table", + "Pistol_Aim_Down", + "Pistol_Aim_Neutral", + "Pistol_Aim_Up", + "Pistol_Idle_Loop", + "Pistol_Reload", + "Pistol_Shoot", + "Punch_Cross", + "Punch_Jab", + "Push_Loop", + "Roll", + "Sitting_Enter", + "Sitting_Exit", + "Sitting_Idle_Loop", + "Sitting_Talking_Loop", + "Spell_Simple_Enter", + "Spell_Simple_Exit", + "Spell_Simple_Idle_Loop", + "Spell_Simple_Shoot", + "Sprint_Loop", + "Swim_Fwd_Loop", + "Swim_Idle_Loop", + "Sword_Attack", + "Sword_Idle", + "Walk_Formal_Loop", + "Walk_Loop", +] as const; + +export type QuaterniusClip = (typeof QUATERNIUS_CLIPS)[number]; + +export const QUATERNIUS_JOINTS = [ + "root", + "pelvis", + "spine_01", + "spine_02", + "spine_03", + "neck_01", + "Head", + "clavicle_l", + "upperarm_l", + "lowerarm_l", + "hand_l", + "index_01_l", + "index_02_l", + "index_03_l", + "index_04_leaf_l", + "middle_01_l", + "middle_02_l", + "middle_03_l", + "middle_04_leaf_l", + "pinky_01_l", + "pinky_02_l", + "pinky_03_l", + "pinky_04_leaf_l", + "ring_01_l", + "ring_02_l", + "ring_03_l", + "ring_04_leaf_l", + "thumb_01_l", + "thumb_02_l", + "thumb_03_l", + "thumb_04_leaf_l", + "clavicle_r", + "upperarm_r", + "lowerarm_r", + "hand_r", + "index_01_r", + "index_02_r", + "index_03_r", + "index_04_leaf_r", + "middle_01_r", + "middle_02_r", + "middle_03_r", + "middle_04_leaf_r", + "pinky_01_r", + "pinky_02_r", + "pinky_03_r", + "pinky_04_leaf_r", + "ring_01_r", + "ring_02_r", + "ring_03_r", + "ring_04_leaf_r", + "thumb_01_r", + "thumb_02_r", + "thumb_03_r", + "thumb_04_leaf_r", + "thigh_l", + "calf_l", + "foot_l", + "ball_l", + "ball_leaf_l", + "thigh_r", + "calf_r", + "foot_r", + "ball_r", + "ball_leaf_r", +] as const; + +export const QUATERNIUS_KERNEL_ID = "quaternius-ual1"; +export const QUATERNIUS_SKIN_NAME = "Armature"; +export const QUATERNIUS_JOINT_COUNT = 65; +export const QUATERNIUS_CLIP_COUNT = 43; + +/** In-place kernel (no root motion) — default for UI playback. */ +export const QUATERNIUS_STANDARD_PUBLIC_PATH = "/quaternius/UAL1_Standard.glb"; +/** Root-motion kernel — locomotion that travels. */ +export const QUATERNIUS_ROOT_MOTION_PUBLIC_PATH = "/quaternius/UAL1_Standard_RM.glb"; + +export const KERNEL_PREVIEW_CLIPS: QuaterniusClip[] = [ + "Idle_Loop", + "Walk_Loop", + "Jog_Fwd_Loop", + "Sprint_Loop", + "Jump_Start", + "Jump_Loop", + "Jump_Land", + "Crouch_Idle_Loop", + "Crouch_Fwd_Loop", + "Punch_Jab", + "Punch_Cross", + "Sword_Attack", + "Death01", + "Dance_Loop", +]; + +/** Map game verbs to Quaternius clip names. Agents should use these, not invented motion. */ +export const GAME_ACTION_TO_CLIP: Record = { + idle: "Idle_Loop", + walk: "Walk_Loop", + jog: "Jog_Fwd_Loop", + run: "Sprint_Loop", + sprint: "Sprint_Loop", + jump: ["Jump_Start", "Jump_Loop", "Jump_Land"], + crouch: "Crouch_Idle_Loop", + crouchWalk: "Crouch_Fwd_Loop", + punch: ["Punch_Jab", "Punch_Cross"], + sword: "Sword_Attack", + hit: ["Hit_Chest", "Hit_Head"], + death: "Death01", + swim: "Swim_Fwd_Loop", + sit: "Sitting_Idle_Loop", +}; + +/** POSIX join so this module stays browser-safe (no node:path). */ +function joinPosix(...parts: string[]): string { + return parts + .filter(Boolean) + .join("/") + .replace(/\/{2,}/g, "/"); +} + +export function quaterniusKernelDir(cwd = process.cwd()): string { + return joinPosix(cwd, "public", "quaternius"); +} + +export function quaterniusStandardDiskPath(cwd = process.cwd()): string { + return joinPosix(quaterniusKernelDir(cwd), "UAL1_Standard.glb"); +} + +export function quaterniusRootMotionDiskPath(cwd = process.cwd()): string { + return joinPosix(quaterniusKernelDir(cwd), "UAL1_Standard_RM.glb"); +} + +export function isDeformJoint(name: string): boolean { + if (name === "root" || name === "Armature") return false; + if (name.includes("_leaf_")) return false; + return true; +} + +export function resolveGameClip( + action: string +): QuaterniusClip | QuaterniusClip[] | null { + return GAME_ACTION_TO_CLIP[action] ?? null; +} diff --git a/lib/tpose-dummy.ts b/lib/tpose-dummy.ts new file mode 100644 index 0000000..7e5477c --- /dev/null +++ b/lib/tpose-dummy.ts @@ -0,0 +1,90 @@ +import { Document, NodeIO } from "@gltf-transform/core"; +import { ALL_EXTENSIONS } from "@gltf-transform/extensions"; + +type Vec3 = [number, number, number]; + +function addBox( + positions: number[], + indices: number[], + min: Vec3, + max: Vec3 +): void { + const base = positions.length / 3; + const corners: Vec3[] = [ + [min[0], min[1], min[2]], + [max[0], min[1], min[2]], + [max[0], max[1], min[2]], + [min[0], max[1], min[2]], + [min[0], min[1], max[2]], + [max[0], min[1], max[2]], + [max[0], max[1], max[2]], + [min[0], max[1], max[2]], + ]; + for (const c of corners) positions.push(c[0], c[1], c[2]); + const faces = [ + [0, 1, 2, 0, 2, 3], + [4, 6, 5, 4, 7, 6], + [0, 4, 5, 0, 5, 1], + [3, 2, 6, 3, 6, 7], + [0, 3, 7, 0, 7, 4], + [1, 5, 6, 1, 6, 2], + ]; + for (const face of faces) { + for (const i of face) indices.push(base + i); + } +} + +/** Unskinned T-pose stand-in (~1.83m, Y-up, facing +Z) for auto-rig tests. */ +export function buildTPoseDummyDocument(): Document { + const positions: number[] = []; + const indices: number[] = []; + + addBox(positions, indices, [-0.14, 0.86, -0.08], [0.14, 1.08, 0.09]); // hips + addBox(positions, indices, [-0.17, 1.08, -0.09], [0.17, 1.48, 0.11]); // torso + addBox(positions, indices, [-0.11, 1.52, -0.11], [0.11, 1.82, 0.13]); // head + addBox(positions, indices, [0.17, 1.38, -0.05], [0.46, 1.50, 0.05]); // L upper arm + addBox(positions, indices, [0.46, 1.38, -0.05], [0.74, 1.50, 0.05]); // L lower arm + addBox(positions, indices, [0.74, 1.36, -0.06], [0.90, 1.50, 0.04]); // L hand + addBox(positions, indices, [-0.46, 1.38, -0.05], [-0.17, 1.50, 0.05]); // R upper arm + addBox(positions, indices, [-0.74, 1.38, -0.05], [-0.46, 1.50, 0.05]); // R lower arm + addBox(positions, indices, [-0.90, 1.36, -0.06], [-0.74, 1.50, 0.04]); // R hand + addBox(positions, indices, [0.03, 0.50, -0.07], [0.16, 0.90, 0.07]); // L thigh + addBox(positions, indices, [0.04, 0.08, -0.06], [0.15, 0.50, 0.06]); // L calf + addBox(positions, indices, [0.03, 0.00, -0.08], [0.16, 0.08, 0.16]); // L foot + addBox(positions, indices, [-0.16, 0.50, -0.07], [-0.03, 0.90, 0.07]); // R thigh + addBox(positions, indices, [-0.15, 0.08, -0.06], [-0.04, 0.50, 0.06]); // R calf + addBox(positions, indices, [-0.16, 0.00, -0.08], [-0.03, 0.08, 0.16]); // R foot + + const doc = new Document(); + const buffer = doc.createBuffer(); + const pos = doc + .createAccessor("pos", buffer) + .setType("VEC3") + .setArray(new Float32Array(positions)); + const idx = doc + .createAccessor("idx", buffer) + .setType("SCALAR") + .setArray(new Uint32Array(indices)); + const material = doc + .createMaterial("dummy") + .setBaseColorFactor([0.82, 0.45, 0.22, 1]) + .setMetallicFactor(0) + .setRoughnessFactor(0.7); + const prim = doc + .createPrimitive() + .setAttribute("POSITION", pos) + .setIndices(idx) + .setMaterial(material) + .setMode(4); + const mesh = doc.createMesh("TPoseDummy").addPrimitive(prim); + const node = doc.createNode("TPoseDummy").setMesh(mesh); + const scene = doc.createScene("Scene").addChild(node); + doc.getRoot().setDefaultScene(scene); + return doc; +} + +export async function buildTPoseDummyGlb(): Promise { + const io = new NodeIO().registerExtensions(ALL_EXTENSIONS); + const bytes = await io.writeBinary(buildTPoseDummyDocument()); + return Buffer.from(bytes); +} diff --git a/next.config.ts b/next.config.ts index a985bad..11707e0 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,6 +1,9 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { + experimental: { + optimizePackageImports: ["three"], + }, async headers() { return [ { diff --git a/package.json b/package.json index 8777c41..c6931c8 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,13 @@ "start": "next start", "lint": "eslint", "test:titles": "node --env-file=.env.local scripts/test-titles.mjs", + "test:kernel": "tsx --test scripts/test-quaternius-kernel.ts", "db:migrate:title": "node --env-file=.env.local scripts/apply-title-migration.mjs" }, "dependencies": { + "@gltf-transform/core": "^4.4.2", + "@gltf-transform/extensions": "^4.4.2", + "@gltf-transform/functions": "^4.4.2", "@supabase/supabase-js": "^2.101.1", "@vercel/analytics": "^2.0.1", "context.dev": "^2.5.0", @@ -19,6 +23,7 @@ "react-dom": "19.2.4", "react-markdown": "^10.1.0", "stripe": "^17.7.0", + "three": "^0.185.1", "undici": "^8.1.0", "zod": "^4.4.3" }, @@ -27,9 +32,11 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@types/three": "^0.185.4", "eslint": "^9", "eslint-config-next": "16.2.1", "tailwindcss": "^4", + "tsx": "^4.23.12", "typescript": "^5" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 313053d..07a2c12 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,15 @@ importers: .: dependencies: + '@gltf-transform/core': + specifier: ^4.4.2 + version: 4.4.2 + '@gltf-transform/extensions': + specifier: ^4.4.2 + version: 4.4.2 + '@gltf-transform/functions': + specifier: ^4.4.2 + version: 4.4.2(@types/node@20.19.37) '@supabase/supabase-js': specifier: ^2.101.1 version: 2.101.1 @@ -32,6 +41,9 @@ importers: stripe: specifier: ^17.7.0 version: 17.7.0 + three: + specifier: ^0.185.1 + version: 0.185.1 undici: specifier: ^8.1.0 version: 8.1.0 @@ -51,6 +63,9 @@ importers: '@types/react-dom': specifier: ^19 version: 19.2.3(@types/react@19.2.14) + '@types/three': + specifier: ^0.185.4 + version: 0.185.4 eslint: specifier: ^9 version: 9.39.4(jiti@2.6.1) @@ -60,6 +75,9 @@ importers: tailwindcss: specifier: ^4 version: 4.2.2 + tsx: + specifier: ^4.23.12 + version: 4.23.12 typescript: specifier: ^5 version: 5.9.3 @@ -137,15 +155,177 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@dimforge/rapier3d-compat@0.12.0': + resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==} + '@emnapi/core@1.9.1': resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + '@emnapi/runtime@1.9.1': resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} '@emnapi/wasi-threads@1.2.0': resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -184,6 +364,15 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@gltf-transform/core@4.4.2': + resolution: {integrity: sha512-qsWKwNSwK+2s834Mt4xbYcyHqCrgNFP7hIv5s487JxebngRfDgelpghNF+kSswGb2/NuapasfK3UViFoSJJoMg==} + + '@gltf-transform/extensions@4.4.2': + resolution: {integrity: sha512-HJH1FM+edC5eNvl6xO0SOXJ/j/3oDoIpSu150OTdJaLBoM3TgCCGIfh4wyhgWAqZrkvgHKVGiZKxcKV5LkgPCQ==} + + '@gltf-transform/functions@4.4.2': + resolution: {integrity: sha512-dclXgv9TshMaWBqPDUYd4xTwBQ2PpuR8p0Y9pokrRzGQDUPXRP6lTDzbqT0UmEmxFSvRyPJjvOWUmzeiRafpvw==} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -210,133 +399,307 @@ packages: cpu: [arm64] os: [darwin] + '@img/sharp-darwin-arm64@0.35.3': + resolution: {integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] + '@img/sharp-darwin-x64@0.34.5': resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] + '@img/sharp-darwin-x64@0.35.3': + resolution: {integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-freebsd-wasm32@0.35.3': + resolution: {integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==} + engines: {node: '>=20.9.0'} + os: [freebsd] + '@img/sharp-libvips-darwin-arm64@1.2.4': resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} cpu: [arm64] os: [darwin] + '@img/sharp-libvips-darwin-arm64@1.3.2': + resolution: {integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==} + cpu: [arm64] + os: [darwin] + '@img/sharp-libvips-darwin-x64@1.2.4': resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} cpu: [x64] os: [darwin] + '@img/sharp-libvips-darwin-x64@1.3.2': + resolution: {integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==} + cpu: [x64] + os: [darwin] + '@img/sharp-libvips-linux-arm64@1.2.4': resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm64@1.3.2': + resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==} + cpu: [arm64] + os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.3.2': + resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==} + cpu: [arm] + os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.3.2': + resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==} + cpu: [ppc64] + os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.3.2': + resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==} + cpu: [riscv64] + os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.3.2': + resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.3.2': + resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==} + cpu: [x64] + os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-arm64@1.3.2': + resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==} + cpu: [arm64] + os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.3.2': + resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==} + cpu: [x64] + os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm64@0.35.3': + resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.35.3': + resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.35.3': + resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.35.3': + resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.35.3': + resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.35.3': + resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-arm64@0.35.3': + resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.35.3': + resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] + '@img/sharp-wasm32@0.35.3': + resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.3': + resolution: {integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + '@img/sharp-win32-arm64@0.34.5': resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [win32] + '@img/sharp-win32-arm64@0.35.3': + resolution: {integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + '@img/sharp-win32-ia32@0.34.5': resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] + '@img/sharp-win32-ia32@0.35.3': + resolution: {integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + '@img/sharp-win32-x64@0.34.5': resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] + '@img/sharp-win32-x64@0.35.3': + resolution: {integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -379,24 +742,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@next/swc-linux-arm64-musl@16.2.1': resolution: {integrity: sha512-ssKq6iMRnHdnycGp9hCuGnXJZ0YPr4/wNwrfE5DbmvEcgl9+yv97/Kq3TPVDfYome1SW5geciLB9aiEqKXQjlQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@next/swc-linux-x64-gnu@16.2.1': resolution: {integrity: sha512-HQm7SrHRELJ30T1TSmT706IWovFFSRGxfgUkyWJZF/RKBMdbdRWJuFrcpDdE5vy9UXjFOx6L3mRdqH04Mmx0hg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@next/swc-linux-x64-musl@16.2.1': resolution: {integrity: sha512-aV2iUaC/5HGEpbBkE+4B8aHIudoOy5DYekAKOMSHoIYQ66y/wIVeaRx8MS2ZMdxe/HIXlMho4ubdZs/J8441Tg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@next/swc-win32-arm64-msvc@16.2.1': resolution: {integrity: sha512-IXdNgiDHaSk0ZUJ+xp0OQTdTgnpx1RCfRTalhn3cjOP+IddTMINwA7DXZrwTmGDO8SUr5q2hdP/du4DcrB1GxA==} @@ -501,24 +868,28 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.2': resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.2': resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.2': resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.2': resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==} @@ -551,6 +922,9 @@ packages: '@tailwindcss/postcss@4.2.2': resolution: {integrity: sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==} + '@tweenjs/tween.js@23.1.3': + resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -578,6 +952,9 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/ndarray@1.0.14': + resolution: {integrity: sha512-oANmFZMnFQvb219SSBIhI1Ih/r4CvHDOzkWyJS/XRqkMrGH5/kaPSA1hQhdIBzouaE+5KpE/f5ylI9cujmckQg==} + '@types/node@20.19.37': resolution: {integrity: sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==} @@ -589,12 +966,21 @@ packages: '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/stats.js@0.17.4': + resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} + + '@types/three@0.185.4': + resolution: {integrity: sha512-gAsBIC07NIFrxjbf7tH2t71c38uulFfk/RFoC7FNBSjMRAQ8J1x/RBvusX0N5PJouaYFJawXQqfCQ0RKUx/1nA==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/webxr@0.5.24': + resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==} + '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} @@ -699,41 +1085,49 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -957,6 +1351,9 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + cwise-compiler@1.1.3: + resolution: {integrity: sha512-WXlK/m+Di8DMMcCjcWr4i+XzcQra9eCdXIJrgh4TUgh0pIS/yJduLxS9JgefsHJ/YVLdgPtXm9r62W92MvanEQ==} + damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -1064,6 +1461,11 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -1223,6 +1625,9 @@ packages: picomatch: optional: true + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -1246,6 +1651,11 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -1375,6 +1785,9 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + iota-array@1.0.0: + resolution: {integrity: sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA==} + is-alphabetical@2.0.1: resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} @@ -1397,6 +1810,9 @@ packages: resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} engines: {node: '>= 0.4'} + is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + is-bun-module@2.0.0: resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} @@ -1545,6 +1961,9 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + ktx-parse@1.1.0: + resolution: {integrity: sha512-mKp3y+FaYgR7mXWAbyyzpa/r1zDWeaunH+INJO4fou3hb45XuNSwar+7llrRyvpMWafxSIi99RNFJ05MHedaJQ==} + language-subtag-registry@0.3.23: resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} @@ -1591,24 +2010,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -1678,6 +2101,9 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + meshoptimizer@1.1.1: + resolution: {integrity: sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==} + micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -1771,6 +2197,18 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + ndarray-lanczos@0.3.0: + resolution: {integrity: sha512-5kBmmG3Zvyj77qxIAC4QFLKuYdDIBJwCG+DukT6jQHNa1Ft74/hPH1z5mbQXeHBt8yvGPBGVrr3wEOdJPYYZYg==} + + ndarray-ops@1.2.2: + resolution: {integrity: sha512-BppWAFRjMYF7N/r6Ie51q6D4fs0iiGmeXIACKY66fLpnwIui3Wc3CXiD/30mgLbDjPpSLrsqcp3Z62+IcHZsDw==} + + ndarray-pixels@5.2.0: + resolution: {integrity: sha512-lTh4tFKziAatVTa9crIsidUyn+lqujVOQpzfdBWvdFu2wo9Uo6z261lVX7SgMyP89xGmj3TMTPbbxl9YDnV4SA==} + + ndarray@1.0.19: + resolution: {integrity: sha512-B4JHA4vdyZU30ELBw3g7/p9bZupyew5a7tX1Y/gGeF2hafrPaQZhgrGQfsvgfYbgdFZjYwuEcnaobeM/WMW+HQ==} + next@16.2.1: resolution: {integrity: sha512-VaChzNL7o9rbfdt60HUj8tev4m6d7iC1igAy157526+cJlXOQu5LzsBXNT+xaJnTP/k+utSX5vMv7m0G+zKH+Q==} engines: {node: '>=20.9.0'} @@ -1895,6 +2333,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + property-graph@4.1.0: + resolution: {integrity: sha512-AvPcP7XECNWy4LGmFQ77k7un4lSKM4eS29PTvW4ck95uYeLxXPWJM7hLuBqK91FaHqCcgJvIUCuNJjjxKE7VKQ==} + property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} @@ -1989,6 +2430,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -2005,6 +2451,15 @@ packages: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + sharp@0.35.3: + resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2115,6 +2570,9 @@ packages: resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} engines: {node: '>=6'} + three@0.185.1: + resolution: {integrity: sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -2141,6 +2599,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.23.12: + resolution: {integrity: sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==} + engines: {node: '>=18.0.0'} + hasBin: true + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -2187,6 +2650,9 @@ packages: unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + uniq@1.0.1: + resolution: {integrity: sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA==} + unist-util-is@6.0.1: resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} @@ -2380,12 +2846,19 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@dimforge/rapier3d-compat@0.12.0': {} + '@emnapi/core@1.9.1': dependencies: '@emnapi/wasi-threads': 1.2.0 tslib: 2.8.1 optional: true + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.9.1': dependencies: tslib: 2.8.1 @@ -2396,6 +2869,84 @@ snapshots: tslib: 2.8.1 optional: true + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))': dependencies: eslint: 9.39.4(jiti@2.6.1) @@ -2442,6 +2993,26 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@gltf-transform/core@4.4.2': + dependencies: + property-graph: 4.1.0 + + '@gltf-transform/extensions@4.4.2': + dependencies: + '@gltf-transform/core': 4.4.2 + ktx-parse: 1.1.0 + + '@gltf-transform/functions@4.4.2(@types/node@20.19.37)': + dependencies: + '@gltf-transform/core': 4.4.2 + '@gltf-transform/extensions': 4.4.2 + ktx-parse: 1.1.0 + ndarray: 1.0.19 + ndarray-lanczos: 0.3.0 + ndarray-pixels: 5.2.0(@types/node@20.19.37) + transitivePeerDependencies: + - '@types/node' + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -2453,103 +3024,206 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@img/colour@1.1.0': - optional: true + '@img/colour@1.1.0': {} '@img/sharp-darwin-arm64@0.34.5': optionalDependencies: '@img/sharp-libvips-darwin-arm64': 1.2.4 optional: true + '@img/sharp-darwin-arm64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.2 + optional: true + '@img/sharp-darwin-x64@0.34.5': optionalDependencies: '@img/sharp-libvips-darwin-x64': 1.2.4 optional: true + '@img/sharp-darwin-x64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.2 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.3': + dependencies: + '@img/sharp-wasm32': 0.35.3 + optional: true + '@img/sharp-libvips-darwin-arm64@1.2.4': optional: true + '@img/sharp-libvips-darwin-arm64@1.3.2': + optional: true + '@img/sharp-libvips-darwin-x64@1.2.4': optional: true + '@img/sharp-libvips-darwin-x64@1.3.2': + optional: true + '@img/sharp-libvips-linux-arm64@1.2.4': optional: true + '@img/sharp-libvips-linux-arm64@1.3.2': + optional: true + '@img/sharp-libvips-linux-arm@1.2.4': optional: true + '@img/sharp-libvips-linux-arm@1.3.2': + optional: true + '@img/sharp-libvips-linux-ppc64@1.2.4': optional: true + '@img/sharp-libvips-linux-ppc64@1.3.2': + optional: true + '@img/sharp-libvips-linux-riscv64@1.2.4': optional: true + '@img/sharp-libvips-linux-riscv64@1.3.2': + optional: true + '@img/sharp-libvips-linux-s390x@1.2.4': optional: true + '@img/sharp-libvips-linux-s390x@1.3.2': + optional: true + '@img/sharp-libvips-linux-x64@1.2.4': optional: true + '@img/sharp-libvips-linux-x64@1.3.2': + optional: true + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': optional: true + '@img/sharp-libvips-linuxmusl-arm64@1.3.2': + optional: true + '@img/sharp-libvips-linuxmusl-x64@1.2.4': optional: true + '@img/sharp-libvips-linuxmusl-x64@1.3.2': + optional: true + '@img/sharp-linux-arm64@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-arm64': 1.2.4 optional: true + '@img/sharp-linux-arm64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.2 + optional: true + '@img/sharp-linux-arm@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-arm': 1.2.4 optional: true + '@img/sharp-linux-arm@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.2 + optional: true + '@img/sharp-linux-ppc64@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-ppc64': 1.2.4 optional: true + '@img/sharp-linux-ppc64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.2 + optional: true + '@img/sharp-linux-riscv64@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-riscv64': 1.2.4 optional: true + '@img/sharp-linux-riscv64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.2 + optional: true + '@img/sharp-linux-s390x@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-s390x': 1.2.4 optional: true + '@img/sharp-linux-s390x@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.2 + optional: true + '@img/sharp-linux-x64@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-x64': 1.2.4 optional: true + '@img/sharp-linux-x64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.2 + optional: true + '@img/sharp-linuxmusl-arm64@0.34.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 optional: true + '@img/sharp-linuxmusl-arm64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 + optional: true + '@img/sharp-linuxmusl-x64@0.34.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-x64': 1.2.4 optional: true + '@img/sharp-linuxmusl-x64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.2 + optional: true + '@img/sharp-wasm32@0.34.5': dependencies: '@emnapi/runtime': 1.9.1 optional: true + '@img/sharp-wasm32@0.35.3': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.3': + dependencies: + '@img/sharp-wasm32': 0.35.3 + optional: true + '@img/sharp-win32-arm64@0.34.5': optional: true + '@img/sharp-win32-arm64@0.35.3': + optional: true + '@img/sharp-win32-ia32@0.34.5': optional: true + '@img/sharp-win32-ia32@0.35.3': + optional: true + '@img/sharp-win32-x64@0.34.5': optional: true + '@img/sharp-win32-x64@0.35.3': + optional: true + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2738,6 +3412,8 @@ snapshots: postcss: 8.5.8 tailwindcss: 4.2.2 + '@tweenjs/tween.js@23.1.3': {} + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -2767,6 +3443,8 @@ snapshots: '@types/ms@2.1.0': {} + '@types/ndarray@1.0.14': {} + '@types/node@20.19.37': dependencies: undici-types: 6.21.0 @@ -2779,10 +3457,23 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/stats.js@0.17.4': {} + + '@types/three@0.185.4': + dependencies: + '@dimforge/rapier3d-compat': 0.12.0 + '@tweenjs/tween.js': 23.1.3 + '@types/stats.js': 0.17.4 + '@types/webxr': 0.5.24 + fflate: 0.8.3 + meshoptimizer: 1.1.1 + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} + '@types/webxr@0.5.24': {} + '@types/ws@8.18.1': dependencies: '@types/node': 20.19.37 @@ -3133,6 +3824,10 @@ snapshots: csstype@3.2.3: {} + cwise-compiler@1.1.3: + dependencies: + uniq: 1.0.1 + damerau-levenshtein@1.0.8: {} data-view-buffer@1.0.2: @@ -3308,6 +4003,35 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + escalade@3.2.0: {} escape-string-regexp@4.0.0: {} @@ -3543,6 +4267,8 @@ snapshots: optionalDependencies: picomatch: 4.0.4 + fflate@0.8.3: {} + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -3567,6 +4293,9 @@ snapshots: dependencies: is-callable: 1.2.7 + fsevents@2.3.3: + optional: true + function-bind@1.1.2: {} function.prototype.name@1.1.8: @@ -3708,6 +4437,8 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + iota-array@1.0.0: {} + is-alphabetical@2.0.1: {} is-alphanumerical@2.0.1: @@ -3738,6 +4469,8 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-buffer@1.1.6: {} + is-bun-module@2.0.0: dependencies: semver: 7.7.4 @@ -3879,6 +4612,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + ktx-parse@1.1.0: {} + language-subtag-registry@0.3.23: {} language-tags@1.0.9: @@ -4052,6 +4787,8 @@ snapshots: merge2@1.4.1: {} + meshoptimizer@1.1.1: {} + micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.3.0 @@ -4208,6 +4945,29 @@ snapshots: natural-compare@1.4.0: {} + ndarray-lanczos@0.3.0: + dependencies: + '@types/ndarray': 1.0.14 + ndarray: 1.0.19 + + ndarray-ops@1.2.2: + dependencies: + cwise-compiler: 1.1.3 + + ndarray-pixels@5.2.0(@types/node@20.19.37): + dependencies: + '@types/ndarray': 1.0.14 + ndarray: 1.0.19 + ndarray-ops: 1.2.2 + sharp: 0.35.3(@types/node@20.19.37) + transitivePeerDependencies: + - '@types/node' + + ndarray@1.0.19: + dependencies: + iota-array: 1.0.0 + is-buffer: 1.1.6 + next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@next/env': 16.2.1 @@ -4355,6 +5115,8 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + property-graph@4.1.0: {} + property-information@7.1.0: {} punycode@2.3.1: {} @@ -4479,6 +5241,8 @@ snapshots: semver@7.7.4: {} + semver@7.8.5: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -4533,6 +5297,39 @@ snapshots: '@img/sharp-win32-x64': 0.34.5 optional: true + sharp@0.35.3(@types/node@20.19.37): + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.3 + '@img/sharp-darwin-x64': 0.35.3 + '@img/sharp-freebsd-wasm32': 0.35.3 + '@img/sharp-libvips-darwin-arm64': 1.3.2 + '@img/sharp-libvips-darwin-x64': 1.3.2 + '@img/sharp-libvips-linux-arm': 1.3.2 + '@img/sharp-libvips-linux-arm64': 1.3.2 + '@img/sharp-libvips-linux-ppc64': 1.3.2 + '@img/sharp-libvips-linux-riscv64': 1.3.2 + '@img/sharp-libvips-linux-s390x': 1.3.2 + '@img/sharp-libvips-linux-x64': 1.3.2 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 + '@img/sharp-libvips-linuxmusl-x64': 1.3.2 + '@img/sharp-linux-arm': 0.35.3 + '@img/sharp-linux-arm64': 0.35.3 + '@img/sharp-linux-ppc64': 0.35.3 + '@img/sharp-linux-riscv64': 0.35.3 + '@img/sharp-linux-s390x': 0.35.3 + '@img/sharp-linux-x64': 0.35.3 + '@img/sharp-linuxmusl-arm64': 0.35.3 + '@img/sharp-linuxmusl-x64': 0.35.3 + '@img/sharp-webcontainers-wasm32': 0.35.3 + '@img/sharp-win32-arm64': 0.35.3 + '@img/sharp-win32-ia32': 0.35.3 + '@img/sharp-win32-x64': 0.35.3 + '@types/node': 20.19.37 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -4667,6 +5464,8 @@ snapshots: tapable@2.3.2: {} + three@0.185.1: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.4) @@ -4693,6 +5492,12 @@ snapshots: tslib@2.8.1: {} + tsx@4.23.12: + dependencies: + esbuild: 0.28.2 + optionalDependencies: + fsevents: 2.3.3 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -4764,6 +5569,8 @@ snapshots: trough: 2.2.0 vfile: 6.0.3 + uniq@1.0.1: {} + unist-util-is@6.0.1: dependencies: '@types/unist': 3.0.3 diff --git a/public/quaternius/License.txt b/public/quaternius/License.txt new file mode 100644 index 0000000..82f86ca --- /dev/null +++ b/public/quaternius/License.txt @@ -0,0 +1,12 @@ +------------------------------------------------------- +License: +CC0 1.0 Universal (CC0 1.0) +Public Domain Dedication +https://creativecommons.org/publicdomain/zero/1.0/ + +------------------------------------------------------ +Models by @Quaternius +Consider supporting me on Patreon! + +https://www.patreon.com/quaternius + diff --git a/public/quaternius/README.txt b/public/quaternius/README.txt new file mode 100644 index 0000000..2442162 --- /dev/null +++ b/public/quaternius/README.txt @@ -0,0 +1,19 @@ +The Universal Animation Library comes in two files: the one ending in _RM has root motion baked into every animation, while the other has root motion disabled. + +Explore all the animations in the Animation Viewer! +https://quaternius.com/animviewer.html +------------------------------------------------------- +License: +CC0 1.0 Universal (CC0 1.0) +Public Domain Dedication +https://creativecommons.org/publicdomain/zero/1.0/ + +------------------------------------------------------ +Models by @Quaternius +Consider supporting me on Patreon! + +https://www.patreon.com/quaternius + +------------------------------------------------------- +Join the Discord Server: +https://discord.gg/vJqnRUYRfT diff --git a/public/quaternius/UAL1_Standard.glb b/public/quaternius/UAL1_Standard.glb new file mode 100644 index 0000000..473e590 Binary files /dev/null and b/public/quaternius/UAL1_Standard.glb differ diff --git a/public/quaternius/UAL1_Standard_RM.glb b/public/quaternius/UAL1_Standard_RM.glb new file mode 100644 index 0000000..49ab49b Binary files /dev/null and b/public/quaternius/UAL1_Standard_RM.glb differ diff --git a/scripts/test-quaternius-kernel.ts b/scripts/test-quaternius-kernel.ts new file mode 100644 index 0000000..a1fb81c --- /dev/null +++ b/scripts/test-quaternius-kernel.ts @@ -0,0 +1,113 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { NodeIO } from "@gltf-transform/core"; +import { ALL_EXTENSIONS } from "@gltf-transform/extensions"; +import { autoRigToQuaterniusKernel, fitAabbToUniversalHeight } from "../lib/auto-rig-humanoid"; +import { buildTPoseDummyGlb } from "../lib/tpose-dummy"; +import { + GAME_ACTION_TO_CLIP, + QUATERNIUS_CLIP_COUNT, + QUATERNIUS_CLIPS, + QUATERNIUS_JOINT_COUNT, + QUATERNIUS_JOINTS, + isDeformJoint, + quaterniusRootMotionDiskPath, + quaterniusStandardDiskPath, + resolveGameClip, +} from "../lib/quaternius-kernel"; + +test("kernel clip and joint tables match the vendored GLB contract", () => { + assert.equal(QUATERNIUS_CLIPS.length, QUATERNIUS_CLIP_COUNT); + assert.equal(QUATERNIUS_JOINTS.length, QUATERNIUS_JOINT_COUNT); + assert.equal(resolveGameClip("walk"), "Walk_Loop"); + assert.deepEqual(resolveGameClip("jump"), [ + "Jump_Start", + "Jump_Loop", + "Jump_Land", + ]); + assert.equal(GAME_ACTION_TO_CLIP.idle, "Idle_Loop"); + assert.equal(isDeformJoint("root"), false); + assert.equal(isDeformJoint("index_04_leaf_l"), false); + assert.equal(isDeformJoint("upperarm_l"), true); +}); + +test("vendored UAL1_Standard.glb is the Universal kernel", async () => { + const io = new NodeIO().registerExtensions(ALL_EXTENSIONS); + const doc = await io.read(quaterniusStandardDiskPath()); + const skin = doc.getRoot().listSkins()[0]; + assert.ok(skin, "missing Armature skin"); + assert.equal(skin.getName(), "Armature"); + const joints = skin.listJoints().map((j) => j.getName()); + assert.equal(joints.length, QUATERNIUS_JOINT_COUNT); + assert.deepEqual(joints, [...QUATERNIUS_JOINTS]); + const clips = doc.getRoot().listAnimations().map((a) => a.getName()); + assert.equal(clips.length, QUATERNIUS_CLIP_COUNT); + for (const name of QUATERNIUS_CLIPS) { + assert.ok(clips.includes(name), `missing clip ${name}`); + } +}); + +test("vendored UAL1_Standard_RM.glb is the same Universal skeleton with root motion", async () => { + const io = new NodeIO().registerExtensions(ALL_EXTENSIONS); + const doc = await io.read(quaterniusRootMotionDiskPath()); + const skin = doc.getRoot().listSkins()[0]; + assert.ok(skin, "missing Armature skin"); + assert.equal(skin.getName(), "Armature"); + assert.equal(skin.listJoints().length, QUATERNIUS_JOINT_COUNT); + const clips = doc.getRoot().listAnimations().map((a) => a.getName()); + assert.equal(clips.length, QUATERNIUS_CLIP_COUNT); + for (const name of QUATERNIUS_CLIPS) { + assert.ok(clips.includes(name), `RM missing clip ${name}`); + } +}); + +test("fitAabbToUniversalHeight scales and grounds a tall Z-up mesh", () => { + const fit = fitAabbToUniversalHeight({ + min: [-50, -20, 0], + max: [50, 20, 180], + }); + assert.equal(fit.upAxis, "z"); + assert.ok(fit.scale > 0); + assert.ok(Math.abs(180 * fit.scale - 1.829) < 0.02); +}); + +test("auto-rig binds a T-pose dummy onto the kernel and keeps clips", async () => { + const dummy = await buildTPoseDummyGlb(); + const result = await autoRigToQuaterniusKernel(dummy); + assert.equal(result.ok, true, result.ok ? "" : result.error); + if (!result.ok) return; + + assert.equal(result.jointCount, QUATERNIUS_JOINT_COUNT); + assert.ok(result.vertexCount > 50); + assert.ok(result.clips.includes("Idle_Loop")); + assert.ok(result.clips.includes("Walk_Loop")); + assert.ok(result.clips.includes("Sprint_Loop")); + assert.equal(result.clips.length, QUATERNIUS_CLIP_COUNT); + + const io = new NodeIO().registerExtensions(ALL_EXTENSIONS); + const doc = await io.readBinary(new Uint8Array(result.glb)); + const skin = doc.getRoot().listSkins()[0]; + assert.equal(skin.listJoints().length, QUATERNIUS_JOINT_COUNT); + const mesh = doc.getRoot().listMeshes().find((m) => m.getName() === "Hero"); + assert.ok(mesh, "hero mesh missing"); + const prim = mesh.listPrimitives()[0]; + assert.ok(prim.getAttribute("JOINTS_0"), "missing JOINTS_0"); + assert.ok(prim.getAttribute("WEIGHTS_0"), "missing WEIGHTS_0"); + + const weights = prim.getAttribute("WEIGHTS_0"); + const joints = prim.getAttribute("JOINTS_0"); + assert.ok(weights && joints); + let badWeights = 0; + let badJoints = 0; + const w = [0, 0, 0, 0]; + const ji = [0, 0, 0, 0]; + for (let i = 0; i < weights.getCount(); i++) { + weights.getElement(i, w); + joints.getElement(i, ji); + const sum = w[0] + w[1] + w[2] + w[3]; + if (Math.abs(sum - 1) > 0.05) badWeights += 1; + if (ji.some((idx) => idx < 0 || idx >= QUATERNIUS_JOINT_COUNT)) badJoints += 1; + } + assert.equal(badWeights, 0, `vertices with weights not summing to 1: ${badWeights}`); + assert.equal(badJoints, 0, `vertices with out-of-range joints: ${badJoints}`); +});