threlte logo
@threlte/extras

<Grid>

webgl <Grid> works with WebGL and is available from the @threlte/extras export. webgpu <Grid> works with WebGPU and is available from the @threlte/extras/webgpu export.

A robust grid implementation with multiple tweakable parameters.

<script lang="ts">
  import Scene from './Scene.svelte'
  import { Canvas, T } from '@threlte/core'
  import { Checkbox, Color, Folder, Pane, List, Slider, Point } from 'svelte-tweakpane-ui'
  import { Grid, TransformControls } from '@threlte/extras'
  import { PlaneGeometry } from 'three'
  import { SimplexNoise } from 'three/examples/jsm/Addons.js'

  let cellSize = $state(1)
  let cellColor = $state('#cccccc')
  let cellThickness = $state(1.4)
  let sectionSize = $state(5)
  let sectionColor = $state('#ff3e00')
  let sectionThickness = $state(2)

  let gridSize = $state<[number, number]>([20, 20])
  const planeOptions = {
    xz: 'xz',
    xy: 'xy',
    zy: 'zy'
  }
  let plane: keyof typeof planeOptions = $state('xz')

  let followCamera = $state(false)
  let infiniteGrid = $state(false)
  let useFadeOrigin = $state(false)
  let fadeOrigin = $state<[number, number, number]>([0, 0, 0])
  let fadeDistance = $state(100)
  let backgroundColor = $state('#003eff')
  let backgroundOpacity = $state(0)
  let fadeStrength = $state(1)
  const gridGeometryOptions = {
    plane: 'default',
    terrain: 'terrain'
  }

  let gridGeometry = $state('default')
  const gridGeometryIsTerrain = $derived(gridGeometry === 'terrain')

  const gridTypeOptions = {
    polar: 'polar',
    grid: 'grid',
    lines: 'lines',
    circular: 'circular'
  }
  let gridType: keyof typeof gridTypeOptions = $state('polar')

  const linesAxisOptions = {
    x: 'x',
    y: 'y',
    z: 'z'
  }
  let linesAxis: keyof typeof linesAxisOptions = $state('x')

  let maxRadius = $state(10)
  let cellDividers = $state(6)
  let sectionDividers = $state(2)

  const terrainSize = 30
  const segments = 100
  const noise = new SimplexNoise()
  const geometry = new PlaneGeometry(terrainSize, terrainSize, segments, segments)
  const positions = geometry.getAttribute('position')
  for (let i = 0; i < positions.count; i += 1) {
    const x = positions.getX(i)
    const y = positions.getY(i)
    const height = noise.noise(x / 5, y / 5) * 1 + noise.noise(x / 40, y / 40) * 2
    positions.setZ(i, height)
  }
  geometry.computeVertexNormals()
</script>

<Pane
  title=""
  position="fixed"
