css3d实现透视相机操作

273 阅读3分钟

CSS3D实现相机移动旋转

  • 矩阵计算使用的是 glmatrix : glmatrix.net/
  • 实现要渲染的物体的基本结构,可以进行平移、旋转、缩放操作。
export class CSS3DObject {
  /**
   * @type {boolean}
   */
  active = true;
  /**
   * @type { ArrayLike<number> | ArrayBufferLike<number> }
   */
  #localMat = mat4.create();
  /**
   * @type { ArrayLike<number> | ArrayBufferLike<number> }
   */
  #worldMat = mat4.create();
  /**
   * @type {CSS3DObject}
   */
  #parent = null;
  /**
   * @type {CSS3DObject[]}
   */
  #children = [];

  get children() {
    return this.#children;
  }
  /**
   * @type {ArrayLike<number>|ArrayBufferLike<number>}
   */
  #rotation = quat.create();
  /**
   * @type {ArrayLike<number>|ArrayBufferLike<number>}
   */
  #position = vec3.create();
  /**
   * @type {ArrayLike<number>|ArrayBufferLike<number>}
   */
  #scale = vec3.create();
  /**
   * @type {boolean}
   */
  #needUpdateLocal = false;
  /**
   * @type {boolean>}
   */
  #needUpdateWorld = false;

  /**
   * @type {HTMLElement}
   */
  element = null;

