r3f but not React
Computer Graphics Ninja にとってThree.jsではつらいことがたくさんあります。
まずひとつに、memoryを抑えるため、使いおわったあとは .dispose()
するので、
全ての変数をあつめる必要があります。(他にも trackをつかう方法 があります。)
また、毎frameの処理を一か所に集める 必要があることです。
ThreeController test - CodeSandbox
function animate() {
requestAnimationFrame( animate );
/* ~~ all frame process here, but... ~~ */
renderer.render( scene, camera );
}
animate();
r3fはinstanceを自由に扱えたり、毎frame処理を自由に追加できるので、
長めのcodeでも、global変数がなくなったり、読みやすくなります。
今回は、この react-three-fiber
を、Reactなしで再現していきます。
import { useRef, useEffect } from "react";
import { useFrame, useThree } from "@react-three/fiber";
function useOrbit() {
const controlsRef = useRef(null);
useEffect(() => () => void controlsRef.current?.dispose(), []);
useFrame(() => void controlsRef.current?.update());
useThree((state) => {
controlsRef.current = new OrbitControls(state.camera, state.gl.domElement);
});
}
Getting Started
makeQueue について
frame処理するfunctionを扱うために、react-spring
の makeQueue
を利用します。
もしerrorが60fpsで出るとbrowserが落ちるので 、each
から try
を通します。
function each(values, each=(value) => value) {
values.forEach(value => {
try {
each(value);
} catch (e) {
console.error(e);
}
})
}
function makeQueue() {
let next = new Set();
let current = next;
return {
add: (fun) => next.add(fun),
delete: (fun) => next.delete(fun),
flush: (...args) => {
if (current.size) {
next = new Set();
each(current, (fun) => fun(...args) && next.add(fun));
current = next;
}
}
}
}
ThreeControllerをつくる
react-spring
の rafz
を参考に、animation frameを制御していきます。
以下のように state
を使うことで、自由に追加したり操作できます。
class ThreeController {
constructor() {
let ts = -1, id = -1;
let queue = _makeQueue();
const raf = (fun=()=>{}) => void (id = requestAnimationFrame(fun));
const caf = () => (ts = -1, id > 0 && cancelAnimationFrame(id));
const loop = () => ts > -1 && void (raf(loop), queue.flush());
const start = () => ts < 0 && void (raf(loop), ts++);
const state = (fun) => void (queue.add(fun), start());
state.add = (fun) => void queue.add(fun);
state.delete = (fun) => void queue.delete(fun);
state.cancel = () => void (queue = _makeQueue(), caf());
this.state = state;
}
{/*~~ ➊ ~~*/}
{/*~~ ➋ ~~*/}
}
つぎのような class method を ➊ に追加すると、class based で React をつかうことができます。 この方法については、class based React.js hooks | TSEI.JP をご覧ください。
effect() {
const { state } = this;
this._initialize();
state(state.render);
}
clean() {
const { state } = this;
if (!state.isInitialized) return;
state.isInitialized = false;
state.gl.dispose();
state.cancel();
}
この Three.js
を初期化する class method を ➋ に 追加します。
state.render
が 毎 frame に実行されて、canvas が描画されます。
_initialize() {
const { props, state } = this;
const { canvas } = props;
if (state.isInitialized) return;
state.isInitialized = true;
state.scene = new THREE.Scene();
state.camera = new THREE.PerspectiveCamera(75, 0, 0.1, 1000)
state.gl = new THREE.WebGLRenderer({ canvas });
state.render = () => (void state.gl.render(state.scene, state.camera)) || true;
}
固有なfunctionにする
queue
に追加時、functionが重複しないよう固有なfunctionにします。
useQueue
は指定したfunctionをmutableになるようにhooksを作成します。
Reactは state
を、contextから参照したり、固有なfunctionにできますが、
vanillaでは useContext
や useRef
が使えないので、id
から保存させます。
// for React
const _fun = (_state={}) => false;
function useQueue(fun=_fun, deps=[]) {
const ref = useRef(fun);
const state = useThreeContext();
const callback = useCallback(() => ref.current(state), deps);
useEffect(() => void (ref.current = fun), [fun]);
useEffect(() => (void state.add(callback)) || (() => state.delete(callback)), deps);
return state;
}
// for vanilla
const _fun = (_state={}) => false;
const _three = new ThreeController();
const _currentMap = new Map();
const _callbackMap = new Map();
function useQueue(id='', fun=_fun) {
const { state } = _three;
if (_currentMap.get(id) !== fun)
_currentMap.set(id, fun);
const callback =
_callbackMap.get(id) ||
_callbackMap.set(id, () => _currentMap.get(id)?.(state)).get(id);
state.add(callback);
return state;
}
useThree, useFrame, useLoader について
最後に、API を用意することで、r3fと同じようにかくことができます。 一度だけ実行したり、3D ModelのLoadが完了したあとに、functionを実行させることもできます。
// for React
const useThree = (fun=_fun, deps=[]) => useQueue((state) => fun(state) && false, deps);
const useFrame = (fun=_fun, deps=[]) => useQueue((state) => fun(state) || true, deps);
const useLoader = (fun=_fun, deps=[]) => useQueue((state) => state.isLoaded? fun(state) && false: true, deps);
// for vanilla
const useThree = (id='', fun=_fun) => useQueue(id, (state) => fun(state) && 0);
const useFrame = (id='', fun=_fun) => useQueue(id, (state) => fun(state) || 1);
const useLoader = (id='', fun=_fun) => useQueue(id, (state) => state.isLoaded? fun(state) && false: true);
Conclusion
最初と同じように、vanillaでもr3fのcodeを利用できるようになりました。
function useOrbit() {
const controlsRef = { current: null };
useFrame('updateOrbit', () => void controlsRef.current?.update());
useThree('setupOrbit', (state) => {
controlsRef.current = new OrbitControls(state.camera, state.gl.domElement);
});
}