threlte logo
@threlte/studio

Static State

Extend the StaticState class to create a new class that holds scene configuration or any other static values that won’t change in production (i.e. are static). Properties added within such a class are automatically integrated into the Studio UI, allowing for easy manipulation and visualization. Changes to these properties will automatically be reflected in the scene and written back to the disk.

This feature is available for classes defined in *.svelte, *.svelte.ts and *.svelte.js files.

For example, you can create a class SceneConfig that extends StaticState and define various properties like directionalLightIntensity, ambientLightIntensity, color, opacity, and showBox. These properties will then be available in the Studio UI for configuration.

Here is an example:

class SceneConfig extends StaticState {
  /**
   * @min 0
   * @max 10
   * @step 0.1
   */
  directionalLightIntensity = $state(3.1)
  /**
   * @min 0
   * @max 1
   */
  ambientLightIntensity = $state(0.13)
  color = $state('#fe3d00')
  /**
   * @min 0
   * @max 1
   */
  opacity = $state(1)
  showBox = $state(true)
}

Using it in your scene yields the following UI:

<script>
  import { Canvas } from '@threlte/core'
  import Scene from './Scene.svelte'
  import { Studio } from '@threlte/studio'
  import { NoToneMapping } from 'three'
</script>

<div>
  <Canvas toneMapping={NoToneMapping}>
    <Studio transient>
      <Scene />
    </Studio>
  </Canvas>
</div>

<style>
  :global(body) {
    margin: 0;
  }

  div {
    width: 100%;
    height: 100%;
  }
</style>
<script lang="ts">
  import { T, type Props } from '@threlte/core'
  import { RoundedBoxGeometry } from '@threlte/extras'
  import type { Mesh } from 'three'

  let { ...props }: Props<typeof Mesh> = $props()
</script>

<T.Mesh {...props}>
  <RoundedBoxGeometry
    radius={0.2}
    args={[1.3, 1.3, 1.3]}
  />
  <T.MeshStandardMaterial color="#fe3d00" />
</T.Mesh>
<script lang="ts">
  import { T, type Props } from '@threlte/core'
  import type { Mesh } from 'three'

  let { ...props }: Props<typeof Mesh> = $props()
</script>

<T.Mesh {...props}>
  <T.IcosahedronGeometry />
  <T.MeshStandardMaterial color="#fe3d00" />
</T.Mesh>
<script lang="ts">
  import { T } from '@threlte/core'
  import { StaticState } from '@threlte/studio'
  import { useStaticState } from '@threlte/studio/extensions'
  import Box from './Box.svelte'
  import Icosahedron from './Icosahedron.svelte'
  import Sphere from './Sphere.svelte'

  const staticStateExtension = useStaticState()
  staticStateExtension.enableEditor()

  class SceneConfig extends StaticState {
    /**
     * @min 1.5
     * @max 5
     */
    gap = $state(2)
  }

  const sceneConfig = new SceneConfig()
</script>

<Icosahedron position={[-sceneConfig.gap, 0, 0]} />
<Box position={[0, 0, 0]} />
<Sphere position={[sceneConfig.gap, 0, 0]} />

<T.PerspectiveCamera
  makeDefault
  fov={33.75}
  position={[0, 2, 10]}
  oncreate={(ref) => {
    ref.lookAt(0, 0, 0)
  }}
/>

<T.DirectionalLight
  position={[3, 10, 7]}
  intensity={2.7}
/>

<T.AmbientLight intensity={0.13} />
<script lang="ts">
  import { T, type Props } from '@threlte/core'
  import type { Mesh } from 'three'

  let { ...props }: Props<typeof Mesh> = $props()
</script>

<T.Mesh {...props}>
  <T.SphereGeometry args={[0.8]} />
  <T.MeshStandardMaterial color="#fe3d00" />
</T.Mesh>
<script>
  import { Canvas } from '@threlte/core'
  import Scene from './Scene.svelte'
  import { Studio } from '@threlte/studio'
  import { NoToneMapping } from 'three'
</script>

<div>
  <Canvas toneMapping={NoToneMapping}>
    <Studio transient>
      <Scene />
    </Studio>
  </Canvas>