  /**
   *
   * @param { HTMLElement } element
   */
  constructor(element) {
    this.element = element;
    vec3.set(this.#scale, 1, 1, 1);
  }

  setPosition(pos) {
    if (vec3.equals(pos, this.#position)) return;
    vec3.copy(this.#position, pos);
    this.#needUpdateLocal = true;
  }

  setScale(scale) {
    if (vec3.equals(scale, this.#scale)) return;
    vec3.copy(this.#scale, scale);
    this.#needUpdateLocal = true;
  }

  translate(val) {
    vec3.add(this.#position, this.#position, val);
    this.#needUpdateLocal = true;
  }

  rotate(angle, axis) {
    const q = quat.create();
    quat.setAxisAngle(q, axis, glMatrix.toRadian(angle));
    quat.normalize(q, q);
    quat.mul(this.#rotation, q);
    this.#needUpdateLocal = true;
  }

  rotateX(val) {
    quat.rotateX(this.#rotation, this.#rotation, glMatrix.toRadian(val));
    quat.normalize(this.#rotation, this.#rotation);
    this.#needUpdateLocal = true;
  }

  rotateY(val) {
    quat.rotateY(this.#rotation, this.#rotation, glMatrix.toRadian(val));
    quat.normalize(this.#rotation, this.#rotation);
    this.#needUpdateLocal = true;
  }

  rotateZ(val) {
    quat.rotateZ(this.#rotation, this.#rotation, glMatrix.toRadian(val));
    quat.normalize(this.#rotation, this.#rotation);
    this.#needUpdateLocal = true;
  }

  getLocalMatrix() {
    if (this.#needUpdateLocal) this.#calcLocalMatrix();
    return this.#localMat;
  }

  getWorldMatrix(force = false) {
    this.#calcWorldMatrix(force);
    return this.#worldMat;
  }

  #calcLocalMatrix() {
    mat4.fromRotationTranslationScale(
      this.#localMat,
      this.#rotation,
      this.#position,
      this.#scale
    );
    this.#needUpdateLocal = false;
    this.#needUpdateWorld = true;
  }

  /**
   *
   * @param {} mat parent  WorldMatrix
   */
  #calcWorldMatrix(force = false) {
    const local = this.getLocalMatrix();
    if (force) {
      if (this.#parent === null) {
        mat4.copy(this.#worldMat, local);
      } else {
        mat4.mul(this.#worldMat, this.#parent.getWorldMatrix(true), local);
      }
    } else {
      if (this.#needUpdateWorld) {
        if (this.#parent === null) {
          mat4.copy(this.#worldMat, local);
        } else {
          mat4.mul(this.#worldMat, this.#parent.getWorldMatrix(), local);
        }
        for (let i = 0; i < this.#children.length; i++) {
          this.#children[i].#needUpdateWorld = true;
        }
        this.#needUpdateWorld = false;
      }
    }
  }

  /**
   *
   * @param { CSS3DObject } obj
   */
  setParent(obj) {
    if (this.#parent === obj) return;
    let parent = this.#parent;
    if (parent) {
      parent.removeChild(this);
    }
    if (obj) {
      obj.addChild(this);
    }
    this.#parent = obj;
  }

  /**
   *
   * @param {CSS3DObject} obj
   */
  addChild(obj) {
    if (this.#children.indexOf(obj) < 0) {
      this.#children.push(obj);
      obj.setParent(this);
    }
  }

  /**
   *
   * @param {CSS3DObject} obj
   */
  removeChild(obj) {
    let idx = this.#children.indexOf(obj);
    this.removeChildAt(idx);
  }

  /**
   *
   * @param {number} idx
   */
  removeChildAt(idx) {
    if (idx < 0 || idx >= this.#children.length) return;
    this.#children[idx].setParent(null);
    this.#children.splice(idx, 1);
  }
}

  • 实现场景类,用于遍历所有物体
xport class CSS3DScene extends CSS3DObject {
  /**
   * @type {WeakMap<any,string>}
   */
  #objects = new WeakMap();

  /**
   *
   * @param {(obj:CSS3DObject)=>void} func
   */
  tranverse(func) {
    /**
     *
     * @param {CSS3DObject} obj
     */
    function foreach(obj) {
      for (let i = 0; i < obj.children.length; i++) {
        if (!obj.active) continue;
        func(obj.children[i]);
        foreach(obj.children[i]);
      }
    }

    func(this);
    foreach(this);
  }
}
export class CSS3DCamera extends CSS3DObject {
  #fovy = 45;
  #viewMatrix = mat4.create();
  #near = 1;

  constructor(element) {
    super(element);
    this.element.style.transformStyle = "preserve-3d";
  }

  setFovY(fovy) {
    this.#fovy = fovy;
  }

  getPersperctive() {
    return 1 / Math.tan(glMatrix.toRadian(this.#fovy / 2));
  }

  getViewMatrix(force = false) {
    mat4.invert(this.#viewMatrix, this.getWorldMatrix(force));
    return this.#viewMatrix;
  }
}
  • 实现场景遍历渲染
export class Css3DRenderer {
  /**
   * @type {HTMLElement}
   */
  #dom = null;
  #width = 100;
  #height = 100;

  constructor(width, height, dom) {
    this.#width = width;
    this.#height = height;
    this.#dom = dom || document.createElement("div");
    if (!dom) {
      this.#dom.style.width = width;
      this.#dom.style.height = height;
      document.body.appendChild(this.#dom);
    }
  }

  resize(width, height) {
    this.#width = width;
    this.#height = height;
  }

  /**
   *
   * @param {CSS3DScene} scene
   * @param {CSS3DCamera} camera
   */
  render(scene, camera) {
    const vm = camera.getViewMatrix(true);
    if (camera.element.parentElement !== this.#dom) {
      camera.element.parentElement = this.#dom;
    }

    const fov = (camera.getPersperctive() * this.#height) / 2;
    this.#dom.style.perspective = `${fov}px`;
    camera.element.style.transform = `translateZ(${fov}px) matrix3d(${
      vm[0]
    },${-vm[1]},${vm[2]},${vm[3]},${vm[4]},${-vm[5]},${vm[6]},${vm[7]},${
      vm[8]
    },${-vm[9]},${vm[10]},${vm[11]},${vm[12]},${-vm[13]},${vm[14]},${
      vm[15]
    })  translate(${this.#width / 2}px,${this.#height / 2}px)`;
    scene.tranverse(function (obj) {
      const m = obj.getWorldMatrix();
      if (obj.element !== null) {
        obj.element.style.transform = `translate(-50%,-50%)  matrix3d(${m[0]},${
          m[1]
        },${m[2]},${m[3]},${-m[4]},${-m[5]},${-m[6]},${-m[7]},${m[8]},${m[9]},${
          m[10]
        },${m[11]},${m[12]},${m[13]},${m[14]},${m[15]})`;
        if (obj.element.parentElement !== camera.element) {
          obj.element.parentElement = camera.element;
        }
      }
    });
  }
}
  • 最后简单的实现个测试用例。
/*main.js*/
import { CSS3DCamera, CSS3DObject, Css3DRenderer, CSS3DScene } from "./css3d.js";

const scene = new CSS3DScene(null);
const camera = new CSS3DCamera(document.querySelector(".camera"));

const renderer = new Css3DRenderer(
  window.innerWidth,
  window.innerHeight,
  document.querySelector(".scene")
);

function resize() {
  renderer.resize(window.innerWidth, window.innerHeight);
}

const group = new CSS3DObject(null),
  front = new CSS3DObject(document.querySelector(".front")),
  back = new CSS3DObject(document.querySelector(".back")),
  left = new CSS3DObject(document.querySelector(".left")),
  right = new CSS3DObject(document.querySelector(".right")),
  top = new CSS3DObject(document.querySelector(".top")),
  down = new CSS3DObject(document.querySelector(".down"));

front.setPosition([0, 0, 50]);
group.addChild(front);

back.rotateY(180);
back.setPosition([0, 0, -50]);
group.addChild(back);

left.rotateY(-90);
left.setPosition([-50, 0, 0]);
group.addChild(left);

right.rotateY(90);
right.setPosition([50, 0, 0]);
group.addChild(right);

top.rotateX(-90);
top.setPosition([0, 50, 0]);
group.addChild(top);

down.rotateX(90);
down.setPosition([0, -50, 0]);
group.addChild(down);

scene.addChild(group);

renderer.render(scene, camera);

window.addEventListener("resize", resize);

camera.setPosition([0, 0, 1000]);

class Input {
  /**
   * @type {Map<string,boolean>}
   */
  static keyMap = new Map();
  static isPressing(key) {}
}

let moveSpeed = 10,
  rotateSpeed = 1;

/**
 *
 * @param {KeyBoardEvent} e
 */
window.onkeypress = function (e) {
  if (e.code === "KeyA") {
    pressA = true;
  } else if (e.code === "KeyD") {
    pressD = true;
  } else if (e.code === "KeyW") {
    pressW = true;
  } else if (e.code === "KeyS") {
    pressS = true;
  } else if (e.code === "KeyQ") {
    pressQ = true;
  } else if (e.code === "KeyE") {
    pressE = true;
  }
};

window.onkeyup = function (e) {
  if (e.code === "KeyA") {
    pressA = false;
  } else if (e.code === "KeyD") {
    pressD = false;
  } else if (e.code === "KeyW") {
    pressW = false;
  } else if (e.code === "KeyS") {
    pressS = false;
  } else if (e.code === "KeyQ") {
    pressQ = false;
  } else if (e.code === "KeyE") {
    pressE = false;
  }
};

let pressA = false,
  pressD = false,
  pressW = false,
  pressS = false,
  pressQ = false,
  pressE = false;

function updateCamera() {
  if (pressA) {
    camera.translate([moveSpeed, 0, 0]);
  }
  if (pressD) {
    camera.translate([-moveSpeed, 0, 0]);
  }
  if (pressW) {
    camera.translate([0, -moveSpeed, 0]);
  }
  if (pressS) {
    camera.translate([0, moveSpeed, 0]);
  }
  if (pressQ) {
    camera.rotateY(rotateSpeed);
  }
  if (pressE) {
    camera.rotateY(-rotateSpeed);
  }
}

function animate() {
  group.rotateX(1);
  updateCamera();
  renderer.render(scene, camera);

  requestAnimationFrame(animate);
}

animate();

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Game</title>
    <link rel="stylesheet" href="./main.css" />
  </head>
  <body>
    <div class="scene">
      <div class="camera">
        <div class="plane front"></div>
        <div class="plane back"></div>
        <div class="plane left"></div>
        <div class="plane right"></div>
        <div class="plane top"></div>
        <div class="plane down"></div>
      </div>
    </div>

    <script type="module" src="./main.js"></script>
  </body>
</html>
/*main.css*/
:root {
  font-size: calc(1em + 1vw);
  box-sizing: border-box;
}

:root,
::before,
::after {
  box-sizing: inherit;
}

body {
  margin: 0;
  padding: 0;
}

.scene {
  overflow: hidden;
  width: 100vw;
  height: 100vh;
  perspective: 200px;
}

.camera {
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  transform: translateZ(-200px) rotateX(30px);
}

.plane {
  display: block;
  position: absolute;
  width: 100px;
  height: 100px;
}

.front {
  background-color: red;
  transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 50, 1);
}

.back {
  background-color: green;
  transform: matrix3d(-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1, 0, 0, 0, -50, 1);
}

.left {
  background-color: blue;
  transform: matrix3d(0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, 0, -50, 0, 0, 1);
}

.right {
  background-color: #ff0;
  transform: matrix3d(0, 0, -1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 50, 0, 0, 1);
}

.top {
  background-color: #0ff;
  transform: matrix3d(1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, -50, 0, 1);
}

.down {
  background-color: #f0f;
  transform: matrix3d(1, 0, 0, 0, 0, 0, -1, 0, 0, 1, 0, 0, 0, 50, 0, 1);
}