pmndrs / lamina

🍰 An extensible, layer based shader material for ThreeJS
MIT License
1.02k stars 42 forks source link

Please create an example for TS Custom Layers #19

Open artokun opened 2 years ago

artokun commented 2 years ago

It took me a few hours to figure it out using the src

// src/layers/CustomLayer/index.tsx

import { forwardRef } from "react";
import { Abstract } from "lamina/vanilla";
import { LayerProps } from "lamina/types";
import { Node, extend } from "@react-three/fiber";
import { getNonUniformArgs } from "../../utils";
import vertexShader from "./vertex.glsl";
import fragmentShader from "./fragment.glsl";

interface CustomLayerProps extends LayerProps {
  color?: THREE.ColorRepresentation | THREE.Color;
  alpha?: number;
}

class CustomLayer extends Abstract {
  // Define stuff as static properties!

  // Uniforms: Must begin with prefix "u_".
  // Assign them their default value.
  // Any uniforms here will automatically be set as properties on the class as setters and getters.
  // There are setters and getters will update the underlying uniforms.
  static u_color = "red"; // Can be accessed as CustomExampleLayer.color
  static u_alpha = 1; // Can be accessed as CustomExampleLayer.alpha

  // Define your fragment shader just like you already do!
  // Only difference is, you must return the final color of this layer
  static fragmentShader = fragmentShader;

  // Optionally Define a vertex shader!
  // Same rules as fragment shaders, except no blend modes.
  // Return a non-transformed vec3 position.
  static vertexShader = vertexShader;

  constructor(props?: CustomLayerProps) {
    super(CustomLayer, {
      name: "CustomLayer",
      ...props,
    });
  }
}

declare global {
  namespace JSX {
    interface IntrinsicElements {
      customLayer_: Node<CustomLayer, typeof CustomLayer>;
    }
  }
}

extend({ CustomLayer_: CustomLayer });

const CustomLayerComponent = forwardRef<CustomLayer, CustomLayerProps>((props, ref) => {
  return <customLayer_ ref={ref} args={getNonUniformArgs(props)} {...props} />;
}) as React.ForwardRefExoticComponent<
  CustomLayerProps & React.RefAttributes<CustomLayer>
>;

export default CustomLayerComponent;
// src/App

import { useRef } from "react";
import { DebugLayerMaterial } from "lamina";
import { Mesh } from "three";
import { Plane } from "@react-three/drei";
import { GroupProps } from "@react-three/fiber";
import CustomLayer, { CustomLayerProps } from "./layers/CustomLayer";

export default function App({
  customLayerProps,
  ...props
}: GroupProps & { customLayerProps?: CustomLayerProps }) {
  const ref = useRef<Mesh>(null!);

  return (
    <group {...props}>
      <Plane
        ref={ref}
        args={[10, 10, 100, 100]}
        rotation={[Math.PI / -2, 0, 0]}
      >
        <DebugLayerMaterial
          color={"#ffffff"}
          lighting={"physical"} //
          transmission={1}
          roughness={0.1}
          thickness={3}
        >
          <CustomLayer color="green" alpha="0.5" />
        </DebugLayerMaterial>
      </Plane>
    </group>
  );
}

If you notice I import the .glsl directly. You can do this without ejecting by using the @rescripts/cli library. It gives you better syntax highlighting

// .rescripts.js

const { edit, getPaths } = require("@rescripts/utilities");

const predicate = (valueToTest) => {
  return valueToTest.oneOf;
};

const transform = (match) => ({
  ...match,
  oneOf: [
    // Need to add as second-to-last to avoid being intercepted by the file-loader in CRA
    ...match.oneOf.slice(0, -1),
    {
      test: /\.(glsl|frag|vert)$/,
      exclude: [/node_modules/],
      use: ["raw-loader", "glslify-loader"],
    },
    ...match.oneOf.slice(-1),
  ],
});

function rescriptGlslifyPlugin() {
  return (config) => {
    const matchingPaths = getPaths(predicate, config);
    return edit(transform, matchingPaths, config);
  };
}

module.exports = [[rescriptGlslifyPlugin]];

and the packlage.json to make the imports work

// package.json

{
  ...
  "dependencies": {
    "glslify": "^7.1.1",
    "glslify-loader": "^2.0.0",
    "lamina": "^1.1.8",
    "raw-loader": "^4.0.2",
    ...
  },
  "scripts": {
    "start": "rescripts start",
    "build": "rescripts build",
    "test": "rescripts test",
    "eject": "rescripts eject"
  },
  ...
  "devDependencies": {
    "@rescripts/cli": "^0.0.16",
    "@rescripts/rescript-env": "^0.0.14",
    "@rescripts/utilities": "^0.0.8"
  }
}

now you can load your glsl files

// src/layers/CustomLayer/vertexShader.glsl

varying vec3 v_Position;

void main() {
  v_Position = position;
  return position * 2.;
}
// src/layers/CustomLayer/fragmentShader.glsl

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;
}
FarazzShaikh commented 2 years ago

The TS example is a good idea, I will replace the JS examples with TS actually as it’s easy to derive JS from TS but not the other way around.

as for the GLSL import, that’s great that it works but I don’t think it’s needed in the library itself. It’s a user land thing. Besides you can use the /*glsl*/ decorator to get syntax highlighting for inline template strings.

artokun commented 2 years ago

Oh? didn't know about the decorator, i'll give that a try!

ghost commented 1 year ago

Hey, I am quite struggling with implementing your solution as I am not the best with Typescript but still trying to enforce it as it's a good practice. If you don't mind sharing - what exactly does your utility method getNonUniformArgs do?

Also, how how would I implement useRef directly on material layer itself - so I can change the props via useFrame? I tried following but I am not getting anywhere.

const materialRef = useRef<CustomLayerProps>();

useFrame((state) =>
{
    const { clock } = state;
    materialRef .current!.time = 0.4 * clock.getElapsedTime();
});

return (
    <mesh position={[2, 0, 0]}>
        <sphereGeometry args={[4.25, 64, 64]} />
        <LayerMaterial color="#434" lighting="standard">
            <CustomLayer ref={materialRef} intensity={0.15} />
        </LayerMaterial>
    </mesh>
);