@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.
<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.
<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.
<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>