Giter Club home page Giter Club logo

react-ogl's Introduction

Size Version Downloads Twitter Discord

Build OGL scenes declaratively with re-usable, self-contained components that react to state, are readily interactive and can participate in React's ecosystem.

react-ogl is a barebones react renderer for OGL with an emphasis on minimalism and modularity. Its reconciler simply expresses JSX as OGL elements — <mesh /> becomes new OGL.Mesh(). This happens dynamically; there's no wrapper involved.

Table of Contents

Installation

# NPM
npm install ogl react-ogl

# Yarn
yarn add ogl react-ogl

# PNPM
pnpm add ogl react-ogl

Getting Started

react-ogl itself is super minimal, but you can use the familiar @react-three/fiber API with some helpers targeted for different platforms:

react-dom

This example uses create-react-app for the sake of simplicity, but you can use your own environment or create a codesandbox.

Show full example
# Create app
npx create-react-app my-app
cd my-app

# Install dependencies
npm install ogl react-ogl

# Start
npm run start

The following creates a re-usable component that has its own state, reacts to events and participates a shared render-loop.

import * as React from 'react'
import { useFrame, Canvas } from 'react-ogl'
import { createRoot } from 'react-dom/client'

function Box(props) {
  // This reference will give us direct access to the mesh
  const mesh = React.useRef()
  // Set up state for the hovered and active state
  const [hovered, setHover] = React.useState(false)
  const [active, setActive] = React.useState(false)
  // Subscribe this component to the render-loop, rotate the mesh every frame
  useFrame(() => (mesh.current.rotation.x += 0.01))
  // Return view, these are regular OGL elements expressed in JSX
  return (
    <mesh
      {...props}
      ref={mesh}
      scale={active ? 1.5 : 1}
      onClick={() => setActive((value) => !value)}
      onPointerOver={() => setHover(true)}
      onPointerOut={() => setHover(false)}
    >
      <box />
      <program
        vertex={`
          attribute vec3 position;
          attribute vec3 normal;

          uniform mat4 modelViewMatrix;
          uniform mat4 projectionMatrix;
          uniform mat3 normalMatrix;

          varying vec3 vNormal;

          void main() {
            vNormal = normalize(normalMatrix * normal);
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
          }
        `}
        fragment={`
          precision highp float;

          uniform vec3 uColor;
          varying vec3 vNormal;

          void main() {
            vec3 normal = normalize(vNormal);
            float lighting = dot(normal, normalize(vec3(10)));

            gl_FragColor.rgb = uColor + lighting * 0.1;
            gl_FragColor.a = 1.0;
          }
        `}
        uniforms={{ uColor: hovered ? 'hotpink' : 'orange' }}
      />
    </mesh>
  )
}

createRoot(document.getElementById('root')).render(
  <Canvas camera={{ position: [0, 0, 8] }}>
    <Box position={[-1.2, 0, 0]} />
    <Box position={[1.2, 0, 0]} />
  </Canvas>,
)

react-native

This example uses expo-cli but you can create a bare app with react-native CLI as well.

Show full example
# Create app and cd into it
npx expo init my-app # or npx react-native init my-app
cd my-app

# Automatically install & link expo modules
npx install-expo-modules@latest
expo install expo-gl

# Install NPM dependencies
npm install ogl react-ogl

# Start
npm run start

We'll also need to configure metro.config.js to look for the mjs file extension that OGL uses.

module.exports = {
  resolver: {
    resolverMainFields: ['browser', 'exports', 'main'], // https://github.com/facebook/metro/issues/670
    sourceExts: ['json', 'js', 'jsx', 'ts', 'tsx', 'cjs', 'mjs'],
    assetExts: ['glb', 'gltf', 'png', 'jpg'],
  },
}

Inside of our app, you can use the same API as web while running on native OpenGL ES — no webview needed.

import * as React from 'react'
import { useFrame, Canvas } from 'react-ogl'

