@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 ThreeJS’s 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 ThreeJS 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: GroundedSkybox | undefined = $state()
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:
-
skybox
is only set to aGroundedSkybox
instance ifground
!==false
and after the texture is available. It is set toundefined
when<Environment>
unmounts. -
A new instance of
GroundedSkybox
is created whenever theground
prop 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 theskybox
bindable to avoid creating and destroying multiple instances. -
scene.environment
and/orscene.background
are still set to the environment texture. This is done so that the environment map still affects materials used in the scene.