| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247 |
- import * as THREE from "three";
- import { Model } from "./model";
- import { loadVRMAnimation } from "@/lib/VRMAnimation/loadVRMAnimation";
- import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
- /**
- * three.jsを使った3Dビューワー
- *
- * setup()でcanvasを渡してから使う
- */
- export class Viewer {
- public isReady: boolean;
- public model?: Model;
- public _renderer?: THREE.WebGLRenderer;
- private _clock: THREE.Clock;
- private _scene: THREE.Scene;
- public _camera?: THREE.PerspectiveCamera;
- private _cameraControls?: OrbitControls;
- private _raycaster?: THREE.Raycaster;
- private _mouse?: THREE.Vector2;
- private sendScreenshotToCallback: boolean;
- private screenshotCallback: BlobCallback | undefined;
- constructor() {
- this.isReady = false;
- this.sendScreenshotToCallback = false;
- this.screenshotCallback = undefined;
- // scene
- const scene = new THREE.Scene();
- this._scene = scene;
- // light
- const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
- directionalLight.position.set(1.0, 1.0, 1.0).normalize();
- scene.add(directionalLight);
- const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
- scene.add(ambientLight);
- // animate
- this._clock = new THREE.Clock();
- this._clock.start();
- }
- public loadVrm(url: string) {
- if (this.model?.vrm) {
- this.unloadVRM();
- }
- // gltf and vrm
- this.model = new Model(this._camera || new THREE.Object3D());
- return this.model.loadVRM(url).then(async () => {
- if (!this.model?.vrm) return;
- // Disable frustum culling
- this.model.vrm.scene.traverse((obj) => {
- obj.frustumCulled = false;
- });
- this._scene.add(this.model.vrm.scene);
- const animation = await loadVRMAnimation("/animations/idle_loop.vrma")
- if (animation) this.model.loadAnimation(animation);
- // HACK: アニメーションの原点がずれているので再生後にカメラ位置を調整する
- requestAnimationFrame(() => {
- this.resetCamera();
- });
- });
- }
- public unloadVRM(): void {
- if (this.model?.vrm) {
- this._scene.remove(this.model.vrm.scene);
- this.model?.unLoadVrm();
- }
- }
- /**
- * Reactで管理しているCanvasを後から設定する
- */
- public setup(canvas: HTMLCanvasElement) {
- const parentElement = canvas.parentElement;
- const width = parentElement?.clientWidth || canvas.width;
- const height = parentElement?.clientHeight || canvas.height;
- // renderer
- this._renderer = new THREE.WebGLRenderer({
- canvas: canvas,
- alpha: true,
- antialias: true,
- });
- this._renderer.outputColorSpace = THREE.SRGBColorSpace;
- this._renderer.setSize(width, height);
- this._renderer.setPixelRatio(window.devicePixelRatio);
- // camera
- this._camera = new THREE.PerspectiveCamera(20.0, width / height, 0.1, 20.0);
- this._camera.position.set(0, 1.3, 1.5);
- this._cameraControls?.target.set(0, 1.3, 0);
- this._cameraControls?.update();
- // camera controls
- this._cameraControls = new OrbitControls(
- this._camera,
- this._renderer.domElement
- );
- this._cameraControls.screenSpacePanning = true;
- this._cameraControls.minDistance = 0.5;
- this._cameraControls.maxDistance = 4;
- this._cameraControls.update();
- // raycaster and mouse
- this._raycaster = new THREE.Raycaster();
- this._mouse = new THREE.Vector2();
- window.addEventListener("resize", () => {
- this.resize();
- });
- this.isReady = true;
- this.update();
- }
- /**
- * canvasの親要素を参照してサイズを変更する
- */
- public resize() {
- if (!this._renderer) return;
- const parentElement = this._renderer.domElement.parentElement;
- if (!parentElement) return;
- this._renderer.setPixelRatio(window.devicePixelRatio);
- this._renderer.setSize(
- parentElement.clientWidth,
- parentElement.clientHeight
- );
- if (!this._camera) return;
- this._camera.aspect =
- parentElement.clientWidth / parentElement.clientHeight;
- this._camera.updateProjectionMatrix();
- }
- public resizeChatMode(on: boolean){
- if (!this._renderer) return;
- const parentElement = this._renderer.domElement.parentElement;
- if (!parentElement) return;
- this._renderer.setPixelRatio(window.devicePixelRatio);
- let width = parentElement.clientWidth;
- let height = parentElement.clientHeight;
- if (on) {width = width/2; height = height/2; }
- this._renderer.setSize(
- width,
- height
- );
- if (!this._camera) return;
- this._camera.aspect =
- parentElement.clientWidth / parentElement.clientHeight;
- this._camera.updateProjectionMatrix();
- }
- /**
- * VRMのheadノードを参照してカメラ位置を調整する
- */
- public resetCamera() {
- const headNode = this.model?.vrm?.humanoid.getNormalizedBoneNode("head");
- if (headNode) {
- const headWPos = headNode.getWorldPosition(new THREE.Vector3());
- this._camera?.position.set(
- this._camera.position.x,
- headWPos.y,
- this._camera.position.z
- );
- this._cameraControls?.target.set(headWPos.x, headWPos.y, headWPos.z);
- this._cameraControls?.update();
- }
- }
- public resetCameraLerp() {
- // y = 1.3 is from initial setup position of camera
- const newPosition = new THREE.Vector3(
- this._camera?.position.x,
- 1.3,
- this._camera?.position.z
- );
- this._camera?.position.lerpVectors(this._camera?.position,newPosition,0);
- // this._cameraControls?.target.lerpVectors(this._cameraControls?.target,headWPos,0.5);
- // this._cameraControls?.update();
- }
- public update = () => {
- requestAnimationFrame(this.update);
- const delta = this._clock.getDelta();
- // update vrm components
- if (this.model) {
- this.model.update(delta);
- }
- if (this._renderer && this._camera) {
- this._renderer.render(this._scene, this._camera);
- if (this.sendScreenshotToCallback && this.screenshotCallback) {
- this._renderer.domElement.toBlob(this.screenshotCallback, "image/jpeg");
- this.sendScreenshotToCallback = false;
- }
- }
- };
- public onMouseClick(event: MouseEvent): boolean {
- if (!this._renderer || !this._camera || !this.model?.vrm) return false;
- const rect = this._renderer.domElement.getBoundingClientRect();
- // calculate mouse position in normalized device coordinates
- this._mouse!.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
- this._mouse!.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
- // update the picking ray with the camera and mouse position
- this._raycaster!.setFromCamera(this._mouse!, this._camera);
- // calculate objects intersecting the picking ray
- const intersects = this._raycaster!.intersectObject(this.model.vrm.scene, true);
- if (intersects.length > 0) {
- return true;
- }
- return false;
- }
- public getScreenshotBlob = (callback: BlobCallback) => {
- this.screenshotCallback = callback;
- this.sendScreenshotToCallback = true;
- };
- }
|