Last active
March 12, 2020 21:21
-
-
Save jonikorpi/57a445682b47ee45b0b0d4c13db620f1 to your computer and use it in GitHub Desktop.
Revisions
-
jonikorpi revised this gist
Mar 12, 2020 . 1 changed file with 4 additions and 4 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -23,14 +23,14 @@ const Example = () => { {/* Batch props get added to the command's context */} <Batch color={[1,1,1]}> {/* Element props are sent to the command's buffers in `instancedAttributes` */} <Element translation={new Float32Array([0,2,0])} /> </Batch> <Element translation={new Float32Array([0,0,0])} /> <Batch color={[1,0,0]}> <Element translation={new Float32Array([0,5,0])} /> <Element translation={new Float32Array([0,10,0])} /> </Batch> </ReglEngine> ); -
jonikorpi revised this gist
Mar 12, 2020 . 1 changed file with 3 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -23,8 +23,10 @@ const Example = () => { {/* Batch props get added to the command's context */} <Batch color={[1,1,1]}> {/* Element props are sent to the command's buffers in `instancedAttributes` */} <Element translation={[0,2,0]} /> </Batch> <Element translation={[0,0,0]} /> <Batch color={[1,0,0]}> <Element translation={[0,5,0]} /> -
jonikorpi revised this gist
Mar 12, 2020 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -16,7 +16,7 @@ const ExampleCommand = regl => ({ const Example = () => { const { Element, Batch } = useCommand(ExampleCommand); // 1 <Element> = 1 instance added to the command, or the closest <Batch> of the command that contains the <Element> return ( <ReglEngine> -
jonikorpi revised this gist
Mar 12, 2020 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -16,7 +16,7 @@ const ExampleCommand = regl => ({ const Example = () => { const { Element, Batch } = useCommand(ExampleCommand); // 1 <Element> = 1 instance added to the command, or the closest <Batch> that contains the <Element> return ( <ReglEngine> -
jonikorpi revised this gist
Mar 12, 2020 . 1 changed file with 1 addition and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -16,6 +16,7 @@ const ExampleCommand = regl => ({ const Example = () => { const { Element, Batch } = useCommand(ExampleCommand); // 1 <Element> = 1 instance added to the command, or the <Batch> that contains it return ( <ReglEngine> -
jonikorpi revised this gist
Mar 12, 2020 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -5,7 +5,7 @@ const ExampleCommand = regl => ({ uniforms: { color: ({color}) => color || defaultColor, }, attributes: { position: …, }, // Apart from this part everything here is standard regl -
jonikorpi revised this gist
Mar 12, 2020 . No changes.There are no files selected for viewing
-
jonikorpi revised this gist
Mar 12, 2020 . 1 changed file with 2 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,8 +1,9 @@ const defaultColor = [1,1,1]; const ExampleCommand = regl => ({ vert: "…", frag: "…", uniforms: { color: ({color}) => color || defaultColor, }, attributes = { position: …, -
jonikorpi revised this gist
Mar 12, 2020 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,4 +1,4 @@ const ExampleCommand = regl => ({ vert: "…", frag: "…", uniforms: { -
jonikorpi revised this gist
Mar 12, 2020 . 1 changed file with 33 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,33 @@ const ExampleCommand = regl => regl({ vert: "…", frag: "…", uniforms: { color: ({color}) => color, }, attributes = { position: …, }, // Apart from this part everything here is standard regl instancedAttributes: { translation: new Float32Array(3), }, }); const Example = () => { const { Element, Batch } = useCommand(ExampleCommand); return ( <ReglEngine> {/* Batch props get added to the command's context */} <Batch color={[1,1,1]}> {/* Element props are sent to the command's buffers in `instancedAttributes` */} <Element translation={[0,0,0]} /> </Batch> <Batch color={[1,0,0]}> <Element translation={[0,5,0]} /> <Element translation={[0,10,0]} /> </Batch> </ReglEngine> ); } -
jonikorpi renamed this gist
Mar 12, 2020 . 1 changed file with 0 additions and 0 deletions.There are no files selected for viewing
File renamed without changes. -
jonikorpi created this gist
Mar 12, 2020 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,448 @@ import React, { useContext, useEffect, useRef, createContext, useState, useCallback, } from "react"; import createCamera from "perspective-camera"; let regl; if (process.env.NODE_ENV === "development") { regl = require("regl"); } else { regl = require("regl/dist/regl.unchecked.js"); } // WARNING: won't work with multiple engine instances const subscribers = new Set(); const useLoopCallback = callback => { useEffect(() => { if (callback) { subscribers.add(callback); return () => { subscribers.delete(callback); }; } }, [callback]); }; const EngineContext = createContext(); export const useEngine = () => useContext(EngineContext); export const useLoop = callback => useContext(EngineContext).useLoop(callback); const CameraContext = createContext(); export const useCamera = () => useEngine().context.camera; export const EngineWithCamera = ({ camera, context, onResize, ...props }) => { const cameraInstance = useRef(createCamera(camera)).current; const handleResize = (width, height) => { const vmax = Math.max(width, height); cameraInstance.viewport[0] = -(vmax / height); cameraInstance.viewport[1] = -(vmax / width); cameraInstance.viewport[2] = vmax / height; cameraInstance.viewport[3] = vmax / width; cameraInstance.update(); if (onResize) { onResize(width, height); } }; return ( <CameraContext.Provider value={cameraInstance}> <Engine context={{ ...context, camera: cameraInstance }} onResize={handleResize} {...props} /> </CameraContext.Provider> ); }; const Engine = ({ children, context, canvasProps = {}, pixelRatio = process.browser ? window.devicePixelRatio : 1, attributes, onResize, debug = process.env.NODE_ENV === "development", defaultShaders, drawOnEveryFrame = true, onLoop, ...props }) => { // FIXME: can't change init variables after first render const [engine, setEngine] = useState(); const [readyForRendering, setReadyForRendering] = useState(false); const engineRef = useRef(null); const commands = useRef(new Map()).current; const internalCanvasRef = useRef(); const canvasRef = canvasProps.ref || internalCanvasRef; useEffect(() => { const canvas = canvasRef.current; const engineInstance = regl({ extensions: ["ANGLE_instanced_arrays", "OES_standard_derivatives"], optionalExtensions: debug ? ["EXT_disjoint_timer_query"] : [], attributes: { antialias: false, cull: { enable: true }, alpha: false, premultipliedAlpha: false, ...attributes, }, profile: debug, pixelRatio, canvas, ...props, }); setEngine(() => engineInstance); engineRef.current = engineInstance; return () => { console.log("destroying regl instance"); setEngine(); engineRef.current = null; engineInstance.destroy(); }; }, []); const draw = useRef(); useEffect(() => { if (engine) { const contextCommand = engine({ context: { ...context, clear: { color: [0.618, 0.618, 0.618, 1] }, }, }); const batchHolder = []; draw.current = () => { try { contextCommand(context => { if (onLoop) { onLoop(context); } engine.clear(context.clear); for (const callback of subscribers) { callback.call(null, context); } for (const command of commands.values()) { const { command: callCommand, batches, indexes, instancedBuffers, needsUpdate, isInstanced, name, } = command; if (needsUpdate) { if (process.env.NODE_ENV === "development") { console.log("updating buffers and indexes for command", name); } // Refresh indexes cache command.indexes.clear(); let index = 0; for (const batch of batches) { batch.offset = index; for (const instance of batch.instances) { command.indexes.set(instance, index); index++; } } command.totalInstances = index; // Refill buffers for (const [key, buffer] of instancedBuffers) { const { dimensions, ArrayConstructor } = buffer; const data = new ArrayConstructor(command.totalInstances * dimensions); for (const { instances } of batches) { for (const instance of instances) { data.set(instance[key], indexes.get(instance) * dimensions); } } buffer.buffer(data); } command.needsUpdate = false; } if (isInstanced) { if (command.totalInstances === 0) { continue; } batchHolder.length = 0; for (const batch of batches) { batchHolder.push(batch); } callCommand(batchHolder); } else { callCommand(); } } }); } catch (err) { loop.cancel(); throw err; } }; const loop = drawOnEveryFrame && engine.frame(draw.current); setReadyForRendering(true); return () => { if (engineRef.current && loop) { loop.cancel(); console.log("destroying regl loop"); } }; } }, [engine]); useEffect(() => { const canvas = canvasRef.current; const handleResize = () => { const { width: canvasWidth, height: canvasHeight } = canvas.getBoundingClientRect(); const width = canvasWidth; const height = canvasHeight; canvas.width = width * pixelRatio; canvas.height = height * pixelRatio; if (!drawOnEveryFrame && draw.current) { engine.poll(); draw.current(); } if (onResize) { onResize(width, height); } }; window.addEventListener("resize", handleResize); handleResize(); return () => { window.removeEventListener("resize", handleResize); }; }, [canvasRef, onResize, pixelRatio, drawOnEveryFrame, engine]); useEffect(() => { if (engine && debug) { const debugInterval = setInterval(() => { console.table( Object.fromEntries( Object.entries(engine.stats).map(([key, value]) => [ key, typeof value === "function" ? value() : value, ]) ) ); let commandStats = []; for (const [ , { command, totalInstances, batches, name, drawOrder }, ] of commands.entries()) { commandStats.push({ "drawOrder": drawOrder, "name": name, "batches": batches.size, "instances": totalInstances, "invocations": command.stats.count, "CPU %": (command.stats.cpuTime / performance.now()) * 100, "CPU/frame %": command.stats.cpuTime / command.stats.count / 16, "GPU %": (command.stats.gpuTime / performance.now()) * 100, "GPU/frame %": command.stats.gpuTime / command.stats.count / 16, }); } console.table(commandStats); }, 10000); return () => { clearInterval(debugInterval); }; } }, [engine, debug, commands]); return ( <> <canvas ref={canvasRef} {...canvasProps}></canvas> {readyForRendering ? ( <EngineContext.Provider value={{ engine, commands, useLoop: useLoopCallback, defaultShaders, context, draw: () => { engine.poll(); draw.current(); }, }} > {children} </EngineContext.Provider> ) : null} </> ); }; export default Engine; const BatchContext = createContext(); export const useBatch = () => useContext(BatchContext); export const useCommand = (draw, name) => { const engineObject = useEngine(); const { engine, commands, defaultShaders } = engineObject; // If command hasn't been created, create it if (!commands.has(draw)) { const { vert, frag, uniforms, attributes, instancedAttributes, drawOrder = 0, ...rest } = draw( engine ); const instancedBuffers = new Map(); const instancedAttributesWithBuffers = {}; for (const attribute in instancedAttributes) { const value = instancedAttributes[attribute]; const buffer = { buffer: engine.buffer({ usage: "dynamic", type: "float32" }), dimensions: value.length, BYTES_PER_ELEMENT: value.BYTES_PER_ELEMENT, ArrayConstructor: value.constructor, }; instancedBuffers.set(attribute, buffer); instancedAttributesWithBuffers[attribute] = { buffer: ({ instancedBuffers }) => instancedBuffers.get(attribute).buffer, divisor: 1, offset: ({ instancedBuffers }, { offset }) => { const buffer = instancedBuffers.get(attribute); return offset * buffer.BYTES_PER_ELEMENT * buffer.dimensions; }, }; } const context = { batches: new Set(), rootBatch: { instances: new Set() }, indexes: new Map(), instancedBuffers, name: name || draw.name || "unnamed command", isInstanced: !!instancedAttributes, needsUpdate: !!instancedAttributes, totalInstances: 0, drawOrder, }; const Batch = ({ children, ...props }) => { const state = useRef({ instances: new Set() }); for (const key in props) { state.current[key] = props[key]; } return <BatchContext.Provider value={state.current}>{children}</BatchContext.Provider>; }; const Element = ({ children = null, onLoop, ...props }) => { // Create a stable identity for this component instance const instance = useRef(props).current; // Use a batch (unbatched instances get batched together) const batch = useBatch() || context.rootBatch; const command = context; useEffect(() => { // Add this instance to its batch within the command // and create the batch if it doesn't exist yet if (!command.batches.has(batch)) { command.batches.add(batch); } batch.instances.add(instance); command.needsUpdate = true; return () => { // Delete this instance batch.instances.delete(instance); command.needsUpdate = true; }; }, [command, instance, batch]); // A function for updating data in buffers for this instance const update = useCallback( (key, data) => { const { instancedBuffers, indexes } = command; const buffer = instancedBuffers.get(key).buffer; if (buffer._buffer.byteLength) { instancedBuffers .get(key) .buffer.subdata(data, data.BYTES_PER_ELEMENT * data.length * indexes.get(instance)); } }, [command, instance] ); // Update buffers from props whenever this component re-renders useEffect(() => { for (const [key] of instancedBuffers) { if (props[key]) { instance[key] = props[key]; update(key, instance[key]); } } }); const { useLoop } = useEngine(); useLoop(onLoop && (context => onLoop(update, instance, context))); return children; }; const command = engine({ instances: context.isInstanced ? (c, { instances }) => instances.size : undefined, context, attributes: { ...attributes, ...instancedAttributesWithBuffers, }, uniforms, vert: vert || defaultShaders.vert, frag: frag || defaultShaders.frag, ...rest, }); context.command = command; context.Batch = Batch; context.Element = Element; commands.set(draw, context); // Sort commands const commandArray = Array.from(commands).sort((a, b) => a[1].drawOrder - b[1].drawOrder); commands.clear(); for (const [command, context] of commandArray) { commands.set(command, context); } } return commands.get(draw); };