Skip to content

CodyJasonBennett/four

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

88 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Size Version Downloads

four

Minimal three.js alternative.

Table of Contents

Installation

To install, use your preferred package manager or CDN:

npm install four@npm:fourwastaken
yarn add four@npm:fourwastaken
pnpm add four@npm:fourwastaken
<script type="module">
  import * as FOUR from 'https://unpkg.com/fourwastaken'
</script>

Note: Vite may have issues consuming WebGPU code which relies on top-level await via ESM. This is well supported since 2021, but you may need to use vite-plugin-top-level-await to use this library with vite.optimizeDeps.

Getting Started

The following creates a renderer, camera, and renders a red cube:

Show WebGL example
import { WebGLRenderer, PerspectiveCamera, Geometry, Material, Mesh } from 'four'

const renderer = new WebGLRenderer()
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.canvas)

const camera = new PerspectiveCamera(45, window.innerWidth / window.innerHeight)
camera.position.z = 5

const geometry = new Geometry({
  position: {
    size: 3,
    data: new Float32Array([
      0.5, 0.5, 0.5, 0.5, -0.5, 0.5, 0.5, 0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5, -0.5, 0.5, 0.5, -0.5, -0.5, 0.5, -0.5,
      -0.5, -0.5, -0.5, -0.5, 0.5, 0.5, -0.5, -0.5, -0.5, -0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5, 0.5, -0.5, -0.5, 0.5,
      0.5, 0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, -0.5, -0.5, -0.5, 0.5, -0.5, -0.5, -0.5, 0.5, -0.5,
      0.5, -0.5, -0.5, -0.5, 0.5, -0.5, -0.5, 0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, 0.5, -0.5,
      -0.5, 0.5, 0.5, -0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, -0.5, 0.5, -0.5, -0.5, -0.5, 0.5, -0.5, 0.5, -0.5, -0.5, -0.5,
      -0.5, -0.5, -0.5, 0.5, -0.5,
    ]),
  },
})
const material = new Material({
  vertex: /* glsl */ `#version 300 es
    uniform mat4 projectionMatrix;
    uniform mat4 modelViewMatrix;
    in vec3 position;
    void main() {
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1);
    }
  `,
  fragment: /* glsl */ `#version 300 es
    out lowp vec4 color;
    void main() {
      color = vec4(1, 0, 0, 1);
    }
  `,
})
const mesh = new Mesh(geometry, material)

renderer.render(mesh, camera)
Show WebGPU example
import { WebGPURenderer, PerspectiveCamera, Geometry, Material, Mesh } from 'four'

const renderer = new WebGPURenderer()
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.canvas)

const camera = new PerspectiveCamera(45, window.innerWidth / window.innerHeight)
camera.position.z = 5

const geometry = new Geometry({
  position: {
    size: 3,
    data: new Float32Array([
      0.5, 0.5, 0.5, 0.5, -0.5, 0.5, 0.5, 0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5, -0.5, 0.5, 0.5, -0.5, -0.5, 0.5, -0.5,
      -0.5, -0.5, -0.5, -0.5, 0.5, 0.5, -0.5, -0.5, -0.5, -0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5, 0.5, -0.5, -0.5, 0.5,
      0.5, 0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, -0.5, -0.5, -0.5, 0.5, -0.5, -0.5, -0.5, 0.5, -0.5,
      0.5, -0.5, -0.5, -0.5, 0.5, -0.5, -0.5, 0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, 0.5, -0.5,
      -0.5, 0.5, 0.5, -0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, -0.5, 0.5, -0.5, -0.5, -0.5, 0.5, -0.5, 0.5, -0.5, -0.5, -0.5,
      -0.5, -0.5, -0.5, 0.5, -0.5,
    ]),
  },
})
const material = new Material({
  vertex: /* wgsl */ `
    struct Uniforms {
      projectionMatrix: mat4x4<f32>,
      modelViewMatrix: mat4x4<f32>,
    };
    @group(0) @binding(0) var<uniform> uniforms: Uniforms;

    @vertex
    fn main(@location(0) position: vec3<f32>) -> @builtin(position) vec4<f32> {
      return uniforms.projectionMatrix * uniforms.modelViewMatrix * vec4(position, 1);
    }
  `,
  fragment: /* wgsl */ `
    @fragment
    fn main() -> @location(0) vec4<f32> {
      return vec4(1, 0, 0, 1);
    }
  `,
})
const mesh = new Mesh(geometry, material)

