lamina

repository·main·Indexed 22 days ago

https://github.com/pmndrs/lamina

An extensible, layer-based shader material system for Three.js (version 1.2.2) that allows for declarative stacking and blending of visual effects such as noise, gradients, and textures. Built on top of three-custom-shader-material (CSM), it provides a LayerMaterial component for React (@react-three/fiber) and a vanilla Three.js implementation. It includes built-in fragment layers (Color, Depth, Fresnel, Gradient, Matcap, Noise, Normal, Texture) and vertex layers (Displace), as well as a DebugLayerMaterial for real-time parameter tweaking.

Tokens
7.5K
Snippets
25
Records
42
Agent score
78%

What's inside lamina

  1. What is Lamina?

    main

    Lamina is an extensible, layer-based shader material system for Three.js. It allows developers to create complex materials by stacking and blending different visual effects (layers) declaratively. This approach is inspired by tools like Spline, making it easy to compose materials like gradients, noise, and textures without writing complex shader boilerplate from scratch.

    Note: As of April 5, 2023, Lamina is archived. It is built on top of three-custom-shader-material (CSM). While it remains usable, it is no longer actively maintained.

  2. Use the `onParse` event for advanced custom layers

    main

    The onParse event is a callback provided to the Abstract constructor that runs after a layer's shader and uniforms have been parsed. It allows you to inject custom functionality that goes beyond the standard layer extension syntax, such as modifying shader code dynamically or updating the debugger (Leva) schema.

    Common use cases include:

    • Injecting specific shader chunks based on a non-uniform parameter.
    • Adding custom controls to the debugger schema.

    Note: Non-uniform parameters (parameters that change the shader structure rather than just being passed as uniforms) must be passed to the layer constructor via the args prop in React.

    class CustomLayer extends Abstract {
      // ... shader definitions ...
    
      mapping: 'uv' | 'world' = 'uv';
    
      constructor(props) {
        super(
          CustomLayer,
          {
            name: 'CustomLayer',
            ...props,
          },
          (self: CustomLayer) => {
            // 1. Add to Leva (debugger) schema
            self.schema.push({
              value: self.mapping,
              label: 'mapping',
              options: ['uv', 'world'],
            })
    
            // 2. Inject shader chunk based on selected mapping
            const mapping = CustomLayer.getMapping(self.mapping)
            self.fragmentShader = self.fragmentShader.replace('lamina_mapping_template', mapping)
          }
        )
      }
    }

    In React, use the args prop to pass these non-uniform values:

    <LayerMaterial>
      <customLayer
        ref={ref}
        color="green"
        alpha={0.5}
        args={[mapping]} // Non-uniform params must use `args`
      />
    </LayerMaterial>
  3. Use LayerMaterial in Vanilla Three.js

    main

    For vanilla Three.js projects, import from lamina/vanilla. Each layer is instantiated as a class and passed into the layers array of the LayerMaterial constructor.

    Important: To match the color behavior of the React version, you must convert colors to Linear encoding using .convertSRGBToLinear().

    import { LayerMaterial, Depth } from 'lamina/vanilla'
    
    const geometry = new THREE.SphereGeometry(1, 128, 64)
    const material = new LayerMaterial({
      color: '#d9d9d9',
      lighting: 'physical',
      transmission: 1,
      layers: [
        new Depth({
          colorA: new THREE.Color('#002f4b').convertSRGBToLinear(),
          colorB: new THREE.Color('#f2fdff').convertSRGBToLinear(),
          alpha: 0.5,
          mode: 'multiply',
          near: 0,
          far: 2,
          origin: new THREE.Vector3(1, 1, 1),
        }),
      ],
    })
    
    const mesh = new THREE.Mesh(geometry, material)
  4. Use custom layers in React-three-fiber

    main

    Custom layers are Vanilla compatible by default. To use them in React, you must use the extend function from @react-three/fiber to register the layer. Once extended, you can use the layer as a JSX element (in camelCase) and use a ref to animate uniforms imperatively.

    import { extend } from "@react-three/fiber"
    
    extend({ CustomLayer })
    
    // ...
    const ref = useRef();
    
    // Animate uniforms using a ref.
    useFrame(({ clock }) => {
      ref.current.color.setRGB(
        Math.sin(clock.elapsedTime),
        Math.cos(clock.elapsedTime),
        Math.sin(clock.elapsedTime),
      )
    })
    
    <LayerMaterial>
      <customLayer
        ref={ref}     // Imperative instance of CustomLayer. Can be used to animate unifroms
        color="green" // Uniforms can be set directly
        alpha={0.5}
      />
    </LayerMaterial>
  5. Use the Debugger to tweak materials

    main

    To visually tweak layer parameters in real-time, replace LayerMaterial with DebugLayerMaterial. This enables a UI that allows you to adjust properties and then copy the resulting JSX code.

    <DebugLayerMaterial color="#ffffff">
      <Depth
        colorA="#810000"
        colorB="#ffd0d0"
        alpha={0.5}
        mode="multiply"
        near={0}
        far={2}
        origin={[1, 1, 1]}
      />
    </DebugLayerMaterial>
  6. Explore Lamina examples

    main

    Lamina provides several example implementations to demonstrate its usage across different environments and complexity levels:

    • Layer Material: Demonstrates basic usage of Lamina within a React environment.
    • Vanilla: Demonstrates basic usage of Lamina in a standard Vanilla ThreeJS environment.
    • Configurator: A specialized tool for creating materials and testing new layers via a UI.
    • Complex: An advanced implementation showcasing Lamina integrated with lighting, instancing, physics, and base material properties.
  7. Use LayerMaterial in React

    main

    In a React environment (typically with @react-three/fiber), you use the LayerMaterial component to define your material. You pass layers as children to the LayerMaterial component. The lighting prop determines the underlying Three.js material type (e.g., 'physical', 'phong', 'standard'), and the material will support all standard properties for that type.

    import { LayerMaterial, Depth } from 'lamina'
    
    function GradientSphere() {
      return (
        <Sphere>
          <LayerMaterial
            color="#ffffff"
            lighting="physical"
            transmission={1}
          >
            <Depth
              colorA="#810000"
              colorB="#ffd0d0"
              alpha={0.5}
              mode="multiply"
              near={0}
              far={2}
              origin={[1, 1, 1]}
            />
          </LayerMaterial>
        </Sphere>
      )
    }
  8. Write a custom layer by extending Abstract

    main

    Custom layers are created by extending the Abstract class. Each layer acts as an isolated shader program that returns a vec4 color.

    Key Rules:

    1. Uniforms: Must start with the prefix u_. They are automatically exposed as class properties (setters/getters).
    2. Varyings: Must start with the prefix v_.
    3. Local Variables: Must start with the prefix f_.
    4. Vertex Shader: Must return a vec3 position. You do not need to set gl_Position; Lamina handles the projection automatically.
    5. Noise Functions: You can use built-in noise functions directly: lamina_noise_perlin(), lamina_noise_simplex(), lamina_noise_worley(), lamina_noise_white(), lamina_noise_swirl().
    import { Abstract } from 'lamina/vanilla'
    
    class CustomLayer extends Abstract {
      static u_color = 'red'
      static u_alpha = 1
    
      static fragmentShader = `   
        uniform vec3 u_color;
        uniform float u_alpha;
        varying vec3 v_Position;
    
        vec4 main() {
          vec4 f_color = vec4(u_color, u_alpha);
          return f_color;
        }
      `
    
      static vertexShader = `   
        varying vec3 v_Position;
    
        void main() {
          v_Position = position;
          return position * 2.;
        }
      `
    
      constructor(props) {
        super(CustomLayer, {
          name: 'CustomLayer',
          ...props,
        })
      }
    }
    import { Abstract } from 'lamina/vanilla'
    
    // Extend the Abstract layer
    class CustomLayer extends Abstract {
      // Define stuff as static properties!
    
      // Uniforms: Must begin with prefix "u_".
      // Assign them their default value.
      // Any unifroms here will automatically be set as properties on the class as setters and getters.
      // There setters and getters will update the underlying unifrom.
      static u_color = 'red' // Can be accessed as CustomLayer.color
      static u_alpha = 1 // Can be accessed as CustomLayer.alpha
    
      // Define your fragment shader just like you already do!
      // Only difference is, you must return the final color of this layer
      static fragmentShader = `   
        uniform vec3 u_color;
        uniform float u_alpha;
    
        // Varyings must be prefixed with "v_"
        varying vec3 v_Position;
    
        vec4 main() {
          // Local variables must be prefixed with "f_"
          vec4 f_color = vec4(u_color, u_alpha);
          return f_color;
        }
      `
    
      // Optionally Define a vertex shader!
      // Same rules as fragment shaders, except no blend modes.
      // Return a non-projected vec3 position.
      static vertexShader = `   
        // Varyings must be prefixed with "v_"
        varying vec3 v_Position;
    
        void main() {
          v_Position = position;
          return position * 2.;
        }
      `
    
      constructor(props) {
        // You MUST call `super` with the current constructor as the first argument.
        // Second argument is optional and provides non-uniform parameters like blend mode, name and visibility.
        super(CustomLayer, {
          name: 'CustomLayer',
          ...props,
        })
      }
    }