>
  <Slider
    bind:value={cellSize}
    label="cellSize"
    step={1}
    min={1}
    max={5}
  />
  <Color
    bind:value={cellColor}
    label="cellColor"
  />
  <Slider
    bind:value={cellThickness}
    label="cellThickness"
    step={0.1}
    min={1}
    max={10}
  />
  <Slider
    bind:value={sectionSize}
    label="sectionSize"
    step={1}
    min={1}
    max={50}
  />
  <Color
    bind:value={sectionColor}
    label="sectionColor"
  />
  <Slider
    bind:value={sectionThickness}
    label="sectionThickness"
    step={0.1}
    min={1}
    max={10}
  />
  <Point
    bind:value={gridSize}
    label="gridSize"
    step={1}
    min={1}
    max={100}
  />
  <List
    bind:value={plane}
    label="plane"
    options={planeOptions}
  />
  <Checkbox
    bind:value={followCamera}
    label="followCamera"
  />
  <Checkbox
    bind:value={infiniteGrid}
    label="infiniteGrid"
  />
  <Checkbox
    bind:value={useFadeOrigin}
    label="useFadeOrigin"
  />
  <Slider
    bind:value={fadeDistance}
    label="fadeDistance"
    step={10}
    min={10}
    max={400}
  />
  <Color
    bind:value={backgroundColor}
    label="backgroundColor"
  />
  <Slider
    bind:value={backgroundOpacity}
    label="backgroundOpacity"
    step={0.01}
    min={0}
    max={1}
  />
  <Slider
    bind:value={fadeStrength}
    label="fadeStrength"
    step={0.1}
    min={0}
    max={20}
  />
  <List
    bind:value={gridGeometry}
    options={gridGeometryOptions}
    label="grid geometry"
  />

  <Folder title="Types of Grid">
    <List
      bind:value={gridType}
      options={gridTypeOptions}
      label="type"
    />
    {#if gridType == 'lines'}
      <List
        bind:value={linesAxis}
        options={linesAxisOptions}
        label="axis"
      />
    {/if}
    {#if gridType == 'polar' || gridType == 'circular'}
      <Slider
        bind:value={maxRadius}
        label="maxRadius"
        step={1}
        min={0}
        max={15}
      />
    {/if}
    {#if gridType == 'polar'}
      <Slider
        bind:value={cellDividers}
        label="cellDividers"
        step={1}
        min={0}
        max={18}
      />
      <Slider
        bind:value={sectionDividers}
        label="sectionDividers"
        step={1}
        min={0}
        max={18}
      />
    {/if}
  </Folder>
</Pane>

<div>
  <Canvas>
    {#if useFadeOrigin}
      <TransformControls
        onobjectChange={(e) => {
          e.target.object.position.toArray(fadeOrigin)
        }}
      />
    {/if}
    {#if gridGeometryIsTerrain}
      <Grid
        position.y={-2}
        {plane}
        {cellColor}
        {cellSize}
        {cellThickness}
        {sectionColor}
        {sectionSize}
        {sectionThickness}
        {followCamera}
        {infiniteGrid}
        {fadeDistance}
        {fadeStrength}
        fadeOrigin={useFadeOrigin ? fadeOrigin : undefined}
        {gridSize}
        {backgroundColor}
        {backgroundOpacity}
        type={gridType}
        axis={linesAxis}
        {maxRadius}
        {cellDividers}
        {sectionDividers}
      >
        <T is={geometry} />
      </Grid>
    {:else}
      <Grid
        {plane}
        {cellColor}
        {cellSize}
        {cellThickness}
        {sectionColor}
        {sectionSize}
        {sectionThickness}
        {followCamera}
        {infiniteGrid}
        {fadeDistance}
        {fadeStrength}
        fadeOrigin={useFadeOrigin ? fadeOrigin : undefined}
        {gridSize}
        {backgroundColor}
        {backgroundOpacity}
        type={gridType}
        axis={linesAxis}
        {maxRadius}
        {cellDividers}
        {sectionDividers}
      />
    {/if}

    <Scene />
  </Canvas>
</div>

<style>
  div {
    height: 100%;
  }
</style>
<script lang="ts">
  import { BoxGeometry, Vector2 } from 'three'
  import { Gizmo, OrbitControls } from '@threlte/extras'
  import { T } from '@threlte/core'

  const positions: Vector2[] = []
  const count = 4
  for (let j = 0; j < count; j += 1) {
    for (let i = 0; i < count; i += 1) {
      positions.push(new Vector2(i, j).multiplyScalar(2).subScalar(3))
    }
  }
</script>

<T.PerspectiveCamera
  makeDefault
  position={15}
  fov={60}
>
  <OrbitControls>
    <Gizmo />
  </OrbitControls>
</T.PerspectiveCamera>

<!-- Make a box in every second cell to show aligment -->
{#each positions as { x, y }}
  <T.Group position={[x, 0.5, y]}>
    <T.Mesh>
      <T.BoxGeometry />
      <T.MeshBasicMaterial
        color="white"
        opacity={0.9}
        transparent
      />
    </T.Mesh>
    <T.LineSegments>
      <T.EdgesGeometry args={[new BoxGeometry()]} />
      <T.LineBasicMaterial color="black" />
    </T.LineSegments>
  </T.Group>
{/each}
<script lang="ts">
  import Scene from './Scene.svelte'
  import { Canvas, T } from '@threlte/core/webgpu'
  import { Checkbox, Color, Folder, Pane, List, Slider, Point } from 'svelte-tweakpane-ui'
  import { Grid, TransformControls } from '@threlte/extras/webgpu'
  import { PlaneGeometry } from 'three/webgpu'
  import { SimplexNoise } from 'three/examples/jsm/Addons.js'

  let cellSize = $state(1)
  let cellColor = $state('#cccccc')
  let cellThickness = $state(1.4)
  let sectionSize = $state(5)
  let sectionColor = $state('#ff3e00')
  let sectionThickness = $state(2)

  let gridSize = $state<[number, number]>([20, 20])
  const planeOptions = {
    xz: 'xz',
    xy: 'xy',
    zy: 'zy'
  }
  let plane: keyof typeof planeOptions = $state('xz')

  let followCamera = $state(false)
  let infiniteGrid = $state(false)
  let useFadeOrigin = $state(false)
  let fadeOrigin = $state<[number, number, number]>([0, 0, 0])
  let fadeDistance = $state(100)
  let backgroundColor = $state('#003eff')
  let backgroundOpacity = $state(0)
  let fadeStrength = $state(1)
  const gridGeometryOptions = {
    plane: 'default',
    terrain: 'terrain'
  }

  let gridGeometry = $state('default')
  const gridGeometryIsTerrain = $derived(gridGeometry === 'terrain')

  const gridTypeOptions = {
    polar: 'polar',
    grid: 'grid',
    lines: 'lines',
    circular: 'circular'
  }
  let gridType: keyof typeof gridTypeOptions = $state('polar')

  const linesAxisOptions = {
    x: 'x',
    y: 'y',
    z: 'z'
  }
  let linesAxis: keyof typeof linesAxisOptions = $state('x')

  let maxRadius = $state(10)
  let cellDividers = $state(6)
  let sectionDividers = $state(2)

  const terrainSize = 30
  const segments = 100
  const noise = new SimplexNoise()
  const geometry = new PlaneGeometry(terrainSize, terrainSize, segments, segments)
  const positions = geometry.getAttribute('position')
  for (let i = 0; i < positions.count; i += 1) {
    const x = positions.getX(i)
    const y = positions.getY(i)
    const height = noise.noise(x / 5, y / 5) * 1 + noise.noise(x / 40, y / 40) * 2
    positions.setZ(i, height)
  }
  geometry.computeVertexNormals()
</script>

<Pane
  title=""
  position="fixed"
>
  <Slider
    bind:value={cellSize}
    label="cellSize"
    step={1}
    min={1}
    max={5}
  />
  <Color
    bind:value={cellColor}
    label="cellColor"
  />
  <Slider
    bind:value={cellThickness}
    label="cellThickness"
    step={0.1}
    min={1}
    max={10}
  />
  <Slider
    bind:value={sectionSize}
    label="sectionSize"
    step={1}
    min={1}
    max={50}
  />
  <Color
    bind:value={sectionColor}
    label="sectionColor"
  />
  <Slider
    bind:value={sectionThickness}
    label="sectionThickness"
    step={0.1}
    min={1}
    max={10}
  />
  <Point
    bind:value={gridSize}
    label="gridSize"
    step={1}
    min={1}
    max={100}
  />
  <List
    bind:value={plane}
    label="plane"
    options={planeOptions}
  />
  <Checkbox
    bind:value={followCamera}
    label="followCamera"
  />
  <Checkbox
    bind:value={infiniteGrid}
    label="infiniteGrid"
  />
  <Checkbox
    bind:value={useFadeOrigin}
    label="useFadeOrigin"
  />
  <Slider
    bind:value={fadeDistance}
    label="fadeDistance"
    step={10}
    min={10}
    max={400}
  />
  <Color
    bind:value={backgroundColor}
    label="backgroundColor"
  />
  <Slider
    bind:value={backgroundOpacity}
    label="backgroundOpacity"
    step={0.01}
    min={0}
    max={1}
  />
  <Slider
    bind:value={fadeStrength}
    label="fadeStrength"
    step={0.1}
    min={0}
    max={20}
  />
  <List
    bind:value={gridGeometry}
    options={gridGeometryOptions}
    label="grid geometry"
  />

  <Folder title="Types of Grid">
    <List
      bind:value={gridType}
      options={gridTypeOptions}
      label="type"
    />
    {#if gridType == 'lines'}
      <List
        bind:value={linesAxis}
        options={linesAxisOptions}
        label="axis"
      />
    {/if}
    {#if gridType == 'polar' || gridType == 'circular'}
      <Slider
        bind:value={maxRadius}
        label="maxRadius"
        step={1}
        min={0}
        max={15}
      />
    {/if}
    {#if gridType == 'polar'}
      <Slider
        bind:value={cellDividers}
        label="cellDividers"
        step={1}
        min={0}
        max={18}
      />
      <Slider
        bind:value={sectionDividers}
        label="sectionDividers"
        step={1}
        min={0}
        max={18}
      />
    {/if}
  </Folder>
</Pane>

<div>
  <Canvas>
    {#if useFadeOrigin}
      <TransformControls
        onobjectChange={(e) => {
          e.target.object.position.toArray(fadeOrigin)
        }}
      />
    {/if}
    {#if gridGeometryIsTerrain}
      <Grid
        position.y={-2}
        {plane}
        {cellColor}
        {cellSize}
        {cellThickness}
        {sectionColor}
        {sectionSize}
        {sectionThickness}
        {followCamera}
        {infiniteGrid}
        {fadeDistance}
        {fadeStrength}
        fadeOrigin={useFadeOrigin ? fadeOrigin : undefined}
        {gridSize}
        {backgroundColor}
        {backgroundOpacity}
        type={gridType}
        axis={linesAxis}
        {maxRadius}
        {cellDividers}
        {sectionDividers}
      >
        <T is={geometry} />
      </Grid>
    {:else}
      <Grid
        {plane}
        {cellColor}
        {cellSize}
        {cellThickness}
        {sectionColor}
        {sectionSize}
        {sectionThickness}
        {followCamera}
        {infiniteGrid}
        {fadeDistance}
        {fadeStrength}
        fadeOrigin={useFadeOrigin ? fadeOrigin : undefined}
        {gridSize}
        {backgroundColor}
        {backgroundOpacity}
        type={gridType}
        axis={linesAxis}
        {maxRadius}
        {cellDividers}
        {sectionDividers}
      />
    {/if}

    <Scene />
  </Canvas>
</div>

<style>
  div {
    height: 100%;
  }
</style>
<script lang="ts">
  import { BoxGeometry, Vector2 } from 'three/webgpu'
  import { OrbitControls } from '@threlte/extras/webgpu'
  import { T } from '@threlte/core/webgpu'

  const positions: Vector2[] = []
  const count = 4
  for (let j = 0; j < count; j += 1) {
    for (let i = 0; i < count; i += 1) {
      positions.push(new Vector2(i, j).multiplyScalar(2).subScalar(3))
    }
  }
</script>

<T.PerspectiveCamera
  makeDefault
  position={15}
  fov={60}
>
  <OrbitControls />
</T.PerspectiveCamera>

<!-- Make a box in every second cell to show aligment -->
{#each positions as { x, y }}
  <T.Group position={[x, 0.5, y]}>
    <T.Mesh>
      <T.BoxGeometry />
      <T.MeshBasicMaterial
        color="white"
        opacity={0.9}
        transparent
      />
    </T.Mesh>
    <T.LineSegments>
      <T.EdgesGeometry args={[new BoxGeometry()]} />
      <T.LineBasicMaterial color="black" />
    </T.LineSegments>
  </T.Group>
{/each}

Usage

This component provides sensible defaults. You can initialize the default grid with just <Grid>. ref passes a reference from the <T.Mesh/> the grid is constructed on.

Grid types

The grid type can be selected by setting the type parameter. The available grid types are:

  • grid: represents a standard box grid. It does not require any additional properties. (default)
  • lines: grid consisting of lines that align along a single world axis. You specify this axis by providing either x, y or z to the axis property.
  • circular: grid formed of concentric circles. It includes a maxRadius property that sets the maximum world-space radius for the grid. A value of 0 removes this limit, allowing the grid to occupy the entire geometry, even if it results in incomplete circles.
  • polar: similar to the circular type, but it also features lines that subdivide the concentric circles. It too has a maxRadius property. Additionally, it has two properties for specifying dividers: cellDividers and sectionDividers. These determine how many lines will segment the circle into various sectors. For example, 2 lines result in 4 segments at 90° each, while 6 lines create 12 sectors at 30° apiece.
GridLinesCircularPolar
Grid previewGrid previewGrid previewGrid preview

Cells and Sections

Grid is split into cells and sections. Cell is meant to represent the smallest units on your grid, whereas section is a group of cells. You can adjust the size of the grid by changing the cellSize and sectionSize parameters. Size is in Three world units, so for example a mesh with BoxGeometry(1,1,1) will fit perfectly into a size 1 cell. By default a cell is 1 unit and a section 10, which means that a grid of 10x10 cells will be outlined with a section line.

Lines

You can adjust the color and thickness of cell and section lines with cellColor, cellThickness, sectionColor, sectionThickness.

Grid size and fading

The <Grid> component is a THREE.Mesh with a PlaneGeometry attached to it. The gridSize parameter defines the size of the PlaneGeometry. You can extend the grid into infinity if you set the infiniteGrid parameter to true. Changing fadeDistance sets how far from the camera position the grid begins to fade by having its alpha reduced. fadeStrength determines how fast it happens (exponent). fadeStrength = 0 means that there is no fading (not recommended for large grids). You can change the fading point of origin to not be the camera position but a custom point by setting fadeOrigin.

Custom geometry

You have the option to insert your own custom geometry into the <Grid/> slot. The preceding example demonstrates this by showcasing a preview of a terrain-like geometry generated using Perlin noise.

<Grid>
  <T.BoxGeometry />
</Grid>

Follow camera

Setting followCamera to true applies a transform that moves the grid to the camera’s position on the chosen plane.

Depth

The grid doesn’t write to the depth buffer by default, so transparent objects rendered after it aren’t hidden behind it. Set depthWrite to true if you want the grid to occlude them.

The grid also loses depth ties with geometry lying on the same plane, so objects resting on it render cleanly along their base edges.

Component Signature

<Grid> extends < T . Mesh > and supports all its props, snippets, bindings and events.

Props

name
type
required
default
description

axis
'x' | 'y' | 'z'
no
'x'
The world axis the lines run along.

backgroundColor
ColorRepresentation
no
'#dadada'
The color drawn between the lines. Only visible when backgroundOpacity is above 0.

backgroundOpacity
number
no
0
The opacity of backgroundColor. 0 leaves the space between the lines transparent.

cellColor
ColorRepresentation
no
'#000000'
The color of the cell lines.

cellDividers
number
no
6
The number of lines through the center that divide the cells. Each line splits the circle in two, so 6 makes 12 segments of 30°.

cellSize
number
no
1
The size of a cell in world units.

cellThickness
number
no
1
The thickness of the cell lines.

depthWrite
boolean
no
false
Whether the grid writes to the depth buffer. Enable to make it occlude transparent objects rendered after it.

fadeDistance
number
no
100
The distance from fadeOrigin at which the grid has fully faded out.

fadeOrigin
Vector3 | Vector3Tuple
no
the camera's position
The point the grid fades out from.

fadeStrength
number
no
1
How quickly the grid fades out with distance. Higher values fade sooner, and 0 disables fading.

followCamera
boolean
no
false
Moves the grid along its plane with the camera, so it never runs out.

gridSize
number | [number, number]
no
[20, 20]
The width and height of the grid plane in world units. A number sets both.

infiniteGrid
boolean
no
false
Scales the grid plane up by fadeDistance so it reaches the horizon. Also ignores maxRadius.

maxRadius
number
no
0
The radius beyond which nothing is drawn, in world units. 0 removes the limit.

plane
'xz' | 'xy' | 'zy'
no
'xz'
The plane the grid lies in.

sectionColor
ColorRepresentation
no
'#0000ee'
The color of the section lines.

sectionDividers
number
no
2
The number of lines through the center that divide the sections. 2 quarters the grid into 4 sections of 90°.

sectionSize
number
no
10
The size of a section in world units. Sections are the larger divisions drawn on top of the cells.

sectionThickness
number
no
2
The thickness of the section lines.

side
Side
no
DoubleSide
Which side of the grid plane is rendered.

type
'grid' | 'lines' | 'circular' | 'polar'
no
'grid'
The pattern drawn on the grid. 'lines' draws parallel lines along axis. 'circular' draws concentric circles. 'polar' draws concentric circles divided by lines through the center.