みかづきブログ・カスタム

基本的にはちょちょいのほいです。

Three.jsにてCanvasを使ってTextureを動的に変更し、VRモードでも動くようにする 🖼

f:id:kimizuka:20201003195101g:plain

以前、VRモードに対応する形で作ったので、そのままVRモードでつくりますが、VRモードじゃなくても要点は変わりません。

❶ Canvasを作る
❷ TextureにCanvasを設定する
❸ MaterialにTextureを設定する
❹ MeshにMaterialと適当なGeometryを設定する
❺ Meshのmaterial.map.needsUpdateにtrueを渡す

という流れです。
注意事項としては❺は1度だけでなく、Textureを更新したいタイミングで設定しなければなりません。
毎ループ更新したかったら、毎ループ設定する必要があります。

また、VRモードでは何故かrequestAnimationが止まってしまうようなので、そこは気をつける必要があります。
未検証ですが、XRSession.requestAnimationFrameを使えば動くのかもしれません。

developer.mozilla.org


JavaScript

import * as THREE from './build/three.module.js';
import { OrbitControls } from './jsm/controls/OrbitControls.js';
import { GLTFLoader } from './jsm/loaders/GLTFLoader.js';
import { BoxLineGeometry } from './jsm/geometries/BoxLineGeometry.js';
import { VRButton } from './jsm/webxr/VRButton.js';

const canvas = document.createElement('canvas');

document.body.appendChild(canvas);

const scene = new THREE.Scene();
const width = window.innerWidth;
const height = window.innerHeight;

const renderer = new THREE.WebGLRenderer({
  canvas,
  antialias: true
});

renderer.setSize(width, height);
renderer.vr.enabled = true;

const camera = new THREE.PerspectiveCamera(45, width / height, 1, 100);

camera.position.set(0, 1, 10);

const controls = new OrbitControls(camera, renderer.domElement);
const light = new THREE.DirectionalLight(0xFFFFFF);

light.position.set(10, 10, 10);
scene.add(light);

const room = new THREE.LineSegments(
  new BoxLineGeometry(8, 8, 8, 10, 10, 10).translate(0, 2, 0),
  new THREE.LineBasicMaterial({
    color: 0xFFFFFF
  })
);

scene.add(room);

document.body.appendChild(VRButton.createButton(renderer));

const plane = (() => {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  const fontSize = 24;

  canvas.width = 320
  canvas.height = 180;

  const geometry = new THREE.PlaneGeometry(canvas.width / 100, canvas.height / 100);
  const texture = new THREE.CanvasTexture(canvas);
  const material = new THREE.MeshBasicMaterial({
    map: texture
  });

  (function update() {
    ctx.fillStyle = 'white';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = 'black';
    ctx.font = `${fontSize}px sans-serif`;
    ctx.fillText(Date.now(), 0, fontSize);

    material.map.needsUpdate = true;

    requestAnimationFrame(update);
  })();

  return new THREE.Mesh(geometry, material);
})();

plane.position.y = 2;
plane.position.z = -2;

scene.add(plane);

renderer.setAnimationLoop(() => {
  controls.update();
  renderer.render(scene, camera);
});

すごくざっくり書くとこんな感じです。
毎フレームDate.now()の結果が更新されます。

ただ、このコードだと、VRモードに入るとDate.nowの更新が止まってしまいます。
理由を調べたのですが、requestAnimationFrameが止まってしまうようです。
なので、updateを一緒にリターンし、renderer.setAnimationLoop内で実行するように改修します。


