Declaratively create scenes with re-usable OGL components that have their own state and effects and can tap into React's infinite ecosystem.
npm install ogl react-ogl
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.
How does this compare to @react-three/fiber
?
react-ogl is a complete re-architecture of @react-three/fiber with:
- no defaults; you have complete control. No default renderer, camera, etc. For library/engine authors, this allows components to be completely transformative of rendering behavior and API. But this freedom leads to boilerplate. For both users and authors, there are —
- extendable helpers; react-ogl exports helper components and hooks for both web and native with an API familiar to @react-three/fiber, but these helpers are also modular. This enables you to change or extend rendering behavior and API while maintaining interop with the react-ogl ecosystem.
The API is the same as @react-three/fiber, but react-ogl is completely extensible.
The following takes complete control and declaratively renders a mesh that can react to state.
import * as OGL from 'ogl'
import { createRoot } from 'react-ogl'
// Init rendering internals
const canvas = document.querySelector('canvas')
const renderer = new OGL.Renderer({ canvas })
const camera = new OGL.Camera(renderer.gl)
camera.position.z = 5
const scene = new OGL.Transform(renderer.gl)
// Or you can use our own internals. This will also set up a render loop.
// const { root, renderer, camera, scene } = createInternals(camera, config)
// Set initial size
renderer.setSize(window.innerWidth, window.innerHeight)
camera.perspective({ aspect: window.innerWidth / window.innerHeight })
// Create root
const root = createRoot(canvas, { renderer, camera, scene })
root.render(
<mesh>
<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: 'white' }}
/>
</mesh>,
)
// Render to screen
const animate = () => {
requestAnimationFrame(animate)
renderer.render({ scene, camera })
}
animate()
react-ogl itself is super minimal, but you can use the familiar @react-three/fiber API with some helpers targeted for different platforms:
Usage with react-dom
This example uses create-react-app
for the sake of simplicity, but you can use your own environment or create a codesandbox.
# Create app
npx create-react-app my-app
cd my-app
# Install dependencies
npm install ogl react-ogl
# Start
npm run start
Inside of our app, we can use the same API as @react-three/fiber but with OGL elements and methods.
import { useRef, useState } from 'react'
import { useFrame, Canvas } from 'react-ogl/web'
import { render } from 'react-dom'
const Box = (props) => {
const mesh = useRef()
const [hovered, setHover] = useState(false)
const [active, setActive] = useState(false)
useFrame(() => (mesh.current.rotation.x += 0.01))
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>
)
}
render(
<Canvas camera={{ position: [0, 0, 8] }}>
<Box position={[-1.2, 0, 0]} />
<Box position={[1.2, 0, 0]} />
</Canvas>,
document.getElementById('root'),
)
Usage with react-native
This example uses expo-cli
but you can create a bare app with react-native
CLI as well.
# 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
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: {
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 OpenGLES — no webview needed.
import React, { useRef, useState } from 'react'
import { useFrame, Canvas } from 'react-ogl/native'
const Box = (props) => {
const mesh = useRef()
const [hovered, setHover] = useState(false)
const [active, setActive] = useState(false)
useFrame(() => (mesh.current.rotation.x += 0.01))
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>
)
}
const App = () => (
<Canvas camera={{ position: [0, 0, 8] }}>
<Box position={[-1.2, 0, 0]} />
<Box position={[1.2, 0, 0]} />
</Canvas>
)
export default App