Перейти к основному содержимому

Copy-paste starting points for Threlte 8 in v10r. Threlte only — no vanilla Three.js. For the architecture and project decisions, read docs/blueprint/3d/3d-integration.md; for Three.js / Threlte fundamentals, use the 3d skill.

Dependencies

// package.json — then rebuild/restart the container
"@threlte/core": "^8.3.1",
"@threlte/extras": "^9.0.0",
"three": "^0.183.2"

Page Setup

Disable SSR and prerender once at the 3d/ layout — WebGL needs window, and the Three.js bundle is browser-only:

// showcases/3d/+layout.ts
export const ssr = false;
export const prerender = false;

A leaf that breaks out of the layout ([model]/+page@.svelte) repeats these flags in its own +page.ts.

Canvas + Scene

Page hosts <Canvas> and wraps it in <svelte:boundary> for the WebGL fallback. Scene contents live in a child component for code-splitting.

<!-- +page.svelte -->
<script lang="ts">
import { Canvas } from '@threlte/core';
import { BoundaryFallback } from '$lib/components/composites';
import Scene from './Scene.svelte';
</script>

<svelte:boundary>
  <div class="container">
    <Canvas>
      <Scene />
    </Canvas>
  </div>

  {#snippet failed(error, reset)}
    <BoundaryFallback
      title="3D scene unavailable"
      description="WebGL is required. Check browser support or graphics drivers."
      minHeight="100vh"
      {reset}
    />
  {/snippet}
</svelte:boundary>

<style>
  .container { width: 100%; height: 100vh; }
</style>

Scene Contents

Scene is <T> proxy components — camera, lights, meshes. No new THREE.Scene(), no renderer, no render loop; Threlte owns the renderer lifecycle.

<!-- Scene.svelte -->
<script lang="ts">
import { T } from '@threlte/core';
import { OrbitControls } from '@threlte/extras';
</script>

<T.PerspectiveCamera makeDefault position={[3, 3, 3]}>
  <OrbitControls />
</T.PerspectiveCamera>
<T.DirectionalLight position={[10, 10, 10]} intensity={1} />
<T.AmbientLight intensity={0.5} />

<T.Mesh>
  <T.BoxGeometry />
  <T.MeshStandardMaterial color="hotpink" />
</T.Mesh>

Animation: useTask

Per-frame work uses useTask from @threlte/core (Threlte 8 — not useFrame, that's Threlte 7). Drive reactive props with $state:

<script lang="ts">
import { T, useTask } from '@threlte/core';

let rotation = $state(0);
useTask((delta) => {
  rotation += delta;
});
</script>

<T.Mesh rotation.y={rotation}>
  <T.BoxGeometry />
  <T.MeshStandardMaterial color="hotpink" />
</T.Mesh>

Reactive Props

Props from $props() flow straight into <T> attributes — no manual needsUpdate. Use $effect only for imperative objects Threlte doesn't proxy (e.g. an AnimationMixer):

<script lang="ts">
let { color = '#ff0000', wireframe = false } = $props();
</script>

<T.Mesh>
  <T.BoxGeometry />
  <T.MeshStandardMaterial {color} {wireframe} />
</T.Mesh>

GLTF Loading

Load with useGltf (returns a store — read $gltf) or the <GLTF> component. Guard the render until it resolves:

<script lang="ts">
import { T, useTask } from '@threlte/core';
import { useGltf } from '@threlte/extras';
import { AnimationMixer } from 'three';

let { defaultAnimation }: { defaultAnimation?: string } = $props();

// svelte-ignore state_referenced_locally
const gltf = useGltf('/models/Fox.glb');
let mixer: AnimationMixer | undefined;

$effect(() => {
  const data = $gltf;
  if (!data || !defaultAnimation) return;
  mixer = new AnimationMixer(data.scene);
  const clip = data.animations.find((c) => c.name === defaultAnimation);
  if (clip) mixer.clipAction(clip).play();
});

useTask((delta) => mixer?.update(delta));
</script>

{#if $gltf}
  <T is={$gltf.scene} />
{/if}

GLB models ship from static/models/ and serve at /models/*.glb. The <GLTF url="..." /> component is the no-animation shortcut.

Route Structure

3D showcases live under the public locale group (routes are /showcases/3d/..., plural):

src/routes/[[locale=locale]]/(public)/showcases/3d/
├── +layout.ts              # ssr=false, prerender=false (whole subtree)
├── +page.svelte            # scene gallery
├── static-scene/           # GLTF helmet, OrbitControls
├── animated-scene/
│   ├── +page.svelte        # Canvas + boundary
│   └── Scene.svelte        # split for code-splitting
├── customize/[model]/      # GLTF customizer
└── [model]/                # config-driven viewer

Gotchas

  • useTask, not useFrame — Threlte 8 renamed the per-frame hook.
  • useGltf is a store — read $gltf, never render scene contents before it resolves.
  • makeDefault type errorT.PerspectiveCamera makeDefault needs @ts-ignore until tsgo supports the conditional type.
  • state_referenced_locally — calling useGltf(prop) at script top reads a prop during init; silence with // svelte-ignore state_referenced_locally.
  • Keep 3D imports per route — don't pull Three.js into a shared +layout; that defeats code-splitting.

References

  • docs/blueprint/3d/3d-integration.md — architecture, components, config registry
  • 3d skill — Three.js + Threlte fundamentals, physics, WebGPU
  • /showcases/3d — live scenes (also the test spec)
  • Threlte: https://threlte.xyz/docs
← Back to Blueprint

Думаете, этот паттерн можно сделать лучше? Расскажите как.

Оставить отзыв