JavaScript

  import * as THREE from './build/three.module.js';
  import { OrbitControls } from './jsm/controls/OrbitControls.js';
  import { GLTFLoader } from './jsm/loaders/GLTFLoader.js';
  import { BoxLineGeometry } from './jsm/geometries/BoxLineGeometry.js';
  import { VRButton } from './jsm/webxr/VRButton.js';

  const canvas = document.createElement('canvas');

  document.body.appendChild(canvas);

  const scene = new THREE.Scene();
  const width = window.innerWidth;
  const height = window.innerHeight;

  const renderer = new THREE.WebGLRenderer({
    canvas,
    antialias: true
  });

  renderer.setSize(width, height);
  renderer.vr.enabled = true;

  const camera = new THREE.PerspectiveCamera(45, width / height, 1, 100);

  camera.position.set(0, 1, 10);

  const controls = new OrbitControls(camera, renderer.domElement);
  const light = new THREE.DirectionalLight(0xFFFFFF);

  light.position.set(10, 10, 10);
  scene.add(light);

  const room = new THREE.LineSegments(
    new BoxLineGeometry(8, 8, 8, 10, 10, 10).translate(0, 2, 0),
    new THREE.LineBasicMaterial({
      color: 0xFFFFFF
    })
  );

  scene.add(room);

  document.body.appendChild(VRButton.createButton(renderer));

  const plane = (() => {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    const fontSize = 24;

    canvas.width = 320
    canvas.height = 180;

    const geometry = new THREE.PlaneGeometry(canvas.width / 100, canvas.height / 100);
    const texture = new THREE.CanvasTexture(canvas);
    const material = new THREE.MeshBasicMaterial({
      map: texture
    });

    function update() {
      ctx.fillStyle = 'white';
      ctx.fillRect(0, 0, canvas.width, canvas.height);
      ctx.fillStyle = 'black';
      ctx.font = `${fontSize}px sans-serif`;
      ctx.fillText(Date.now(), 0, fontSize);
      ctx.fillText(x, 0, fontSize * 2);

      material.map.needsUpdate = true;
    }

    const plane = new THREE.Mesh(geometry, material);

    plane.update = update;

    return plane;
  })();

  plane.position.y = 2;
  plane.position.z = -2;

  scene.add(plane);

  renderer.setAnimationLoop(() => {
    controls.update();
    plane.update();
    renderer.render(scene, camera);
  });

こんな感じです。
plane.updateという名前がバッティングしそうで若干気になりますが、いまのところこれでVRモードでも動作します。

また、


JavaScript

import * as THREE from './build/three.module.js';
import { OrbitControls } from './jsm/controls/OrbitControls.js';
import { GLTFLoader } from './jsm/loaders/GLTFLoader.js';
import { BoxLineGeometry } from './jsm/geometries/BoxLineGeometry.js';
import { VRButton } from './jsm/webxr/VRButton.js';

const canvas = document.createElement('canvas');

document.body.appendChild(canvas);

const scene = new THREE.Scene();
const width = window.innerWidth;
const height = window.innerHeight;

const renderer = new THREE.WebGLRenderer({
  canvas,
  antialias: true
});

renderer.setSize(width, height);
renderer.vr.enabled = true;

const camera = new THREE.PerspectiveCamera(45, width / height, 1, 100);

camera.position.set(0, 1, 10);

const controls = new OrbitControls(camera, renderer.domElement);
const light = new THREE.DirectionalLight(0xFFFFFF);

light.position.set(10, 10, 10);
scene.add(light);

const room = new THREE.LineSegments(
  new BoxLineGeometry(8, 8, 8, 10, 10, 10).translate(0, 2, 0),
  new THREE.LineBasicMaterial({
    color: 0xFFFFFF
  })
);

scene.add(room);

document.body.appendChild(VRButton.createButton(renderer));

const plane = (() => {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  const fontSize = 24;

  canvas.width = 320
  canvas.height = 180;

  const geometry = new THREE.PlaneGeometry(canvas.width / 100, canvas.height / 100);
  const texture = new THREE.CanvasTexture(canvas);
  const material = new THREE.MeshBasicMaterial({
    map: texture
  });

  setInterval(() => {
    ctx.fillStyle = 'white';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = 'black';
    ctx.font = `${fontSize}px sans-serif`;
    ctx.fillText(Date.now(), 0, fontSize);

    material.map.needsUpdate = true;
  }, 1000 / 24);

  return new THREE.Mesh(geometry, material);
})();

plane.position.y = 2;
plane.position.z = -2;

scene.add(plane);

renderer.setAnimationLoop(() => {
  controls.update();
  renderer.render(scene, camera);
});

といった感じで、requestAnimationFrameをやめて、setIntervalにするだけでも動くのでこっちの方が手軽かもしれません。
Textureの更新タイミングとアプリケーションの更新タイミングを別にしたい場合は、この実装でも良い気がします。