@threlte/extras
<Environment>
Asynchronously loads a single equirectangular-mapped texture and sets the provided scene’s environment and or background to the texture. Here is an example of such a texture.
<script lang="ts">
import { Canvas } from '@threlte/core'
import { Checkbox, Folder, List, Pane, Slider } from 'svelte-tweakpane-ui'
import Scene from './Scene.svelte'
let autoRotateCamera = $state(false)
let environmentIsBackground = $state(true)
let useEnvironment = $state(true)
let environmentInputsDisabled = $derived(!useEnvironment)
const extensions = {
exr: 'exr',
hdr: 'hdr',
jpg: 'jpg'
}
const hdrFiles = {
aerodynamics_workshop: 'aerodynamics_workshop_1k.hdr',
industrial_sunset_puresky: 'industrial_sunset_puresky_1k.hdr',
mpumalanga_veld_puresky: 'mpumalanga_veld_puresky_1k.hdr',
shanghai_riverside: 'shanghai_riverside_1k.hdr'
}
const exrFiles = {
piz_compressed: 'piz_compressed.exr'
}
const jpgFiles = {
equirect_ruined_room: 'equirect_ruined_room.jpg'
}
let extension = $state(extensions.hdr)
const extensionFilePath = $derived(`/textures/equirectangular/${extension}/`)
let exrFile = $state(exrFiles.piz_compressed)
let hdrFile = $state(hdrFiles.shanghai_riverside)
let jpgFile = $state(jpgFiles.equirect_ruined_room)
const extensionIsEXR = $derived(extension === 'exr')
const extensionIsHDR = $derived(extension === 'hdr')
const environmentFile = $derived(extensionIsHDR ? hdrFile : extensionIsEXR ? exrFile : jpgFile)
let materialMetalness = $state(1)
let materialRoughness = $state(0)
const environmentUrl = $derived(extensionFilePath + environmentFile)
</script>
<Pane
title="Environment"
position="fixed"
>
<Checkbox
label="use <Environment>"
bind:value={useEnvironment}
/>
<Checkbox
disabled={environmentInputsDisabled}
label="is background"
bind:value={environmentIsBackground}
/>
<List
disabled={environmentInputsDisabled}
options={extensions}
bind:value={extension}
label="extension"
/>
{#if extensionIsHDR}
<List
disabled={environmentInputsDisabled}
options={hdrFiles}
bind:value={hdrFile}
label="file"
/>
{:else if extensionIsEXR}
<List
disabled={environmentInputsDisabled}
options={exrFiles}
bind:value={exrFile}
label="file"
/>
{:else}
<List
disabled={environmentInputsDisabled}
options={jpgFiles}
bind:value={jpgFile}
label="file"
/>
{/if}
<Folder title="material props">
<Slider
disabled={environmentInputsDisabled}
bind:value={materialMetalness}
label="metalness"
min={0}
max={1}
step={0.1}
/>
<Slider
disabled={environmentInputsDisabled}
bind:value={materialRoughness}
label="roughness"
min={0}
max={1}
step={0.1}
/>
</Folder>
<Folder title="camera">
<Checkbox
bind:value={autoRotateCamera}
label="auto rotate"
/>
</Folder>
</Pane>
<div>
<Canvas>
<Scene
{autoRotateCamera}
{environmentUrl}
{environmentIsBackground}
{materialMetalness}
{materialRoughness}
{useEnvironment}
/>
</Canvas>
</div>
<style>
div {
height: 100%;
}
</style>
<script lang="ts">
import { Environment, OrbitControls } from '@threlte/extras'
import { T } from '@threlte/core'
type Props = {
autoRotateCamera?: boolean
environmentUrl: string
environmentIsBackground?: boolean
isBackground?: boolean
materialMetalness?: number
materialRoughness?: number
useEnvironment?: boolean
}
let {
autoRotateCamera = false,
environmentUrl,
environmentIsBackground = true,
materialMetalness = 1,
materialRoughness = 0,
useEnvironment = true
}: Props = $props()
</script>
<T.PerspectiveCamera
makeDefault
position.z={5}
>
<OrbitControls autoRotate={autoRotateCamera} />
</T.PerspectiveCamera>
<T.Mesh>
<T.TorusGeometry />
<T.MeshStandardMaterial
metalness={materialMetalness}
roughness={materialRoughness}
/>
</T.Mesh>
{#if useEnvironment}
<Environment
isBackground={environmentIsBackground}
url={environmentUrl}
/>
{/if}
Fetching, Loading, and Assigning Textures
<Environment>’s url prop is used to fetch and load textures. If it is provided, a corresponding loader will be used to fetch, load, and assign the texture to scene.environment.
<Environment> supports loading exr, hdr, and any other file type that can be loaded by Three.js’ TextureLoader such as jpg files. This means you can swap the url prop at any time and <Environment> will dispose of the previous texture and assign the new one to the scene’s environment and/or background properties. Loaders within <Environment> are created on demand and cached for future use until <Environment> unmounts.
Internally <Environment> creates a loader based on the extension of the url prop. Refer to the table below to determine what kind of loader is used for a particular extension.
| extension | loader | | ---------- | -------------------------------------------------------------------------------------------------- | | exr | Three.EXRLoader | | hdr | Three.RGBELoader | | all others | Three.TextureLoader |
Any time <Environment> loads a texture, it will dispose of the old one. The texture is also disposed when <Environment> unmounts.
Loaders Are Simple
Loaders used inside <Environment> are not extendable. They only fetch and load the texture at url. If you need to use the methods that Three.js loaders have, you should create the loader outside of <Environment> and load it there then pass the texture through the texture prop.
<script>
import { TextureLoader } from 'three'
const loader = new TextureLoader().setPath('https://path/to/texture/').setRequestHeader({
// .. request headers that will be used when fetching the texture
})
const promise = loader.loadAsync('texture.jpg').then((texture) => {
texture.mapping = EquirectangularReflectionMapping
return texture
})
</script>
{#await promise then texture}
<Environment {texture} />
{/await}
texture Prop for Preloaded Textures
<Environment> provides a bindable texture prop that you can use if you’ve already loaded the texture somewhere else in your application. The example below provides a preloaded texture to <Environment> instead of having it fetch and load one.
<script lang="ts">
import Scene from './Scene.svelte'
import { Canvas } from '@threlte/core'
</script>
<div>
<Canvas>
<Scene />
</Canvas>
</div>
<style>
div {
height: 100%;
}
</style>
<script lang="ts">
import { Environment, OrbitControls } from '@threlte/extras'
import { EquirectangularReflectionMapping } from 'three'
import { RGBELoader } from 'three/examples/jsm/Addons.js'
import { T, useLoader } from '@threlte/core'
const { load } = useLoader(RGBELoader)
const map = load('/textures/equirectangular/hdr/industrial_sunset_puresky_1k.hdr', {
transform(texture) {
texture.mapping = EquirectangularReflectionMapping
return texture
}
})
</script>
<T.PerspectiveCamera
makeDefault
position.z={5}
>
<OrbitControls />
</T.PerspectiveCamera>
<T.Mesh>
<T.MeshStandardMaterial
metalness={1}
roughness={0}
/>
<T.SphereGeometry />
</T.Mesh>
{#await map then texture}
<Environment
isBackground
{texture}
/>
{/await}
Be aware that if <Environment> loads a texture, it will set the texture bindable prop after it has been loaded. This means that if you provide both url and texture properties, the texture at url will eventually be assigned to texture.
<Environment {texture} url="/path/to/texture/file" />
Loading only occurs if url is passed to <Environment>.
Restoring Props When Scene Updates
All of <Environment>’s props are reactive, even scene. If the scene prop is updated, <Environment> will restore the initial environment and background properties of the last scene.
The example below creates a custom render task that draws two scenes to the canvas - one on the left and one on the right. Pressing the button switches the environment to be applied to either the left or the right side. You can observe that when side updates, the original background and environment for the previous side are restored.
<script lang="ts">
import Scene from './Scene.svelte'
import { Button, Checkbox, Pane } from 'svelte-tweakpane-ui'
import { Canvas } from '@threlte/core'
const sides = ['left', 'right'] as const
let i = $state(0)
let side = $derived(sides[i])
let useEnvironment = $state(true)
let isBackground = $state(false)
let disabled = $derived(!useEnvironment)
</script>
<Pane
title="Environment - Swapping Scenes"
position="fixed"
>
<Checkbox
bind:value={useEnvironment}
label="use <Environment>"
/>
<Checkbox
bind:value={isBackground}
{disabled}
label="is background"
/>
<Button
{disabled}
on:click={() => {
i = (i + 1) % sides.length
}}
title="swap scene"
/>
</Pane>
<div>
<Canvas>
<Scene
{isBackground}
{side}
{useEnvironment}
/>
</Canvas>
</div>
<style>
div {
height: 100%;
}
</style>
<script lang="ts">
import { Environment } from '@threlte/extras'
import { Color, PerspectiveCamera, Scene } from 'three'
import { T, observe, useTask, useThrelte } from '@threlte/core'
type Side = 'left' | 'right'
type Props = {
side?: Side
useEnvironment?: boolean
isBackground?: boolean
}
let { side = 'left', isBackground = true, useEnvironment = true }: Props = $props()
const scenes: Record<Side, Scene> = {
left: new Scene(),
right: new Scene()
}
scenes.left.background = new Color('red')
scenes.right.background = new Color('green')
const scene = $derived(scenes[side])
const { autoRender, renderer, size, renderStage } = useThrelte()
// scene is split vertically so the aspect needs to be adjusted
// we could use `useThrelte().camera` here but then we'd have to narrow its type to know if it's a PerspectiveCamera or OrthographicCamera
const camera = new PerspectiveCamera()
camera.position.setZ(10)
// we don't need to run this in the task since we can observe the size store
observe(
() => [size],
([size]) => {
camera.aspect = 0.5 * (size.width / size.height)
camera.updateProjectionMatrix()
}
)
useTask(
() => {
const halfWidth = 0.5 * size.current.width
renderer.setViewport(0, 0, halfWidth, size.current.height)
renderer.setScissor(0, 0, halfWidth, size.current.height)
renderer.render(scenes.left, camera)
renderer.setViewport(halfWidth, 0, halfWidth, size.current.height)
renderer.setScissor(halfWidth, 0, halfWidth, size.current.height)
renderer.render(scenes.right, camera)
},
{ autoInvalidate: false, stage: renderStage }
)
$effect(() => {
const lastAutoRender = autoRender.current
const lastScissorTest = renderer.getScissorTest()
autoRender.set(false)
renderer.setScissorTest(true)
return () => {
autoRender.set(lastAutoRender)
renderer.setScissorTest(lastScissorTest)
}
})
const metalness = 1
const roughness = 0
</script>
<T.AmbientLight attach={scenes.right} />
<T.Mesh attach={scenes.left}>
<T.TorusKnotGeometry />
<T.MeshStandardMaterial
{metalness}
{roughness}
/>
</T.Mesh>
<T.Mesh attach={scenes.right}>
<T.TorusKnotGeometry />
<T.MeshStandardMaterial
{metalness}
{roughness}
/>
</T.Mesh>
{#if useEnvironment}
<Environment
url="/textures/equirectangular/hdr/shanghai_riverside_1k.hdr"
{isBackground}
{scene}
/>
{/if}
Suspended Loading
Any textures that are loaded by <Environment> are suspended so they may be used in a suspense context. This means if you’re fetching the file over a slow network, you can show something in the “fallback” snippet of a <Suspense> component while the texture is being fetched and loaded.
<script>
import { Suspense, Text } from '@threlte/extras'
</script>
<Suspense>
{#snippet fallback()}
<Text text="loading environment" />
{/snippet}
<Environment url="https//url-of-your-file.hdr" />
</Suspense>
Note that suspension only occurs if url is provided. When a texture is provided through the texture prop, there is nothing that needs to loaded, so there’s nothing that needs to be suspended.
Grounded Skyboxes
<Environment> also supports ground projected environments through ThreeJS’s GroundedSkybox addon. To use this feature, set the ground prop to true or to an object with optional height, radius and resolution props. height defaults to 1, radius to 1, and resolution to 128
<script lang="ts">
import { Canvas } from '@threlte/core'
import { Checkbox, Pane } from 'svelte-tweakpane-ui'
import Scene from './Scene.svelte'
let useGround = $state(true)
</script>
<Pane
title="ground projection"
position="fixed"
>
<Checkbox
bind:value={useGround}
label="use ground projection"
/>
</Pane>
<div>
<Canvas>
<Scene {useGround} />
</Canvas>
</div>
<style>
div {
height: 100%;
}
</style>
<script lang="ts">
import { Environment, OrbitControls, Suspense } from '@threlte/extras'
import { GroundedSkybox } from 'three/examples/jsm/Addons.js'
import { T } from '@threlte/core'
type Props = {
useGround?: boolean
}
let { useGround = true }: Props = $props()
let skybox = $state.raw<GroundedSkybox>()
const groundOptions = { height: 15, radius: 100 }
const ground = $derived(useGround === false ? useGround : groundOptions)
const radius = 0.5
const y = groundOptions.height - radius - 0.1
$effect(() => {
skybox?.position.setY(y)
})
</script>
<Suspense>
<T.PerspectiveCamera
makeDefault
position.x={5}
position.y={2}
position.z={5}
>
<OrbitControls
maxDistance={20}
maxPolarAngle={0.5 * Math.PI}
/>
</T.PerspectiveCamera>
<T.Mesh rotation.x={0.5 * Math.PI}>
<T.MeshStandardMaterial metalness={1} />
<T.TorusGeometry args={[2, radius]} />
</T.Mesh>
<Environment
isBackground
url="/textures/equirectangular/hdr/blouberg_sunrise_2_1k.hdr"
{ground}
bind:skybox
/>
</Suspense>
The bindable skybox prop is a reference to the created GroundedSkybox instance. When using this feature, ThreeJS recommends setting the instance’s position.y to height. This will position the “flat” part of the skybox at the origin.
<script>
let skybox = $state() // GroundedSkybox | undefined
const height = 15
$effect(() => {
skybox?.position.setY(height)
})
</script>
<Environment
bind:skybox
url="file.hdr"
ground={{ height }}
/>
There are a couple of important things to consider when using the ground property:
-
skyboxis only set to aGroundedSkyboxinstance ifground!==falseand after the texture is available. It is set toundefinedwhen<Environment>unmounts. -
A new instance of
GroundedSkyboxis created whenever thegroundprop updates to something other thanfalse. This is a limitation of the addon. If you need to modify the skybox’s properties, try to do it through theskyboxbindable to avoid creating and destroying multiple instances. -
scene.environmentand/orscene.backgroundare still set to the environment texture. This is done so that the environment map still affects materials used in the scene.