function Box(props) {
  // This reference will give us direct access to the mesh
  const mesh = React.useRef()
  // Set up state for the hovered and active state
  const [hovered, setHover] = React.useState(false)
  const [active, setActive] = React.useState(false)
  // Subscribe this component to the render-loop, rotate the mesh every frame
  useFrame(() => (mesh.current.rotation.x += 0.01))
  // Return view, these are regular OGL elements expressed in JSX
  return (
    <mesh
      {...props}
      ref={mesh}
      scale={active ? 1.5 : 1}
      onClick={() => setActive((value) => !value)}
      onPointerOver={() => setHover(true)}
      onPointerOut={() => setHover(false)}
    >
      <box />
      <program
        vertex={`
          attribute vec3 position;
          attribute vec3 normal;

          uniform mat4 modelViewMatrix;
          uniform mat4 projectionMatrix;
          uniform mat3 normalMatrix;

          varying vec3 vNormal;

          void main() {
            vNormal = normalize(normalMatrix * normal);
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
          }
        `}
        fragment={`
          precision highp float;

          uniform vec3 uColor;
          varying vec3 vNormal;

          void main() {
            vec3 normal = normalize(vNormal);
            float lighting = dot(normal, normalize(vec3(10)));

            gl_FragColor.rgb = uColor + lighting * 0.1;
            gl_FragColor.a = 1.0;
          }
        `}
        uniforms={{ uColor: hovered ? 'hotpink' : 'orange' }}
      />
    </mesh>
  )
}

export default () => (
  <Canvas camera={{ position: [0, 0, 8] }}>
    <Box position={[-1.2, 0, 0]} />
    <Box position={[1.2, 0, 0]} />
  </Canvas>
)

Canvas

react-ogl provides an x-platform <Canvas /> component for web and native that serves as the entrypoint for your OGL scenes. It is a real DOM canvas or native view that accepts OGL elements as children (see creating elements).

Canvas Props

In addition to its platform props, <Canvas /> accepts a set of RenderProps to configure react-ogl and its rendering behavior.

<Canvas
  // Configures the react rendering mode. Defaults to `blocking`
  mode={"legacy" | "blocking" | "concurrent"}
  // Creates, sets, or configures the default renderer.
  // Accepts a callback, an external renderer, or renderer constructor params/properties.
  // Defaults to `new OGL.Renderer({ alpha: true, antialias: true, powerPreference: 'high-performance' })
  renderer={(canvas: HTMLCanvasElement) => new Renderer(canvas) | renderer | { ...params, ...props }}
  // Sets the renderer pixel ratio from a clamped range or value. Default is `[1, 2]`
  dpr={[min, max] | value}
  // Sets or configures the default camera.
  // Accepts an external camera, or camera constructor params/properties.
  // Defaults to `new OGL.Camera(gl, { fov: 75, near: 1, far: 1000 })` with position-z `5`
  camera={camera | { ...params, ...props }}
  // Enables orthographic projection when using OGL's built-in camera. Default is `false`
  orthographic={true | false}
  // Defaults to `always`
  frameloop={'always' | 'never'}
  // An optional callback invoked after canvas creation and before commit.
  onCreated={(state: RootState) => void}
  // Optionally configures custom events. Defaults to built-in events exported as `events`
  events={EventManager | undefined}
>
  {/* Accepts OGL elements as children */}
  <transform />
</Canvas>

// e.g.

<Canvas
  renderer={{ alpha: true }}
  camera={{ fov: 45, position: [0, 1.3, 3] }}
  onCreated={(state) => void state.gl.clearColor(1, 1, 1, 0)}
>
  <transform />
</Canvas>

Custom Canvas

A react 18 style createRoot API creates an imperative Root with the same options as <Canvas />, but you're responsible for updating it and configuring things like events (see events). This root attaches to an HTMLCanvasElement and renders OGL elements into a scene. Useful for creating an entrypoint with react-ogl and for headless contexts like a server or testing (see testing).

import { createRoot, events } from 'react-ogl'

const canvas = document.querySelector('canvas')
const root = createRoot(canvas, { events })
root.render(
  <mesh>
    <box />
    <normalProgram />
  </mesh>,
)
root.unmount()

createRoot can also be used to create a custom <Canvas />. The following constructs a custom canvas that renders its children into react-ogl.

import * as React from 'react'
import { createRoot, events } from 'react-ogl'

function CustomCanvas({ children }) {
  // Init root from canvas
  const [canvas, setCanvas] = React.useState()
  const root = React.useMemo(() => canvas && createRoot(canvas, { events }), [canvas])
  // Render children as a render-effect
  root?.render(children)
  // Cleanup on unmount
  React.useEffect(() => () => root?.unmount(), [root])
  // Use callback-style ref to access canvas in render
  return <canvas ref={setCanvas} />
}

