1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

BabylonJS カメラ入力をカスタマイズする

Last updated at Posted at 2024-12-16

babylonjs のカメラ移動をカスタマイズする

babylon.js ではUniversalCameraというカメラが用意されています。UniversalCameraはファーストパーソンシューティングのようなカメラ操作を提供してくれます。FreeCameraのマウス、キーボード操作にタッチとゲームパッド操作を追加したものです。

UniversalCameraにはデフォルトでキーボードによる平行移動が実装されていますが、カメラの向いた方を正面としてその姿勢のまま移動方向を決定してしまうため上を見上げていると空に向かって移動しようとしてしまいます。今回はこのデフォルトの移動の代わりに水平方向にしか移動しないキーボード移動を実装してみます。Customizing Camera Inputs に従って4つの関数を実装したオブジェクトをUniversalCameraに渡します。

フィールド作成にはNode Geometryを使用しました。Alignブロックを用いて床面の法線にあわせて箱の向きを変えています。材質はインデックスを指定して後でビルド後のメッシュにMultiMaterialを指定することで複数材質が指定できます。

UniversalCameraにはメッシュとの衝突判定が用意されていますのでこれも有効にします。これでフィールド上を歩き回るような操作が実現できます。詳しくは Camera Collisions を参照してください。

以下のコードは 2024年12月15日の Babylon.js Playground 7.40.2(WebGL2) Javascript で動作したことがあります。

const _assumedFramesPerSecond = 60;

/**
 * 水平方向に平行移動するカメラキー移動
 */
class HorizontalCameraKeyboardMoveInput {
  /**
   * 上位からセットされる
   * @type {BABYLON.Camera}
   */
  camera = null;
  engine = null;
  scene = null;
  moveSpeed = 0.4 / _assumedFramesPerSecond;
  shiftRate = 2.0;
  ctrlRate = 0.5;
  preShift = false;
  preCtrl = false;
  codes = [];
  codesUp = ['KeyW'];
  codesDown = ['KeyS'];
  codesLeft = ['KeyA'];
  codesRight = ['KeyD'];
  onCanvasBlurObserver = null;
  onKeyboardObserver = null;

  attachControl(noPreventDefault) {
    noPreventDefault = BABYLON.Tools.BackCompatCameraNoPreventDefault(arguments);
    if (this.onCanvasBlurObserver) {
      return;
    }

    this.scene = this.camera.getScene();
    this.engine = this.scene.getEngine();

    this.onCanvasBlurObserver = this.engine.onCanvasBlurObservable.add(() => {
      this.codes.length = 0;
    });

    this.onKeyboardObserver = this.scene.onKeyboardObservable.add((info) => {
      const evt = info.event;
      const {code, shiftKey, ctrlKey} = evt;
      this.preShift = shiftKey;
      this.preCtrl = ctrlKey;
      if (evt.metaKey) {
        return;
      }

      if (info.type === BABYLON.KeyboardEventTypes.KEYDOWN) {
        if (this.codesUp.indexOf(code) >= 0 ||
          this.codesDown.indexOf(code) >= 0 ||
          this.codesLeft.indexOf(code) >= 0 ||
          this.codesRight.indexOf(code) >= 0) {
          const index = this.codes.findIndex(v => v.code === code);
          if (index < 0) { // 存在しなかったら追加する
            this.codes.push({code});
          }
          if (!noPreventDefault) {
            evt.preventDefault();
          }
        }
      } else {
        if (this.codesUp.indexOf(code) >= 0 ||
          this.codesDown.indexOf(code) >= 0 ||
          this.codesLeft.indexOf(code) >= 0 ||
          this.codesRight.indexOf(code) >= 0) {
          const index = this.codes.findIndex(v => v.code === code);
          if (index >= 0) { // 存在したら削除する
            this.codes.splice(index, 1);
          }
          if (!noPreventDefault) {
            evt.preventDefault();
          }
        }
      }

    });
  }

  detachControl() {
    this.codes.length = 0;
    if (!this.scene) {
      return;
    }

    if (this.onKeyboardObserver) {
      this.scene.onKeyboardObservable.remove(this.onKeyboardObserver);
    }
    if (this.onCanvasBlurObserver) {
      this.engine.onCanvasBlurObservable.remove(this.onCanvasBlurObserver);
    }
    this.onKeyboardObserver = null;
    this.onCanvasBlurObserver = null;
  }

  checkInputs() {
    if (!this.onKeyboardObserver) {
      return;
    }
    const camera = this.camera;
    for (let index = 0; index < this.codes.length; index++) {
      const {code} = this.codes[index];

      const local = new BABYLON.Vector3();
      if (this.codesLeft.indexOf(code) >= 0) {
        local.x += -1;
      } else if (this.codesUp.indexOf(code) >= 0) {
        local.z += 1;
      } else if (this.codesRight.indexOf(code) >= 0) {
        local.x += 1;
      } else if (this.codesDown.indexOf(code) >= 0) {
        local.z += -1;
      }
      if (camera.getScene().useRightHandedSystem) {
        local.z *= -1;
      }

      if (local.length() === 0) {
        continue;
      }

      const dir = camera.getDirection(local.normalize());
      dir.y = 0;
      dir.normalize();
      if (this.preShift) console.log('shift!');
      const rate = (this.preCtrl ? this.ctrlRate : (this.preShift ? this.shiftRate : 1));
      const move = dir.scale(this.moveSpeed * rate);
      camera.cameraDirection.addInPlace(move);
    }

  }

  getClassName() {
    return 'HorizontalCameraKeyboardMoveInput';
  }