renderer.render(mesh, camera)

Object3D

An Object3D represents a basic 3D object and its transforms. Objects are linked via their parent and children properties, constructing a rooted scene-graph.

const object = new Object3D()
object.add(new Object3D(), new Object3D())
object.traverse((node) => {
  if (node !== object) object.remove(node)
  if (!node.visible) return true
})

Vector3

A Vector3 represents a three-dimensional (x, y, z) vector and describes local position in Object3D.position. It is also used to control local scale in Object3D.scale.

object.position.set(1, 2, 3)
object.position.x = 4
object.position[0] = 5

Quaternion

A Quaternion represents a four-dimensional vector with a rotation axis (x, y, z) and magnitude (w) and describes local orientation in Object3D.quaternion.

object.quaternion.set(0, 0, 0, 1)
object.quaternion.fromEuler(Math.PI / 2, 0, 0)
object.quaternion.x *= -1
object.quaternion[0] *= -1

Matrix4

A Matrix4 represents a 4x4 transformation matrix and describes world transforms in Object3D.matrix.

object.matrix.set(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 2, 3, 1)
object.matrix[12] = 4
object.matrix.invert()
object.matrix.identity()

Mesh

A Mesh contains a Geometry and Material to describe visual behavior, and can be manipulated in 3D as an Object3D.

const geometry = new Geometry({ ... })
const material = new Material({ ... })
const mesh = new Mesh(geometry, material)

Geometry

A Geometry contains an Attribute list of vertex or storage buffer data, with a GPU buffer allocated for each Attribute.

const geometry = new Geometry({
  position: { size: 2, data: new Float32Array([-1, -1, 3, -1, -1, 3]) },
  uv: { size: 2, data: new Float32Array([0, 0, 2, 0, 0, 2]) },
  index: { size: 1, data: new Uint16Array([0, 1, 2]) },
})

A DrawRange can also be configured to control rendering without submitting vertex data. This is useful for GPU-computed geometry or vertex pulling, as demonstrated in the fullscreen demos.

const geometry = new Geometry()
geometry.drawRange = { start: 0, count: 3 } // renders 3 vertices at starting index 0

Attribute

An Attribute defines a data view, its per-vertex size, and an optional per-instance divisor (see instancing).

// Creates a 4x4 instance matrix for 2 instances
{
  data: new Float32Array([
    1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1,
    1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1,
  ]),
  size: 16,
  divisor: 1,
}

Material

A Material describes a program or shader interface for rasterization and compute (see compute), defining a vertex and fragment or compute shader, respectively.

Show WebGL example
const material = new Material({
  vertex: /* glsl */ `#version 300 es
    uniform mat4 projectionMatrix;
    uniform mat4 modelViewMatrix;
    in vec3 position;
    void main() {
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1);
    }
  `,
  fragment: /* glsl */ `#version 300 es
    out lowp vec4 color;
    void main() {
      color = vec4(1, 0, 0, 1);
    }
  `,
  side: 'front',
  transparent: false,
  depthTest: true,
  depthWrite: true,
})
Show WebGPU example
const material = new Material({
  vertex: /* wgsl */ `
    struct Uniforms {
      projectionMatrix: mat4x4<f32>,
      modelViewMatrix: mat4x4<f32>,
    };
    @group(0) @binding(0) var<uniform> uniforms: Uniforms;

    @vertex
    fn main(@location(0) position: vec3<f32>) -> @builtin(position) vec4<f32> {
      return uniforms.projectionMatrix * uniforms.modelViewMatrix * vec4(position, 1);
    }
  `,
  fragment: /* wgsl */ `
    @fragment
    fn main() -> @location(0) vec4<f32> {
      return vec4(1, 0, 0, 1);
    }
  `,
  side: 'front',
  transparent: false,
  depthTest: true,
  depthWrite: true,
})

Uniforms

The following uniforms are built-in and will be automatically populated when specified:

Type Name Description Conversion
mat4x4 modelMatrix world-space mesh transform local space => world space
mat4x4 projectionMatrix clip-space camera projection view space => clip space
mat4x4 viewMatrix inverse camera transform world space => view space
mat4x4 modelViewMatrix premultiplied model-view transform local space => view space
mat4x4 normalMatrix isotropic inverse model-view or "normal" transform local space => view space