Creating elements

react-ogl renders React components into an OGL scene-graph, and can be used on top of other renderers like react-dom and react-native that render for web and native, respectively. react-ogl components are defined by primitives or lower-case elements native to the OGL namespace (for custom elements, see extend).

function Component(props) {
  return (
    <mesh {...props}>
      <box />
      <normalProgram />
    </mesh>
  )
}

;<transform>
  <Component position={[1, 2, 3]} />
</transform>

These elements are not exported or implemented internally, but merely expressed as JSX — <mesh /> becomes new OGL.Mesh(). This happens dynamically; there's no wrapper involved.

JSX, properties, and shortcuts

react-ogl elements can be modified with JSX attributes or props. These are native to their underlying OGL objects.

<transform
  // Set non-atomic properties with literals
  // transform.visible = false
  visible={false}
  // Copy atomic properties with a stable reference (e.g. useMemo)
  // transform.rotation.copy(rotation)
  rotation={rotation}
  // Set atomic properties with declarative array syntax
  // transform.position.set(1, 2, 3)
  position={[1, 2, 3]}
  // Set scalars with shorthand for vector properties
  // transform.scale.set(1, 1, 1)
  scale={1}
  // Set CSS names or hex values as shorthand for color properties
  // transform.color.set('red')
  color="red"
  // Set sub properties with prop piercing or dash-case
  // transform.rotation.x = Math.PI / 2
  rotation-x={Math.PI / 2}
/>

Setting constructor arguments via args

An array of constructor arguments (args) can be passed to instantiate elements' underlying OGL objects. Changing args will reconstruct the object and update any associated refs.

// new OGL.Text({ font, text: 'Text' })
<text args={[{ font, text: 'Text' }]} />

Built-in elements that require a gl context such as <mesh />, <geometry />, or <program /> are marked as effectful (see extend) and do not require an OGLRenderingContext to be passed via args. They can be constructed mutably and manipulated via props:

<mesh>
  <box />
  <normalProgram />
</mesh>

<geometry /> and <program /> also accept attributes and shader sources as props, which are passed to their respective constructors. This does not affect other properties like drawRange or uniforms.

<mesh>
  <geometry
    position={{ size: 3, data: new Float32Array([-0.5, 0.5, 0, -0.5, -0.5, 0, 0.5, 0.5, 0, 0.5, -0.5, 0]) }}
    uv={{ size: 2, data: new Float32Array([0, 1, 1, 1, 0, 0, 1, 0]) }}
    index={{ data: new Uint16Array([0, 1, 2, 1, 3, 2]) }}
  />
  {/* prettier-ignore */}
  <program
    vertex={/* glsl */ `...`}
    fragment={/* glsl */ `...`}
    uniforms={{ uniform: value }}
  />
</mesh>

Attaching into element properties via attach

Some elements do not follow the traditional scene-graph and need to be added by other means. For this, the attach prop can describe where an element is added via a property or a callback to add & remove the element.

// Attaches into parent.property, parent.sub.property, and parent.array[0]
<parent>
  <element attach="property" />
  <element attach="sub-property" />
  <element attach="array-0" />
</parent>

// Attaches via parent#setProperty and parent#removeProperty
<parent>
  <element
    attach={(parent, self) => {
      parent.setProperty(self)
      return () => parent.removeProperty(self)
    }}
    // lambda version
    attach={(parent, self) => (parent.setProperty(self), () => parent.removeProperty(self))}
  />
</parent>

Elements who extend OGL.Geometry or OGL.Program will automatically attach via attach="geometry" and attach="program", respectively.

<mesh>
  <box />
  <normalProgram />
</mesh>

Creating custom elements via extend

react-ogl tracks an internal catalog of constructable elements, defaulting to the OGL namespace. This catalog can be expanded via extend to declaratively use custom elements as native elements.

import { extend } from 'react-ogl'

class CustomElement {}
extend({ CustomElement })

<customElement />

TypeScript users will need to extend the OGLElements interface to describe custom elements and their properties.

import { OGLElement, extend } from 'react-ogl'

class CustomElement {}

declare module 'react-ogl' {
  interface OGLElements {
    customElement: OGLElement<typeof CustomElement>
  }
}

extend({ CustomElement })