</div>

<style>
  :global(body) {
    margin: 0;
  }

  div {
    width: 100%;
    height: 100%;
  }
</style>
<script lang="ts">
  import { T, type Props } from '@threlte/core'
  import { RoundedBoxGeometry } from '@threlte/extras'
  import type { Mesh } from 'three'
  import { SceneConfig } from './config.svelte'

  let { ...props }: Props<typeof Mesh> = $props()

  const sceneConfig = new SceneConfig()
</script>

<T.Mesh {...props}>
  <RoundedBoxGeometry
    radius={0.3}
    args={[1.3, 1.3, 1.3]}
  />
  <T.MeshStandardMaterial
    color={sceneConfig.color}
    transparent
    opacity={sceneConfig.opacity}
    alphaToCoverage
  />
</T.Mesh>
<script lang="ts">
  import { T, type Props } from '@threlte/core'
  import type { Mesh } from 'three'
  import { SceneConfig } from './config.svelte'

  let { ...props }: Props<typeof Mesh> = $props()

  const sceneConfig = new SceneConfig()
</script>

<T.Mesh {...props}>
  <T.IcosahedronGeometry />
  <T.MeshStandardMaterial
    color={sceneConfig.color}
    transparent
    opacity={sceneConfig.opacity}
    alphaToCoverage
  />
</T.Mesh>
<script lang="ts">
  import { T } from '@threlte/core'
  import { useStaticState } from '@threlte/studio/extensions'
  import Box from './Box.svelte'
  import { SceneConfig } from './config.svelte'
  import Icosahedron from './Icosahedron.svelte'
  import Sphere from './Sphere.svelte'

  const staticStateExtension = useStaticState()
  staticStateExtension.enableEditor()

  const config = new SceneConfig()
</script>

<T.PerspectiveCamera
  makeDefault
  fov={33.75}
  position={[0, 2, 10]}
  oncreate={(ref) => {
    ref.lookAt(0, 0, 0)
  }}
/>

<T.DirectionalLight
  position={[3, 10, 7]}
  intensity={config.directionalLightIntensity}
/>

<T.AmbientLight intensity={config.ambientLightIntensity} />