In WebGPU, uniforms are bound to a single uniform buffer, preceded by storage buffers, and followed by sampler-texture for texture uniforms.

// Storage buffers
@group(0) @binding(0)
var<storage, read_write> data: array<vec2<f32>>;

// Uniform buffer
struct Uniforms {
  time: f32,
};
@group(0) @binding(1) var<uniform> uniforms: Uniforms;

// Texture bindings
@group(0) @binding(2) var sample: sampler;
@group(0) @binding(3) var color: texture_2d<f32>;

@group(0) @binding(4) var sample_2: sampler;
@group(0) @binding(5) var color_2: texture_2d<f32>;

Blending

By default, opaque meshes do not blend but replace values, and transparent meshes alpha blend by the following blend equation:

material.blending = {
  color: {
    operation: 'add',
    srcFactor: 'src-alpha',
    dstFactor: 'one-minus-src-alpha',
  },
  alpha: {
    operation: 'add',
    srcFactor: 'one',
    dstFactor: 'one-minus-src-alpha',
  },
}

This gets applied to the final fragment color as src * srcFactor + dst * dstFactor, assuming a premultiplied alpha.

Custom blending can be used for postprocessing and various VFX. The following are the most common configurations:

Blend Mode BlendOperation BlendFactor (src) BlendFactor (dst)
Additive add src-alpha one
Subtractive reverse-subtract src-alpha one
Multiply add dst-color zero
Screen add one-minus-src-color one
Maximize max src-alpha dst-alpha
Custom add one one
Local Additive add dst-alpha one
Disabled add one zero

Texture

A Texture transports or stores image or video data to the GPU as well as data like normals or depth.

const pixel = new Uint8ClampedArray([76, 51, 128, 255])
const image = new ImageData(pixel, 1, 1)
const texture = new Texture(image)

Sampler

A Sampler configures texel filtering and transforms for a texture, and can be used to sample a texture multiple times with different configurations in a shader.

const sampler = new Sampler({
  magFilter: 'nearest',
  minFilter: 'nearest',
  wrapS: 'clamp',
  wrapT: 'clamp',
  anisotropy: 1,
})
texture.sampler = sampler

RenderTarget

A RenderTarget constructs a frame buffer object which can be drawn to, similar to the canvas itself. Unlike the canvas, render targets can have multiple attachments or texture channels, configurable as the third argument count, enabling efficient use of techniques like deferred rendering and postprocessing.

// Create render target with 4 channels
const width = window.innerWidth
const height = window.innerHeight
const count = 4
const renderTarget = new RenderTarget(width, height, count)

// Resize with page
window.addEventListener('resize', () => {
  const width = window.innerWidth
  const height = window.innerHeight
  renderTarget.setSize(width, height)
})

// Bind and render to render target
renderer.setRenderTarget(renderTarget)
renderer.render(scene, camera)

// Unbind to canvas
renderer.setRenderTarget(null)

Camera

A Camera contains matrices and a frustum for projection transforms and queries. The type of projection is defined by Camera.projectionMatrix.

Frustum

A Frustum contains clipping planes used for frustum culling and queries, set from a projectionViewMatrix.

camera.frustum.fromMatrix4(camera.projectionViewMatrix)
if (camera.frustum.contains(mesh)) {
  // ...
}

PerspectiveCamera

A PerspectiveCamera calculates a perspective or non-linear projectionMatrix, where objects appear smaller by distance.

const fov = 75
const aspect = canvas.width / canvas.height
const near = 0.1
const far = 1000
const camera = new PerspectiveCamera(fov, aspect, near, far)

OrthographicCamera

An OrthographicCamera calculates an orthographic or linear projectionMatrix, where objects are unaffected by distance.

const near = 0.1
const far = 1000
const left = -(canvas.width / 2)
const right = canvas.width / 2
const bottom = -(canvas.height / 2)
const top = canvas.height / 2
const camera = new OrthographicCamera(near, far, left, right, bottom, top)

Rendering

Four supports WebGL 2 and WebGPU with WebGLRenderer and WebGPURenderer, respectively, and implements a shared API for rendering and compute.