Effectful elements that require a gl context can mark themselves as effectful and receive a OGLRenderingContext when constructed, making args mutable and enabling the use of props. This is done for OGL built-in elements like <mesh />, <geometry />, and <program />.

import { extend } from 'react-ogl'

class CustomElement {
  constructor(gl) {
    this.gl = gl
  }
}
extend({ CustomElement }, true)

<customElement />

Adding third-party objects via <primitive />

Objects created outside of React (e.g. globally or from a loader) can be added to the scene-graph with the <primitive /> element via its object prop. Primitives can be interacted with like any other element, but will modify object and cannot make use of args.

import * as OGL from 'ogl'

const object = new OGL.Transform()

<primitive object={object} position={[1, 2, 3]} />

Hooks

react-ogl ships with hooks that allow you to tie or request information to your components. These are called within the body of <Canvas /> and contain imperative and possibly stateful code.

Root State

Each <Canvas /> or Root encapsulates its own OGL state via React context and a Zustand store, as defined by RootState. This can be accessed and modified with the onCreated canvas prop, and with hooks like useOGL.

interface RootState {
  // Zustand setter and getter for live state manipulation.
  // See https://github.com/pmndrs/zustand
  get(): RootState
  set(fn: (previous: RootState) => (next: Partial<RootState>)): void
  // Canvas layout information
  size: { width: number; height: number }
  // OGL scene internals
  renderer: OGL.Renderer
  gl: OGL.OGLRenderingContext
  scene: OGL.Transform
  camera: OGL.Camera
  // OGL perspective and frameloop preferences
  orthographic: boolean
  frameloop: 'always' | 'never'
  // Internal XR manager to enable WebXR features
  xr: XRManager
  // Frameloop internals for custom render loops
  priority: number
  subscribed: React.MutableRefObject<Subscription>[]
  subscribe: (refCallback: React.MutableRefObject<Subscription>, renderPriority?: number) => void
  unsubscribe: (refCallback: React.MutableRefObject<Subscription>, renderPriority?: number) => void
  // Optional canvas event manager and its state
  events?: EventManager
  mouse: OGL.Vec2
  raycaster: OGL.Raycast
  hovered: Map<number, Instance<OGL.Mesh>['object']>
}

Accessing state via useOGL

Returns the current canvas' RootState, describing react-ogl state and OGL rendering internals (see root state).

const { renderer, gl, scene, camera, ... } = useOGL()

To subscribe to a specific key, useOGL accepts a Zustand selector:

const renderer = useOGL((state) => state.renderer)

Frameloop subscriptions via useFrame

Subscribes an element into a shared render loop outside of React. useFrame subscriptions are provided a live RootState, the current RaF time in seconds, and a XRFrame when in a WebXR session. Note: useFrame subscriptions should never update React state but prefer external mechanisms like refs.

const object = React.useRef<OGL.Transform>(null!)

useFrame((state: RootState, time: number, frame?: XRFrame) => {
  object.current.rotation.x = time / 2000
  object.current.rotation.y = time / 1000
})

return <transform ref={object} />

Loading assets via useLoader

Synchronously loads and caches assets with a loader via suspense. Note: the caller component must be wrapped in React.Suspense.

const texture = useLoader(OGL.TextureLoader, '/path/to/image.jpg')

Multiple assets can be requested in parallel by passing an array:

const [texture1, texture2] = useLoader(OGL.TextureLoader, ['/path/to/image1.jpg', '/path/to/image2.jpg'])

Custom loaders can be implemented via the LoaderRepresentation signature:

class CustomLoader {
  async load(gl: OGLRenderingContext, url: string): Promise<void> {}
}

const result = useLoader(CustomLoader, '/path/to/resource')

Object traversal via useGraph

Traverses an OGL.Transform for unique meshes and programs, returning an ObjectMap.

const { nodes, programs } = useGraph(object)

<mesh geometry={nodes['Foo'].geometry} program={programs['Bar']} />

Transient updates via useStore

Returns the internal Zustand store. Useful for transient updates outside of React (e.g. multiplayer/networking).

const store = useStore()
React.useLayoutEffect(() => store.subscribe(state => ...), [store])

Access internals via useInstanceHandle

Exposes an object's react-internal Instance state from a ref.

Note: this is an escape hatch to react-internal fields. Expect this to change significantly between versions.

const ref = React.useRef<OGL.Transform>()
const instance = useInstanceHandle(ref)

