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

extensionloader
exrThree.EXRLoader
hdrThree.RGBELoader
all othersThree.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.

Scene.svelte
<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.

Scene.svelte
<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.

Scene.svelte
<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:

  1. skybox is only set to a GroundedSkybox instance if ground !== false and after the texture is available. It is set to undefined when <Environment> unmounts.

  2. A new instance of GroundedSkybox is created whenever the ground prop updates to something other than false. This is a limitation of the addon. If you need to modify the skybox’s properties, try to do it through the skybox bindable to avoid creating and destroying multiple instances.

  3. scene.environment and/or scene.background are still set to the environment texture. This is done so that the environment map still affects materials used in the scene.

Component Signature

Props

name
type
required
default
description

ground
boolean | { height?: number, radius?: number, resolution?: number }
no
false
creates a ground projected skybox

isBackground
boolean
no
false
whether to set `scene.background` to the loaded environment texture

scene
THREE.Scene
no
useThrelte().scene
the scene that will have its environment and/or background set

skybox
undefined | THREE.GroundedSkybox
no
undefined
a reference to the created `GroundedSkybox` instance when using grounded projection

texture
THREE.DataTexture | THREE.Texture
no
a bindable of the loaded texture

url
string
no
the url to a texture to load and use