  getSimpleName() {
    return 'horizontalkeyboard';
  }
}

const createScene = async () => {
  const earthGravity = -9.81;

  const scene = new BABYLON.Scene(engine);
  scene.collisionsEnabled = true;
  scene.gravity = new BABYLON.Vector3(0, earthGravity / _assumedFramesPerSecond, 0);

  const light = new BABYLON.HemisphericLight('light1',
    new BABYLON.Vector3(0.707, 0.707, 0),
    scene);

  const fieldSize = 51;
  const boxSize = 3;
  const boxNum = 57;
  {
    let count = 0;
    const _in = (val, type) => {
      const bl = new BABYLON.GeometryInputBlock(`bl${count}`, type);
      bl.value = val;
      count += 1;
      return bl;
    };
    const _float = (val, to) => {
      const bl = _in(val, BABYLON.NodeGeometryBlockConnectionPointTypes.Float);
      bl.output.connectTo(to);
    };
    const _int = (val, to) => {
      const bl = _in(val, BABYLON.NodeGeometryBlockConnectionPointTypes.Int);
      bl.output.connectTo(to);
    };
    const _v3 = (x, y, z, to) => {
      const bl = _in(new BABYLON.Vector3(x, y, z), 
        BABYLON.NodeGeometryBlockConnectionPointTypes.Vector3);
      bl.output.connectTo(to);
    };

    const ng = new BABYLON.NodeGeometry('node');
    {
      const bplane = new BABYLON.GridBlock('bplain');
      const bnoise = new BABYLON.NoiseBlock('bnoise');
      const bmul = new BABYLON.MathBlock('bmul');
      bmul.operation = BABYLON.MathBlockOperations.Multiply;
      const bpos = new BABYLON.GeometryInputBlock('bpos');
      bpos.contextualValue = BABYLON.NodeGeometryContextualSources.Positions;
      const badd = new BABYLON.MathBlock('badd');
      badd.operation = BABYLON.MathBlockOperations.Add;
      const bsetpos = new BABYLON.SetPositionsBlock('bsetpos');
      const bcompute = new BABYLON.ComputeNormalsBlock('bcompute');
      const bsetmtl = new BABYLON.SetMaterialIDBlock('bsetmtl');
      const bface = new BABYLON.InstantiateOnFacesBlock('bface');
      const bobj = new BABYLON.BoxBlock('bobj');
      const btrans = new BABYLON.GeometryTransformBlock('btrans');
      const bnormal = new BABYLON.GeometryInputBlock('bnormal');
      bnormal.contextualValue = BABYLON.NodeGeometryContextualSources.Normals;
      const balign = new BABYLON.AlignBlock('balign');
      const bmerge = new BABYLON.MergeGeometryBlock('bmerge');
      const out = new BABYLON.GeometryOutputBlock('out');

      _float(fieldSize, bplane.width);
      _float(fieldSize, bplane.height);
      _int(fieldSize, bplane.subdivisionsX);
      _int(fieldSize, bplane.subdivisionsY);

      _int(1, bsetmtl.id);
      bplane.geometry.connectTo(bsetmtl.geometry);

      bnoise.output.connectTo(bmul.left);
      _v3(0, 1, 0, bmul.right);
      bmul.output.connectTo(badd.left);
      bpos.output.connectTo(badd.right);

      bsetmtl.output.connectTo(bsetpos.geometry);
      badd.output.connectTo(bsetpos.positions);
      bsetpos.output.connectTo(bcompute.geometry);

      // 箱
      _float(boxSize, bobj.size);
      bobj.geometry.connectTo(btrans.value);
      _v3(0, boxSize * 0.5, 0, btrans.translation);

      bnormal.output.connectTo(balign.target);
      _v3(0, 1, 0, balign.source);
      // 面配置
      _int(boxNum, bface.count);
      btrans.output.connectTo(bface.instance);
      bcompute.output.connectTo(bface.geometry);
      balign.matrix.connectTo(bface.matrix);
      // 箱と面
      bface.output.connectTo(bmerge.geometry0);
      bcompute.output.connectTo(bmerge.geometry1);

      bmerge.output.connectTo(out.geometry);
      ng.outputBlock = out;
    }
    ng.build();
    const mesh = ng.createMesh('ngmesh', scene);
    mesh.checkCollisions = true;
    const mtls = new BABYLON.MultiMaterial('mtl', scene);
    const mtl0 = new BABYLON.StandardMaterial('mtl0');
    mtl0.diffuseTexture = new BABYLON.Texture('textures/crate.png');
    const mtl1 = new BABYLON.StandardMaterial('mtl1');
    mtl1.diffuseColor = new BABYLON.Color3(0.8, 0.8, 0.2);
    mtls.subMaterials.push(mtl0);
    mtls.subMaterials.push(mtl1);
    mesh.material = mtls;
  }

  const camera = new BABYLON.UniversalCamera('camera1',
    new BABYLON.Vector3(0, 25, 0),
    scene);
  camera.inputs.removeByType('FreeCameraKeyboardMoveInput');
  camera.inputs.add(new HorizontalCameraKeyboardMoveInput());
  camera.checkCollisions = true;
  camera.applyGravity = true;
  camera.needMoveForGravity = true;
  camera.minZ = 0.02;
  camera.attachControl(false);
  return scene;
};
実行結果
run1.png
WASDキーで平行移動、Shift同時で速く移動、Ctrl同時でゆっくり移動します
対応するノード
node2.png
Node Geometry Editor で同じ構成のグラフを組んだ場合(ブロックの個別名は違います)

参照

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?