React.useLayoutEffect(() => {
  instance.parent.object.foo()
}, [])

<transform ref={ref} />

Events

react-ogl implements mesh pointer-events with OGL.Raycast that can be tapped into via the following props:

<mesh
  // Fired when the mesh is clicked or tapped.
  onClick={(event: OGLEvent<MouseEvent>) => ...}
  // Fired when a pointer becomes inactive over the mesh.
  onPointerUp={(event: OGLEvent<PointerEvent>) => ...}
  // Fired when a pointer becomes active over the mesh.
  onPointerDown={(event: OGLEvent<PointerEvent>) => ...}
  // Fired when a pointer moves over the mesh.
  onPointerMove={(event: OGLEvent<PointerEvent>) => ...}
  // Fired when a pointer enters the mesh's bounds.
  onPointerOver={(event: OGLEvent<PointerEvent>) => ...}
  // Fired when a pointer leaves the mesh's bounds.
  onPointerOut={(event: OGLEvent<PointerEvent>) => ...}
/>

Events contain the original event as nativeEvent and properties from OGL.RaycastHit.

{
  nativeEvent: PointerEvent | MouseEvent,
  localPoint: Vec3,
  distance: number,
  point: Vec3,
  faceNormal: Vec3,
  localFaceNormal: Vec3,
  uv: Vec2,
  localNormal: Vec3,
  normal: Vec3,
}

Custom events

Custom events can be implemented per the EventManager interface and passed via the events Canvas prop.

const events: EventManager = {
  connected: false,
  connect(canvas: HTMLCanvasElement, state: RootState) {
    // Bind handlers
  },
  disconnect(canvas: HTMLCanvasElement, state: RootState) {
    // Cleanup
  },
}

<Canvas events={events}>
  <mesh onPointerMove={(event: OGLEvent<PointerEvent>) => console.log(event)}>
    <box />
    <normalProgram />
  </mesh>
</Canvas>
Full example
const events = {
  connected: false,
  connect(canvas: HTMLCanvasElement, state: RootState) {
    state.events.handlers = {
      pointermove(event: PointerEvent) {
        // Convert mouse coordinates
        state.mouse.x = (event.offsetX / state.size.width) * 2 - 1
        state.mouse.y = -(event.offsetY / state.size.height) * 2 + 1

        // Filter to interactive meshes
        const interactive: OGL.Mesh[] = []
        state.scene.traverse((node: OGL.Transform) => {
          // Mesh has registered events and a defined volume
          if (
            node instanceof OGL.Mesh &&
            (node as Instance<OGL.Mesh>['object']).__handlers &&
            node.geometry?.attributes?.position
          )
            interactive.push(node)
        })

        // Get elements that intersect with our pointer
        state.raycaster!.castMouse(state.camera, state.mouse)
        const intersects: OGL.Mesh[] = state.raycaster!.intersectMeshes(interactive)

        // Call mesh handlers
        for (const entry of intersects) {
          if ((entry as unknown as any).__handlers) {
            const object = entry as Instance<OGL.Mesh>['object']
            const handlers = object.__handlers

            const handlers = object.__handlers
            handlers?.onPointerMove?.({ ...object.hit, nativeEvent: event })
          }
        }
      },
    }

    // Bind
    state.events.connected = true
    for (const [name, handler] of Object.entries(state.events.handlers)) {
      canvas.addEventListener(name, handler)
    }
  },
  disconnect(canvas: HTMLCanvasElement, state: RootState) {
    // Unbind
    state.events.connected = false
    for (const [name, handler] of Object.entries(state.events.handlers)) {
      canvas.removeEventListener(name, handler)
    }
  },
}

<Canvas events={events}>
  <mesh onPointerMove={(event: OGLEvent<PointerEvent>) => console.log(event)}>
    <box />
    <normalProgram />
  </mesh>
</Canvas>

Portals

Portal children into a foreign OGL element via createPortal, which can modify children's RootState. This is particularly useful for postprocessing and complex render effects.

function Component {
  // scene & camera are inherited from portal parameters
  const { scene, camera, ... } = useOGL()
}

const scene = new OGL.Transform()
const camera = new OGL.Camera()

