lamina
Version:
š° An extensable, layer based shader material for ThreeJS.
483 lines (375 loc) ⢠17.3 kB
Markdown
<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]` |