const renderer = new WebGLRenderer()
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.canvas)

//

const renderer = new WebGPURenderer()
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.canvas)

Instancing

Four instances by default, which better aligns with WebGPU. Instanced rendering rasterizes multiple vertex primitives with the same shader interface to render multiple meshes at the cost of one.

You can specify the number of instances to render with Mesh.instances and add variance or control each instance with Attribute.divisor to specify a per-instance divisor. A divisor of one will be used by a single instance, and a divisor greater than one will be used by multiple instances.

Note: Attributes can only allocate primitive types in WebGPU (gpuweb/gpuweb#1652), so you must allocate and index storage or uniform buffers via the instance_index built-in for complex types like matrices.

Show WebGL example
const geometry = new Geometry({
  position: { size: 3, data: new Float32Array([...]) },
  instanceMatrix: { divisor: 1, size: 16, data: new Float32Array([...]) }
})

const material = new Material({
  vertex: /* glsl */`#version 300 es
    uniform mat4 projectionMatrix;
    uniform mat4 modelViewMatrix;
    in mat4 instanceMatrix;
    in vec3 position;
    void main() {
      gl_Position = projectionMatrix * modelViewMatrix * instanceMatrix * vec4(position, 1);
    }
  `,
  fragment: /* glsl */`#version 300 es
    out lowp vec4 color;
    void main() {
      color = vec4(1, 0, 0, 1);
    }
  `
})

const mesh = new Mesh(geometry, material)
mesh.instances = 2
Show WebGPU example
const geometry = new Geometry({
  position: { size: 3, data: new Float32Array([...]) },
})

const material = new Material({
  uniforms: {
    instanceMatrix: new Float32Array([...]),
  },
  vertex: /* wgsl */ `
    struct Uniforms {
      projectionMatrix: mat4x4<f32>,
      modelViewMatrix: mat4x4<f32>,
      instanceMatrix: array<mat4x4<f32>, 2>,
    };
    @group(0) @binding(0) var<uniform> uniforms: Uniforms;

    @vertex
    fn main(
      @builtin(instance_index) instanceID: u32,
      @location(0) position: vec3<f32>,
    ) -> @builtin(position) vec4<f32> {
      return uniforms.projectionMatrix * uniforms.modelViewMatrix * uniforms.instanceMatrix[instanceID] * vec4(position, 1.0);
    }
  `,
  fragment: /* wgsl */ `
    @fragment
    fn main() -> @location(0) vec4<f32> {
      return vec4(1, 0, 0, 1);
    }
  `,
})

const mesh = new Mesh(geometry, material)
mesh.instances = 2

Compute

Four supports compute for both WebGL and WebGPU via transform feedback and compute pipelines, respectively. This can be used in lieu of pixel shaders to write directly to buffer storage without any CPU reads/writes to textures. Useful for high precision compute or large simulations where texture memory is limited.

The following populates geometry buffers on the GPU, computing a fullscreen triangle geometry:

Show WebGL example
const geometry = new Geometry({
  position: { size: 2, data: new Float32Array(6) },
  uv: { size: 2, data: new Float32Array(6) },
})
const computeMaterial = new Material({
  compute: /* glsl */ `#version 300 es
    out vec2 uv;
    out vec2 position;

    void main() {
      uv = vec2(gl_VertexID << 1 & 2, gl_VertexID & 2);
      position = uv * 2.0 - 1.0;
    }
  `,
})
const mesh = new Mesh(geometry, computeMaterial)

renderer.compute(mesh)
Show WebGPU example
const geometry = new Geometry({
  position: { size: 2, data: new Float32Array(6) },
  uv: { size: 2, data: new Float32Array(6) },
})
const computeMaterial = new Material({
  compute: /* wgsl */ `
    @group(0) @binding(0)
    var<storage, read_write> position: array<vec2<f32>>;

    @group(0) @binding(1)
    var<storage, read_write> uv: array<vec2<f32>>;

    @compute @workgroup_size(64)
    fn main(@builtin(local_invocation_index) i: u32) {
      uv[i] = vec2<f32>(vec2((i << 1) & 2, i & 2));
      position[i] = uv[i] * 2 - 1;
    }
  `,
})
const mesh = new Mesh(geometry, computeMaterial)

renderer.compute(mesh)