threlte logo
@threlte/extras

<Bounds>

webgl <Bounds> works with WebGL and is available from the @threlte/extras export. webgpu <Bounds> works with WebGPU and is available from the @threlte/extras/webgpu export.

<Bounds> is a component that will automatically center the camera to its children.

<script lang="ts">
  import { Canvas } from '@threlte/core'
  import Scene from './Scene.svelte'
  import { Pane, Checkbox, List, Button, Wheel } from 'svelte-tweakpane-ui'
  import { Suspense } from '@threlte/extras'

  let camera = $state<'perspective' | 'orthographic'>('perspective')
  let controls = $state<'orbit' | 'camera' | 'trackball' | 'none'>('orbit')
  let animate = $state(true)
  let margin = $state(1.5)
  let enabled = $state(true)
  let version = $state(0)
</script>

<Pane
  title=""
  position="fixed"
>
  <List
    label="camera"
    bind:value={camera}
    options={{
      OrthographicCamera: 'orthographic',
      PerspectiveCamera: 'perspective'
    }}
  />
  <List
    label="controls"
    bind:value={controls}
    options={{
      OrbitControls: 'orbit',
      CameraControls: 'camera',
      TrackballControls: 'trackball',
      None: 'none'
    }}
  />
  <Wheel
    bind:value={margin}
    label="margin"
    step={0.1}
  />
  <Checkbox
    bind:value={animate}
    label="animate"
  />
  <Checkbox
    bind:value={enabled}
    label="enabled"
  />
  <Button
    title="Reset scene"
    on:click={() => (version += 1)}
  />
</Pane>

