Skip to main content

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-springmakeQueueを利用します。 もし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-springrafz を参考に、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では useContextuseRef が使えないので、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);
});
}

Happy HardCode🙃