<Icosahedron position={[-2, 0, 0]} />
{#if config.showBox}
  <Box position={[0, 0, 0]} />
{/if}
<Sphere position={[2, 0, 0]} />
<script lang="ts">
  import { T, type Props } from '@threlte/core'
  import type { Mesh } from 'three'
  import { SceneConfig } from './config.svelte'

  let { ...props }: Props<typeof Mesh> = $props()

  const sceneConfig = new SceneConfig()
</script>

<T.Mesh {...props}>
  <T.SphereGeometry args={[0.8]} />
  <T.MeshStandardMaterial
    color={sceneConfig.color}
    transparent
    opacity={sceneConfig.opacity}
    alphaToCoverage
  />
</T.Mesh>
import { StaticState } from '@threlte/studio'

export class SceneConfig extends StaticState {
  /**
   * @min 0
   * @max 10
   * @step 0.1
   */
  directionalLightIntensity = $state(3.1)
  /**
   * @min 0
   * @max 1
   */
  ambientLightIntensity = $state(0.13)
  color = $state('#fe3d00')
  /**
   * @min 0
   * @max 1
   */
  opacity = $state(1)

  showBox = $state(true)
}

Example

Scenario

You want to create a scene that hosts three objects and you want to dial in the gap between the objects.

Scene.svelte
<script>
  import Icosahedron from './Icosahedron.svelte'
  import Sphere from './Sphere.svelte'
  import Box from './Box.svelte'
</script>

<Icosahedron position={[-2, 0, 0]} />
<Sphere position={[0, 0, 0]} />
<Box position={[2, 0, 0]} />

Implementation

Create a State Container

Create a new class SceneConfig that extends StaticState and define a gap property. It must use $state to be reactive in order for the changes to be reflected in the scene.

Scene.svelte
<script>
  import { StaticState } from '@threlte/studio'
  import Icosahedron from './Icosahedron.svelte'
  import Sphere from './Sphere.svelte'
  import Box from './Box.svelte'

  class SceneConfig extends StaticState {
    gap = $state(1.5)
  }
</script>

<Icosahedron position={[-2, 0, 0]} />
<Sphere position={[0, 0, 0]} />
<Box position={[2, 0, 0]} />

Create an Instance

Create a new instance of SceneConfig and use it to update the position of the objects.

Scene.svelte
<script>
  import { StaticState } from '@threlte/studio'
  import Icosahedron from './Icosahedron.svelte'
  import Sphere from './Sphere.svelte'
  import { StaticState } from '@threlte/studio'

  class SceneConfig extends StaticState {
    gap = $state(1.5)
  }

  const sceneConfig = new SceneConfig()
</script>

<Icosahedron position={[-sceneConfig.gap, 0, 0]} />
<Sphere position={[0, 0, 0]} />
<Box position={[sceneConfig.gap, 0, 0]} />

Bonus: Use UI Modifiers

To tweak the resulting UI, you can use JSDoc tags to add modifiers. For example, you can add @min and @max to the gap property to restrict the range of values that can be entered. This will yield a slider in the Studio UI.

class SceneConfig extends StaticState {
  /**
   * @min 1.5
   * @max 5
   */
  gap = $state(2)
}

You’re done! Changes to the gap property in the Studio UI will automatically be reflected in the scene and written back to the disk.

<script>
  import { Canvas } from '@threlte/core'
  import Scene from './Scene.svelte'
  import { Studio } from '@threlte/studio'
  import { NoToneMapping } from 'three'
</script>

<div>
  <Canvas toneMapping={NoToneMapping}>
    <Studio transient>
      <Scene />
    </Studio>
  </Canvas>
</div>

<style>
  :global(body) {
    margin: 0;
  }

  div {
    width: 100%;
    height: 100%;
  }
</style>
<script lang="ts">
  import { T, type Props } from '@threlte/core'
  import { RoundedBoxGeometry } from '@threlte/extras'
  import type { Mesh } from 'three'

  let { ...props }: Props<typeof Mesh> = $props()
</script>

<T.Mesh {...props}>
  <RoundedBoxGeometry
    radius={0.2}
    args={[1.3, 1.3, 1.3]}
  />
  <T.MeshStandardMaterial color="#fe3d00" />
</T.Mesh>
<script lang="ts">
  import { T, type Props } from '@threlte/core'
  import type { Mesh } from 'three'

  let { ...props }: Props<typeof Mesh> = $props()
</script>

<T.Mesh {...props}>
  <T.IcosahedronGeometry />
  <T.MeshStandardMaterial color="#fe3d00" />
</T.Mesh>
<script lang="ts">
  import { T } from '@threlte/core'
  import { StaticState } from '@threlte/studio'
  import { useStaticState } from '@threlte/studio/extensions'
  import Box from './Box.svelte'
  import Icosahedron from './Icosahedron.svelte'
  import Sphere from './Sphere.svelte'

  const staticStateExtension = useStaticState()
  staticStateExtension.enableEditor()

  class SceneConfig extends StaticState {
    /**
     * @min 1.5
     * @max 5
     */
    gap = $state(2)
  }

  const sceneConfig = new SceneConfig()
</script>

<Icosahedron position={[-sceneConfig.gap, 0, 0]} />
<Box position={[0, 0, 0]} />
<Sphere position={[sceneConfig.gap, 0, 0]} />

<T.PerspectiveCamera
  makeDefault
  fov={33.75}
  position={[0, 2, 10]}
  oncreate={(ref) => {
    ref.lookAt(0, 0, 0)
  }}
/>

<T.DirectionalLight
  position={[3, 10, 7]}
  intensity={2.7}
/>

<T.AmbientLight intensity={0.13} />
<script lang="ts">
  import { T, type Props } from '@threlte/core'
  import type { Mesh } from 'three'

  let { ...props }: Props<typeof Mesh> = $props()
</script>

<T.Mesh {...props}>
  <T.SphereGeometry args={[0.8]} />
  <T.MeshStandardMaterial color="#fe3d00" />
</T.Mesh>