UNPKG

lamina

Version:

šŸ° An extensable, layer based shader material for ThreeJS.

483 lines (375 loc) • 17.3 kB
<br /> <h1 align="center">lamina</h1> <h3 align="center">šŸ° An extensible, layer based shader material for ThreeJS</h3> <br> <p align="center"> <a href="https://www.npmjs.com/package/lamina" target="_blank"> <img src="https://img.shields.io/npm/v/lamina.svg?style=flat&colorA=000000&colorB=000000" /> </a> <a href="https://www.npmjs.com/package/lamina" target="_blank"> <img src="https://img.shields.io/npm/dm/lamina.svg?style=flat&colorA=000000&colorB=000000" /> </a> <a href="https://twitter.com/pmndrs" target="_blank"> <img src="https://img.shields.io/twitter/follow/pmndrs?label=%40pmndrs&style=flat&colorA=000000&colorB=000000&logo=twitter&logoColor=000000" alt="Chat on Twitter"> </a> <a href="https://discord.gg/ZZjjNvJ" target="_blank"> <img src="https://img.shields.io/discord/740090768164651008?style=flat&colorA=000000&colorB=000000&label=discord&logo=discord&logoColor=000000" alt="Chat on Twitter"> </a> </p> <br /> <p align="center"> <a href="https://codesandbox.io/embed/github/pmndrs/lamina/tree/main/examples/complex-materials" target="_blank"><img width="400" src="https://raw.githubusercontent.com/pmndrs/lamina/main/examples/complex-materials/thumbnail.png" /></a> <a href="https://codesandbox.io/embed/github/pmndrs/lamina/tree/main/examples/layer-materials" target="_blank"><img width="400" src="https://github.com/pmndrs/lamina/blob/main/assets/lamina.png?raw=true" /></a> </p> <p align="middle"> <i>These demos are real, you can click them! They contain the full code, too. šŸ“¦</i> More examples <a href="./examples">here</a> </p> <br /> `lamina` lets you create materials with a declarative, system of layers. Layers make it incredibly easy to stack and blend effects. This approach was first made popular by the [Spline team](https://spline.design/). ```jsx 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> ) } ``` <details> <summary>Show Vanilla example</summary> Lamina can be used with vanilla Three.js. Each layer is just a class. ```js 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: '#002f4b', colorB: '#f2fdff', alpha: 0.5, mode: 'multiply', near: 0, far: 2, origin: new THREE.Vector3(1, 1, 1), }), ], }) const mesh = new THREE.Mesh(geometry, material) ``` Note: To match the colors of the react example, you must convert all colors to Linear encoding like so: ```js 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), }), ``` </details> ## Layers ### `LayerMaterial` `LayerMaterial` can take in the following parameters: | Prop | Type | Default | | ---------- | ----------------------------------------------------------------------- | ----------------- | | `name` | `string` | `"LayerMaterial"` | | `color` | `THREE.ColorRepresentation \| THREE.Color` | `"white"` | | `alpha` | `number` | `1` | | `lighting` | `'phong' \| 'physical' \| 'toon' \| 'basic' \| 'lambert' \| 'standard'` | `'basic'` | | `layers`\* | `Abstract[]` | `[]` | The `lighting` prop controls the shading that is applied on the material. The material then accepts all the material properties supported by ThreeJS of the material type specified by the `lighting` prop. \* Note: the `layers` prop is only available on the `LayerMaterial` class, not the component. <strong>Pass in layers as children in React.</strong> ### Built-in layers Here are the layers that lamina currently provides | Name | Function | | ----------------------- | -------------------------------------- | | Fragment Layers | | | [`Color`](#color) | Flat color. | | [`Depth`](#depth) | Depth based gradient. | | [`Fresnel`](#fresnel) | Fresnel shading (strip or rim-lights). | | [`Gradient`](#gradient) | Linear gradient. | | [`Matcap`](#matcap) | Load in a Matcap. | | [`Noise`](#noise) | White, perlin or simplex noise . | | [`Normal`](#normal) | Visualize vertex normals. | | [`Texture`](#texture) | Image texture. | | Vertex Layers | | | [`Displace`](#displace) | Displace vertices using. noise | See the section for each layer for the options on it. ### Debugger Lamina comes with a handy debugger that lets you tweek parameters till you're satisfied with the result! Then, just copy the JSX and paste! Replace `LayerMaterial` with `DebugLayerMaterial` to enable it. ```jsx <DebugLayerMaterial color="#ffffff"> <Depth colorA="#810000" // colorB="#ffd0d0" alpha={0.5} mode="multiply" near={0} far={2} origin={[1, 1, 1]} /> </DebugLayerMaterial> ``` Any custom layers are automatically compatible with the debugger. However, for advanced inputs, see the [Advanced Usage](#advanced-usage) section. ### Writing your own layers You can write your own layers by extending the `Abstract` class. The concept is simple: > Each layer can be treated as an isolated shader program that produces a `vec4` color. The color of each layer will be blended together using the specified blend mode. A list of all available blend modes can be found [here](#blendmode) ```ts 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 by "v_" varying vec3 v_Position; vec4 main() { // Local variables must be prefixed by "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 by "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, }) } } ``` šŸ‘‰ Note: The vertex shader must return a vec3. You do not need to set `gl_Position` or transform the model view. lamina will handle this automatically down the chain. šŸ‘‰ Note: You can use lamina's noise functions inside of your own layer without any additional imports: `lamina_noise_perlin()`, `lamina_noise_simplex()`, `lamina_noise_worley()`, `lamina_noise_white()`, `lamina_noise_swirl()`. If you need a specialized or advance use-case, see the [Advanced Usage](#advanced-usage) section ### Using your own layers <strong>Custom layers are Vanilla compatible by default.</strong> To use them with React-three-fiber, you must use the `extend` function to add the layer to your component library! ```jsx 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> ``` ## Advanced Usage For more advanced custom layers, lamina provides the `onParse` event. > This event runs after the layer's shader and uniforms are parsed. This means you can use it to inject functionality that isn't by the basic layer extension syntax. Here is a common use case - Adding non-uniform options to layers that directly sub out shader code. ```ts class CustomLayer extends Abstract { static u_color = 'red' static u_alpha = 1 static vertexShader = `...` static fragmentShader = ` // ... float f_dist = lamina_mapping_template; // Temp value, will be used to inject code later on. // ... ` // Get some shader code based off mapping parameter static getMapping(mapping) { switch (mapping) { default: case 'uv': return `some_shader_code` case 'world': return `some_other_shader_code` } } // Set non-uniform defaults. mapping: 'uv' | 'world' = 'uv' // Non unifrom params must be passed to the constructor constructor(props) { super( CustomLayer, { name: 'CustomLayer', ...props, }, // This is onParse callback (self: CustomLayer) => { // Add to Leva (debugger) schema. // This will create a dropdown select component on the debugger. self.schema.push({ value: self.mapping, label: 'mapping', options: ['uv', 'world'], }) // Get shader chunk based off selected mapping value const mapping = CustomLayer.getMapping(self.mapping) // Inject shader chunk in current layer's shader code self.fragmentShader = self.fragmentShader.replace('lamina_mapping_template', mapping) } ) } } ``` In react... ```jsx // ... <LayerMaterial> <customLayer ref={ref} color="green" alpha={0.5} args={[mapping]} // Non unifrom params must be passed to the constructor using `args` /> </LayerMaterial> ``` ## Layers Every layer has these props in common. | Prop | Type | Default | | --------- | ------------------------- | ------------------------- | | `mode` | [`BlendMode`](#blendmode) | `"normal"` | | `name` | `string` | `<this.constructor.name>` | | `visible` | `boolean` | `true` | All props are optional. ### `Color` Flat color. | Prop | Type | Default | | ------- | ------------------------------------------ | ------- | | `color` | `THREE.ColorRepresentation \| THREE.Color` | `"red"` | | `alpha` | `number` | `1` | ### `Normal` Visualize vertex normals | Prop | Type | Default | | ----------- | ----------------------------------------- | ----------- | | `direction` | `THREE.Vector3 \| [number,number,number]` | `[0, 0, 0]` | | `alpha` | `number` | `1` | ### `Depth` Depth based gradient. Colors are lerp-ed based on `mapping` props which may have the following values: - `vector`: distance from `origin` to fragment's world position. - `camera`: distance from camera to fragment's world position. - `world`: distance from fragment to center (0, 0, 0). | Prop | Type | Default | | --------- | ------------------------------------------ | ----------- | | `colorA` | `THREE.ColorRepresentation \| THREE.Color` | `"white"` | | `colorB` | `THREE.ColorRepresentation \| THREE.Color` | `"black"` | | `alpha` | `number` | `1` | | `near` | `number` | `2` | | `far` | `number` | `10` | | `origin` | `THREE.Vector3 \| [number,number,number]` | `[0, 0, 0]` | | `mapping` | `"vector" \| "camera" \| "world"` | `"vector"` | ### `Fresnel` Fresnel shading. | Prop | Type | Default | | ----------- | ------------------------------------------ | --------- | | `color` | `THREE.ColorRepresentation \| THREE.Color` | `"white"` | | `alpha` | `number` | `1` | | `power` | `number` | `0` | | `intensity` | `number` | `1` | | `bias` | `number` | `2` | ### `Gradient` Linear gradient based off distance from `start` to `end` in a specified `axes`. `start` and `end` are points on the `axes` selected. The distance between `start` and `end` is used to lerp the colors. | Prop | Type | Default | | ---------- | ------------------------------------------ | --------- | | `colorA` | `THREE.ColorRepresentation \| THREE.Color` | `"white"` | | `colorB` | `THREE.ColorRepresentation \| THREE.Color` | `"black"` | | `alpha` | `number` | `1` | | `contrast` | `number` | `1` | | `start` | `number` | `1` | | `end` | `number` | `-1` | | `axes` | `"x" \| "y" \| "z"` | `"x"` | | `mapping` | `"local" \| "world" \| "uv"` | `"local"` | ### `Noise` Various noise functions. | Prop | Type | Default | | --------- | ------------------------------------------- | ----------- | | `colorA` | `THREE.ColorRepresentation \| THREE.Color` | `"white"` | | `colorB` | `THREE.ColorRepresentation \| THREE.Color` | `"black"` | | `colorC` | `THREE.ColorRepresentation \| THREE.Color` | `"white"` | | `colorD` | `THREE.ColorRepresentation \| THREE.Color` | `"black"` | | `alpha` | `number` | `1` | | `scale` | `number` | `1` | | `offset` | `THREE.Vector3 \| [number, number, number]` | `[0, 0, 0]` | | `mapping` | `"local" \| "world" \| "uv"` | `"local"` | | `type` | `"perlin' \| "simplex" \| "cell" \| "curl"` | `"perlin"` | ### `Matcap` Set a Matcap texture. | Prop | Type | Default | | ------- | --------------- | ----------- | | `map` | `THREE.Texture` | `undefined` | | `alpha` | `number` | `1` | ### `Texture` Set a texture. | Prop | Type | Default | | ------- | --------------- | ----------- | | `map` | `THREE.Texture` | `undefined` | | `alpha` | `number` | `1` | ### `BlendMode` Blend modes currently available in lamina | `normal` | `divide` | | ---------- | ----------- | | `add` | `overlay` | | `subtract` | `screen` | | `multiply` | `softlight` | | `lighten` | `reflect` | | `darken` | `negation` | ## Vertex layers Layers that affect the vertex shader ### `Displace` Displace vertices with various noise. | Prop | Type | Default | | ---------- | ------------------------------------------- | ----------- | | `strength` | `number` | `1` | | `scale` | `number` | `1` | | `mapping` | `"local" \| "world" \| "uv"` | `"local"` | | `type` | `"perlin' \| "simplex" \| "cell" \| "curl"` | `"perlin"` | | `offset` | `THREE.Vector3 \| [number,number,number]` | `[0, 0, 0]` |