<transform>
  {createPortal(<Component />, scene, { camera })
</transform>

Testing

In addition to createRoot (see custom canvas), react-ogl exports an act method which can be used to safely flush async effects in tests. The following emulates a legacy root and asserts against RootState (see root state).

import * as React from 'react'
import * as OGL from 'ogl'
import { type Root, type RootStore, type RootState, createRoot, act } from 'react-ogl'

it('tests against a react-ogl component or scene', async () => {
  const transform = React.createRef<OGL.Transform>()

  const root: Root = createRoot(document.createElement('canvas'))
  const store: RootStore = await act(async () => root.render(<transform ref={transform} />))
  const state: RootState = store.getState()

  expect(transform.current).toBeInstanceOf(OGL.Transform)
  expect(state.scene.children).toStrictEqual([transform.current])
})

react-ogl's People

Contributors

codyjasonbennett avatar drcmda avatar exponenta avatar matiasngf avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

react-ogl's Issues

[critical] Uniform diff check should update only known uniforms

Bug in this place:

acc[uniform] = entry

when you use a special material with internal state and custom uniforms, react will produce errors because default uniforms will be dropped.

This is critical, because witout this will not working a special components for program with attach!

for ex:

import { Program } from "ogl";
import { extend, Node } from "react-ogl";

import React from "react";

const FRAG = `
precision highp float;

uniform vec3 uColor;
uniform vec2 uPoint;
uniform sampler2D uSampler0;

varying vec3 vNormal;
varying vec2 vUv;

void main() {
	gl_FragColor = texture2D(uSampler0, vUv);
	gl_FragColor = mix (vec4(1.0, 0., 0., 1.), gl_FragColor, step(0.01, length(uPoint - vUv)));
}
`;

const VERT = `
attribute vec3 position;
attribute vec3 normal;
attribute vec2 uv;

uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform mat3 normalMatrix;

varying vec3 vNormal;
varying vec2 vUv;

void main() {
	vNormal = normalize(normalMatrix * normal);
	vUv = uv;
	gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;

export interface IProgProps {
	mousePoint?: Array<number>;
	color?: string | any;
	children: any[];
}

export class BaseProgramWithTexture extends Program {
	constructor(gl: GLContext) {
		super(gl, {
			vertex: VERT,
			fragment: FRAG,
			transparent: true,
			uniforms:{
				uColor: { value: [1,1,1] },
				uPoint: { value: [0,0] },
				uSampler0: { value: null },
			}
		});
	}

	set texture(v) {
		this.uniforms.uSampler0 = {value: v};
	}

	get texture() {
		return this.uniforms.uSampler0?.value;
	}
}

extend({BaseProgramWithTexture});

declare global {
	namespace JSX {
	  interface IntrinsicElements {
		baseProgramWithTexture: Node<BaseProgramWithTexture, typeof BaseProgramWithTexture>
	  }
	}
  }


export default React.forwardRef<Program, IProgProps>(
	({ color = "pink", mousePoint = [0, 0], children }, ref) => {
		return (
			<baseProgramWithTexture
				ref={ref}
				uniforms = {{
					uColor: color
				}}
			>
                        <ChessTexture/>
			</baseProgramWithTexture>
		);
	}
);

// ChessTexture.tsx
// which should apply texture to `texture` field that is setter
export default ({width = 256, height = 256, step = 64}) => {
	return React.createElement('texture', {
		attach: 'texture',
		image: generateCheckmate(width, height, step)
	})
}


//programRef is reference on to our baseProgramWithTexture and uPoint MUST BE AVAILABLE
	const handleMove = ({ hit }: any) => {
		(
			programRef.current.uniforms as Record<string, { value: any }>
		).uPoint.value = hit?.uv || [0, 0];
	};

Will have a a lot of warns:
image

And if we try to change uPoint manually - will crash, because reconciler remove required fields.

Raycast not working when node has parent

Sample:

import { createRef, useRef, useState } from "react";
import { useFrame, Canvas } from "react-ogl/web";
import { render } from "react-dom";
import { Mesh, Renderer } from "ogl";

const Box = (props) => {
	const mesh = useRef<Mesh>();
	const [hovered, setHover] = useState(false);
	const [active, setActive] = useState(false);

	return (
		<mesh
			{...props}
			ref={mesh}
			scale={active ? 1.5 : 1}
			onClick={() => setActive((value) => !value)}
			onPointerOver={() => setHover(true)}
			onPointerOut={() => setHover(false)}
		>
			<plane {...props} />
			<program
				vertex={`
          attribute vec3 position;
          attribute vec3 normal;

          uniform mat4 modelViewMatrix;
          uniform mat4 projectionMatrix;
          uniform mat3 normalMatrix;

          varying vec3 vNormal;

          void main() {
            vNormal = normalize(normalMatrix * normal);
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
          }
        `}
				fragment={`
          precision highp float;

          uniform vec3 uColor;
          varying vec3 vNormal;

          void main() {
            vec3 normal = normalize(vNormal);
            float lighting = dot(normal, normalize(vec3(10)));

            gl_FragColor.rgb = uColor + lighting * 0.1;
            gl_FragColor.a = 1.0;
          }
        `}
				uniforms={{ uColor: hovered ? "hotpink" : "orange" }}
			/>
		</mesh>
	);
};

const ref = createRef<HTMLCanvasElement>();
render(
	<Canvas
		camera={{ position: [0, 1.6, 8] }}
		ref={ref}
		renderer={() =>
			new Renderer ({
				canvas: ref.current,
				dpr: 2,
				antialias: true,
				autoClear: true,
			})
		}
	>
		<transform position={[0, 1.6, 0]}>
			<Box key="grid" width={4} height={2} />

			<transform position={[-2, 0, 0]} rotation={[0, Math.PI / 6, 0]}>
				<Box
					key="left"
					position={[-0.5, 0, 0]}
					width={1}
					height={2}
				/>
			</transform>

			<transform position={[2, 0, 0]} rotation={[0, -Math.PI / 6, 0]}>
				<Box
					key="right"
					position={[0.5, 0, 0]}
					width={1}
					height={2}
				/>
			</transform>
		</transform>
	</Canvas>,
	document.getElementById("react-content")
);

Expected:
Paneles will be hover and color changed

Actual:
Paneles not react

[feature] Custom GL required class

Now, ogl-react resolve only prebuilded classes from
https://github.com/pmndrs/react-ogl/blob/main/src/constants.ts#L7

But for some case need has a custom class that not extends from listed, and which requires a pass gl to args.

atm this resolved as hack:

image
That looks starange.

So, maybe we can check a static field to classes that required to construct with GL and will check it in this:
https://github.com/pmndrs/react-ogl/blob/main/src/reconciler.ts#L35

like :

if (!object && elem.requireGL || GL_ELEMENTS.some((elem) => Object.prototype.isPrototypeOf.call(elem, target) || elem === target)) {
}

[feature] Dispose

Dispose is required because we can't know when React kill node or only replace it.
For mesh this is critical, because Geometry allocating GPU memory, Textures too, and other GL objects.

Suggest add to this:
https://github.com/pmndrs/react-ogl/blob/main/src/reconciler.ts#L121

Or remove call:

https://github.com/oframe/ogl/blob/master/src/core/Program.js#L208
https://github.com/oframe/ogl/blob/master/src/core/Geometry.js#L266

Or dispose call with nullish check, dispose or destroy is convential, and can be implemented in subclasses.

react-devtools crash

When I inspect any react-ogl elements in devtools (React Developer Tools version 4.23.0) devtools crashes with an error that it “Could not find ID for Fiber ” (where is the name of the react-dom component that renders <Canvas />).

Screen Shot 2022-02-22 at 12 23 55 PM

Crash when try use texture from Unit8Array.

Some js native classes have a set method but not have a copy.
Map/Set/TypedArray (UInt8Array for example).
Ok, there are not reason handle a Map/Set as props because there are not API what use it, but should be way pass ArrayBuffer view.

This line try to use copy method that wrong for this case:
https://github.com/pmndrs/react-ogl/blob/main/src/utils.ts#L144

Possible fix:

// we should bypass set for array buffer, because it not resizable. You cant set a 4 values to array of 0. 
if (target?.set && !ArrayBuffer.isView(value)) {
      if (target.constructor.name === value.constructor.name && target.copy) {
        target.copy(value)
      } else if (Array.isArray(value)) {
        target.set(...value)
      } else {
        // Support shorthand scalar syntax like scale={1}
        const scalar = new Array(target.length).fill(value)
        target.set(...scalar)
      }
} else {

This sometime can be used for API's like a :

{ /* texture 2x2 filled to red */ }
<texture 
     width={2} 
     height={2} 
     image={new Uint8Array([255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255]} 
/>

OGL TextureLoader and react-ogl useLoader are incompatible

useLoader passes url as url but TextureLoader only accepts src

https://github.com/oframe/ogl/blob/b9817c6b207507365471c8af0387e0df16a3f843/src/extras/TextureLoader.js#L13

urls.map(async (url: string) => {

urls.map(async (url: string) => {
  // @ts-ignore OGL's loaders don't have a consistent signature
  if (classExtends(loader, OGL.TextureLoader)) return loader.load(gl, { url })
  
  return await loader.load(gl, url)
}),

React 18 conflict dependencies

I can't run npm install anymore once I installed react-ogl in a clean Next project due to conflicting dependencies between React 18 and React ^17. What am I missing?

NPM version: 8.18.0
Node version: 16.15.0

The log:

npm ERR! code ERESOLVE
npm ERR! ERESOLVE could not resolve
npm ERR!
npm ERR! While resolving: [email protected]
npm ERR! Found: [email protected]
npm ERR! node_modules/react
npm ERR!   peer react@"^17.0.2 || ^18.0.0-0" from [email protected]
npm ERR!   node_modules/next
npm ERR!     next@"12.2.5" from the root project
npm ERR!   peer react@"^18.2.0" from [email protected]
npm ERR!   node_modules/react-dom
npm ERR!     peer react-dom@"^17.0.2 || ^18.0.0-0" from [email protected]
npm ERR!     node_modules/next
npm ERR!       next@"12.2.5" from the root project
npm ERR!     peerOptional react-dom@">=17.0" from [email protected]
npm ERR!     node_modules/react-ogl
npm ERR!       react-ogl@"^0.6.4" from the root project
npm ERR!     2 more (react-use-measure, the root project)
npm ERR!   8 more (react-ogl, react-use-measure, styled-jsx, suspend-react, ...)
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer react@"17.0.1" from [email protected]
npm ERR! node_modules/react-native
npm ERR!   peer react-native@">=0.64.0-rc.0 || 0.0.0-*" from @react-native-community/[email protected]
npm ERR!   node_modules/react-native/node_modules/@react-native-community/cli
npm ERR!     @react-native-community/cli@"^5.0.1-alpha.1" from [email protected]
npm ERR!   peerOptional react-native@">=0.64" from [email protected]
npm ERR!   node_modules/react-ogl
npm ERR!     react-ogl@"^0.6.4" from the root project
npm ERR!   1 more (@expo/browser-polyfill)
npm ERR!
npm ERR! Conflicting peer dependency: [email protected]
npm ERR! node_modules/react
npm ERR!   peer react@"17.0.1" from [email protected]
npm ERR!   node_modules/react-native
npm ERR!     peer react-native@">=0.64.0-rc.0 || 0.0.0-*" from @react-native-community/[email protected]
npm ERR!     node_modules/react-native/node_modules/@react-native-community/cli
npm ERR!       @react-native-community/cli@"^5.0.1-alpha.1" from [email protected]
npm ERR!     peerOptional react-native@">=0.64" from [email protected]
npm ERR!     node_modules/react-ogl
npm ERR!       react-ogl@"^0.6.4" from the root project
npm ERR!     1 more (@expo/browser-polyfill)
npm ERR!
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force, or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
npm ERR!
npm ERR! See /Users/arno/.npm/eresolve-report.txt for a full report.

package.json

{
  "name": "my-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "next": "12.2.5",
    "ogl": "^0.0.97",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-ogl": "^0.6.4"
  },
  "devDependencies": {
    "@types/node": "18.7.13",
    "@types/react": "18.0.17",
    "@types/react-dom": "18.0.6",
    "eslint": "8.22.0",
    "eslint-config-next": "12.2.5",
    "typescript": "4.7.4"
  }
}

Can't pass custom renderer constructor to state

Follow props signature, we able to use a custom renderer, but it not make sense because we can't pass canvas reference, because canvas not exist when we create it.

https://github.com/pmndrs/react-ogl/blob/main/src/shared/utils.ts#L105

Need to allow use a functional argument of rendere that will return instance for allowing this:
image

Like this:
https://github.com/pmndrs/react-three-fiber/blob/0fd9337565e7ae264b539b0ef9be05c04f749b89/packages/fiber/src/web/index.tsx#L39

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.