<div>
  {#key version}
    <Canvas>
      <Suspense>
        <Scene
          {camera}
          {controls}
          {margin}
          {animate}
          {enabled}
        />
      </Suspense>
    </Canvas>
  {/key}
</div>

<style>
  div {
    height: 100%;
  }
</style>
<script lang="ts">
  import { isInstanceOf, T, useTask } from '@threlte/core'
  import {
    Bounds,
    CameraControls,
    OrbitControls,
    Sparkles,
    TrackballControls,
    useGltf,
    useSuspense,
    useTexture
  } from '@threlte/extras'
  import {
    Color,
    DoubleSide,
    MeshBasicMaterial,
    MeshPhongMaterial,
    ShaderMaterial,
    Uniform
  } from 'three'
  import vertexShader from './vertex'
  import fragmentShader from './fragment'

  interface Props {
    camera: 'orthographic' | 'perspective'
    controls: 'camera' | 'orbit' | 'trackball' | 'none'
    margin: number
    animate: boolean
    enabled: boolean
  }

  let { camera, controls, margin, animate, enabled }: Props = $props()

  const suspend = useSuspense()
  const gltf = suspend(useGltf('/models/portal/portal.glb'))
  const texture = suspend(
    useTexture('/models/portal/portal_baked.jpg', {
      transform(result) {
        result.flipY = false
        return result
      }
    })
  )

  const poleLightMaterial = new MeshBasicMaterial({ color: 0xff_ff_e5 })
  const bakedMaterial = new MeshPhongMaterial()

  const portalLightMaterial = new ShaderMaterial({
    uniforms: {
      uTime: new Uniform(0),
      uColorStart: new Uniform(new Color('#1E88E5')),
      uColorEnd: new Uniform(new Color('#5E35B1'))
    },
    side: DoubleSide,
    vertexShader,
    fragmentShader
  })

  $effect(() => {
    if ($texture) {
      bakedMaterial.map = $texture
    }
  })

  useTask((dt) => {
    portalLightMaterial.uniforms.uTime.value += dt
  })
</script>

{#snippet Controls()}
  {#if controls === 'orbit'}
    <OrbitControls
      enableDamping
      enableZoom={false}
      enablePan={false}
    />
  {:else if controls === 'camera'}
    <CameraControls />
  {:else if controls === 'trackball'}
    <TrackballControls />
  {/if}
{/snippet}

{#if camera === 'perspective'}
  <T.PerspectiveCamera
    makeDefault
    position.x={20}
    position.y={10}
    position.z={-20}
    fov={50}
  >
    {@render Controls()}
  </T.PerspectiveCamera>
{:else if camera === 'orthographic'}
  <T.OrthographicCamera
    makeDefault
    position.x={20}
    position.y={10}
    position.z={-20}
    zoom={50}
  >
    {@render Controls()}
  </T.OrthographicCamera>
{/if}

<T.DirectionalLight
  position.y={10}
  position.z={10}
/>

<T.AmbientLight />

{#if $gltf}
  <Bounds
    {margin}
    {animate}
    {enabled}
  >
    <T
      is={$gltf.scene}
      oncreate={(ref) => {
        ref.traverse((child) => {
          if (!isInstanceOf(child, 'Mesh')) {
            return
          }

          if (child.name === 'Portal') {
            child.material = portalLightMaterial
          } else if (child.name === 'LampLight1' || child.name === 'LampLight2') {
            child.material = poleLightMaterial
          } else {
            child.material = bakedMaterial
          }
        })
      }}
    >
      <Sparkles
        position={[0, 0.8, 0]}
        size={4}
        scale={[4, 1.5, 4]}
      />
    </T>
  </Bounds>
{/if}
export default `
//  Classic Perlin 3D Noise 
//  by Stefan Gustavson
//
vec4 permute(vec4 x) {
  return mod(((x*34.0)+1.0)*x, 289.0);
}

vec4 taylorInvSqrt(vec4 r) {
  return 1.79284291400159 - 0.85373472095314 * r;
}

vec3 fade(vec3 t) {
  return t*t*t*(t*(t*6.0-15.0)+10.0);
}

float cnoise(vec3 P) {
  vec3 Pi0 = floor(P); // Integer part for indexing
  vec3 Pi1 = Pi0 + vec3(1.0); // Integer part + 1
  Pi0 = mod(Pi0, 289.0);
  Pi1 = mod(Pi1, 289.0);
  vec3 Pf0 = fract(P); // Fractional part for interpolation
  vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0
  vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
  vec4 iy = vec4(Pi0.yy, Pi1.yy);
  vec4 iz0 = Pi0.zzzz;
  vec4 iz1 = Pi1.zzzz;

  vec4 ixy = permute(permute(ix) + iy);
  vec4 ixy0 = permute(ixy + iz0);
  vec4 ixy1 = permute(ixy + iz1);

  vec4 gx0 = ixy0 / 7.0;
  vec4 gy0 = fract(floor(gx0) / 7.0) - 0.5;
  gx0 = fract(gx0);
  vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0);
  vec4 sz0 = step(gz0, vec4(0.0));
  gx0 -= sz0 * (step(0.0, gx0) - 0.5);
  gy0 -= sz0 * (step(0.0, gy0) - 0.5);

  vec4 gx1 = ixy1 / 7.0;
  vec4 gy1 = fract(floor(gx1) / 7.0) - 0.5;
  gx1 = fract(gx1);
  vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1);
  vec4 sz1 = step(gz1, vec4(0.0));
  gx1 -= sz1 * (step(0.0, gx1) - 0.5);
  gy1 -= sz1 * (step(0.0, gy1) - 0.5);

  vec3 g000 = vec3(gx0.x,gy0.x,gz0.x);
  vec3 g100 = vec3(gx0.y,gy0.y,gz0.y);
  vec3 g010 = vec3(gx0.z,gy0.z,gz0.z);
  vec3 g110 = vec3(gx0.w,gy0.w,gz0.w);
  vec3 g001 = vec3(gx1.x,gy1.x,gz1.x);
  vec3 g101 = vec3(gx1.y,gy1.y,gz1.y);
  vec3 g011 = vec3(gx1.z,gy1.z,gz1.z);
  vec3 g111 = vec3(gx1.w,gy1.w,gz1.w);

  vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110)));
  g000 *= norm0.x;
  g010 *= norm0.y;
  g100 *= norm0.z;
  g110 *= norm0.w;
  vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111)));
  g001 *= norm1.x;
  g011 *= norm1.y;
  g101 *= norm1.z;
  g111 *= norm1.w;

  float n000 = dot(g000, Pf0);
  float n100 = dot(g100, vec3(Pf1.x, Pf0.yz));
  float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z));
  float n110 = dot(g110, vec3(Pf1.xy, Pf0.z));
  float n001 = dot(g001, vec3(Pf0.xy, Pf1.z));
  float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z));
  float n011 = dot(g011, vec3(Pf0.x, Pf1.yz));
  float n111 = dot(g111, Pf1);

  vec3 fade_xyz = fade(Pf0);
  vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z);
  vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y);
  float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x); 

  return 2.2 * n_xyz;
}

varying vec2 vUv;

uniform float uTime;
uniform vec3 uColorStart;
uniform vec3 uColorEnd;

void main () {
  // Displace the UV
  vec2 displacedUv = vUv + cnoise(vec3(vUv * 5.0, uTime * 0.1));

  // Perlin noise
  float strength = cnoise(vec3(displacedUv * 5.0, uTime * 0.2));

  // Outer glow
  float outerGlow = distance(vUv, vec2(0.5)) * 6.0 - 1.8;

  strength += outerGlow;
  strength += step(-0.2, strength) * 0.8;

  vec3 color = mix(uColorStart, uColorEnd, strength);

  gl_FragColor = vec4(color, 1.0);
}`
export default `
varying vec2 vUv;

void main () {
  vUv = uv;

  vec4 modelPosition = modelMatrix * vec4(position, 1.0);
  vec4 viewPosition = viewMatrix * modelPosition;
  vec4 projectionPosition = projectionMatrix * viewPosition;
  gl_Position = projectionPosition;
}`

Component Signature

<Bounds> extends < T . Group > and supports all its props, snippets, bindings and events.

Props

name
type
required
default
description

animate
boolean
no
true
Animates the camera to the fitted position instead of jumping to it.

enabled
boolean
no
true
Fits the camera to the children automatically, and refits whenever the canvas size, camera, controls, margin or animate change. When false, fit manually with the component's fit export.

margin
number
no
1
Space left around the children on every side when fitting, in world units.

onFit
() => void
no
Called when a fit has finished, after any animation.