Threejs入门02 使用视角指示器 viewHelper

效果如图,同threejs的editor中右下角的视角指示器。

一、代码调用、修改

1.1 代码目录:

html 复制代码
- utils
    - threeworld
        - viewHelper
            - baseViewHelper.js
            - viewHelper.ts
        - threeWorld.ts
- views
    - demo1.vue

1.2 引入并修改viewHelper

在上一篇文的基础上,引入【baseViewHelper.js】(three.js/examples/jsm/helpers/ViewHelper.js),同时增加文件viewHelper.ts

ts 复制代码
// @ts-nocheck
import { ViewHelper as ViewHelperBase } from './baseViewHelper.js';

class ViewHelper extends ViewHelperBase {
  animating: any;
  update(delta: number) {
    throw new Error('Method not implemented.');
  }

	private panel: HTMLDivElement | undefined;

	constructor( editorCamera: THREE.PerspectiveCamera, container: HTMLDivElement ) {

		super( editorCamera, container );

		this.panel = document.createElement('div');
		this.panel.style.cssText = `position: absolute; bottom: 0; right: 0; z-index: 33; width: 128px; height: 128px`;
		container.style.position = 'relative';
		container.appendChild(this.panel);
		
		this.panel.addEventListener('pointerup', this.onPointerup.bind(this));
		this.panel.addEventListener('pointerdown', this.onPointerdown.bind(this));

	}

	onPointerup(event: MouseEvent): void {
    event.stopPropagation();
		this.handleClick(event);
  }

  onPointerdown(event: MouseEvent): void {
    event.stopPropagation();
  }

}

export { ViewHelper };

接下来,就在上一篇文的threeWorld.ts中增加这个viewHelper实例。(下面只更新需要改变的函数)

ts 复制代码
export default class ThreeWorld {
  private clock: THREE.Clock = new THREE.Clock();

  init(): void {
    
    // ...
    this.setRenderer();
    this.addHelper();

    this.addControls();
    // ...
    
  }


  render(): void {
    // ...
    if (this.viewHelper && this.renderer) {
      this.viewHelper?.render(this.renderer);
    }
  }

  animnate(): void {
    
    const delta = this.clock.getDelta();

    this.render();
    this.orbitControls?.update();
    if (this.viewHelper) {
      
      if (this.viewHelper.animating) {
        this.viewHelper.update(delta)
      }
    }
    
    requestAnimationFrame(this.animnate.bind(this));
  }

  loadModel(modelPath: string) {
    return new Promise(resolve => {
      this.gltfLoader?.load(modelPath, (gltf) => {
      
        const obj = gltf.scene;        

        this.scene?.add(obj);
        const crood = this.center(obj);
        this.updateControls(crood);
        
        resolve(obj);
      }, process => {}, err => {})
    })
  }

  destroy() {
    // @ts-ignore
    this.viewHelper?.dispose();
  }

  addControls(): void {
    if  (this.dom && this.camera && this.renderer) {
      this.orbitControls = new OrbitControls(this.camera, this.renderer?.domElement);
      this.orbitControls.update();
    }
  }

  updateControls(crood: THREE.Vector3): void {
    const { x, y, z } = crood;
    this.orbitControls?.target.set(x, y, z);
    this.orbitControls?.update();
  }

  addHelper(): void {
    if (this.dom && this.camera && this.renderer) {
      this.viewHelper = new ViewHelper(this.camera, this.dom);
      this.renderer.autoClear = false; // mark0
    }
  }

}

另外,注意mark0标记处的那行代码,记得加上,

1.3 demo修改

最后就可以调用了~~~

vue 复制代码
<template>
  <div class="demo1">
    <!-- load model by gltf loader -->
  </div>
</template>

<script lang="ts" setup>
import { onMounted, onUnmounted, ref, shallowReactive,  } from 'vue';
import ThreeWorld from '@/utils/threeWorld/threeWorld';
import type {  ShallowReactive  } from 'vue';


let w: ShallowReactive<ThreeWorld>;

onMounted(() => {
  w = shallowReactive(new ThreeWorld({
    dom: document.querySelector('div.demo1'),
  }))

  w.loadModel(`./static/models/charger/charger003.gltf`).then(res => {
    w.animnate()
  });

})

onUnmounted(() => {
  w.destroy();
})
</script>


<style scoped>
.demo1 {
  width: 500px;
  height: 600px;
}
</style>

二、baseViewHelper做了什么(源码阅读)

baseViewHelper 继承并拓展了Object3D类

2.1 绘制基本的图形

js 复制代码
class ViewHelper extends Object3D {
  constructor( camera, domElement ) {
    // 一、绘制x、y、z 3条轴,即, xAxis、yAxis、zAxis


    // 二、绘制6个圆点:posXAxisHelper为有轴的圆点,negXAxisHelper为反方向、没有轴、相对小为0.8且目前透明度为0.5的圆点,即,posXAxisHelper、posYAxisHelper、posZAxisHelper、negXAxisHelper、negYAxisHelper、negZAxisHelper
  } 
}

2.2 监听方位点击事件

主要是采取raycaster进行鼠标拾取,官网相关api

下文中通过mouse记录鼠标点击的位置,接着通过raycaster.setFromCamera(mouse, orthoCamera)增加鼠标位置至相机的光线投射,然后通过raycaster.intersectObjects(interactiveObjects) 检测和射线相交的几个方位圆点。若存在结果,则this.animating = true,取第一个(距离最近)执行prepareAnimationData函数。

ts 复制代码
this.handleClick = function ( event ) {

    if ( this.animating === true ) return false;

    const rect = domElement.getBoundingClientRect();
    const offsetX = rect.left + ( domElement.offsetWidth - dim );
    const offsetY = rect.top + ( domElement.offsetHeight - dim );
    mouse.x = ( ( event.clientX - offsetX ) / ( rect.right - offsetX ) ) * 2 - 1;
    mouse.y = - ( ( event.clientY - offsetY ) / ( rect.bottom - offsetY ) ) * 2 + 1;

    
    // 获取鼠标点击的物体:raycaster拾取相关api
    raycaster.setFromCamera( mouse, orthoCamera );

    const intersects = raycaster.intersectObjects( interactiveObjects );

    if ( intersects.length > 0 ) {

      const intersection = intersects[ 0 ]; // 检测所有在射线与物体之间,包括或不包括后代的相交部分。返回结果时,相交部分将按距离进行排序,最近的位于第一个。
      const object = intersection.object;

      prepareAnimationData( object, this.center ); // 判断点击视角示意图中的哪个方位点,记录当前位置的四元数q1,目标四元数q2

      this.animating = true;

      return true;

    } else {

      return false;

    }

  };

2.3 prepareAnimationData

这个函数顾名思义,准备animation前的相关要素。

  1. 通过switch之前存储在圆点中的type,判断是点击视角示意图中的哪个方位;
  2. 用q1、q2两个四元数分别存储每一帧绘制中相机当下camera.position目标targetPosition的position信息等,
ts 复制代码
function prepareAnimationData( object, focusPoint ) {

  switch ( object.userData.type ) {

    case 'posX':
      targetPosition.set( 1, 0, 0 );
      targetQuaternion.setFromEuler( new Euler( 0, Math.PI * 0.5, 0 ) );
      break;

    case 'posY':
      targetPosition.set( 0, 1, 0 );
      targetQuaternion.setFromEuler( new Euler( - Math.PI * 0.5, 0, 0 ) );
      break;

    case 'posZ':
      targetPosition.set( 0, 0, 1 );
      targetQuaternion.setFromEuler( new Euler() );
      break;

    case 'negX':
      targetPosition.set( - 1, 0, 0 );
      targetQuaternion.setFromEuler( new Euler( 0, - Math.PI * 0.5, 0 ) );
      break;

    case 'negY':
      targetPosition.set( 0, - 1, 0 );
      targetQuaternion.setFromEuler( new Euler( Math.PI * 0.5, 0, 0 ) );
      break;

    case 'negZ':
      targetPosition.set( 0, 0, - 1 );
      targetQuaternion.setFromEuler( new Euler( 0, Math.PI, 0 ) );
      break;

    default:
      console.error( 'ViewHelper: Invalid axis.' );

  }

  radius = camera.position.distanceTo( focusPoint );
  targetPosition.multiplyScalar( radius ).add( focusPoint );

  dummy.position.copy( focusPoint );

  dummy.lookAt( camera.position ); 
  q1.copy( dummy.quaternion );

  dummy.lookAt( targetPosition );
  q2.copy( dummy.quaternion );

}

2.4 更新每一帧的信息

  1. 执行update函数的时机:在demo中调用requestAnimationFrame时。这里的delta传参是Clock.getDelta(),这个函数是用于获得前后两次执行的该方法的时间间隔。
  2. 采用quaternion.rotateTowards将当下的四元数q1按照step向目标q2进行旋转,该方法确保最终的四元数不会超过q2
ts 复制代码
this.update = function (delta) {

  const step = delta * turnRate;

  // animate position by doing a slerp and then scaling the position on the unit sphere
  // 将该四元数q1按照步长 step 向目标 q2 进行旋转。该方法确保最终的四元数不会超过 q2。
  q1.rotateTowards( q2, step );

  // applyQuaternion( q1 ).multiplyScalar( radius ).add( this.center ) ,即 旋转 * 缩放 + 中心点
  camera.position.set( 0, 0, 1 ).applyQuaternion( q1 ).multiplyScalar( radius ).add( this.center );

  // animate orientation
  // 相机视角旋转
  camera.quaternion.rotateTowards( targetQuaternion, step );

  if ( q1.angleTo( q2 ) === 0 ) {

    this.animating = false;

  }

};

2.5 绘制每一帧的画面

每一帧清除缓存,绘制相关画面:判断旋转角度,改变相关的方位点的透明度。

ts 复制代码
this.render = function ( renderer ) {

  // quaternion表示对象局部旋转的Quaternion(四元数),此处用来获取相机的旋转信息
  // quaternion.invert(),翻转该四元数 ------ 计算 conjugate 。假定该四元数具有单位长度。
  // quaternion.conjugate()返回该四元数的旋转共轭。 四元数的共轭表示的是,围绕旋转轴在相反方向上的相同旋转。
  this.quaternion.copy( camera.quaternion ).invert(); 
  this.updateMatrixWorld(); // 更新子对象的matrixWorld

  point.set( 0, 0, 1 );
  point.applyQuaternion( camera.quaternion ); // 应用四元数

  // 旋转过程中,<0的透明度设为0.5
  if ( point.x >= 0 ) {

    posXAxisHelper.material.opacity = 1;
    negXAxisHelper.material.opacity = 0.5;

  } else {

    posXAxisHelper.material.opacity = 0.5;
    negXAxisHelper.material.opacity = 1;

  }

  if ( point.y >= 0 ) {

    posYAxisHelper.material.opacity = 1;
    negYAxisHelper.material.opacity = 0.5;

  } else {

    posYAxisHelper.material.opacity = 0.5;
    negYAxisHelper.material.opacity = 1;

  }

  if ( point.z >= 0 ) {

    posZAxisHelper.material.opacity = 1;
    negZAxisHelper.material.opacity = 0.5;

  } else {

    posZAxisHelper.material.opacity = 0.5;
    negZAxisHelper.material.opacity = 1;

  }

  // canvas绘制rect 相关api
  const x = domElement.offsetWidth - dim;

  renderer.clearDepth();

  renderer.getViewport( viewport );
  renderer.setViewport( x, 0, dim, dim );

  renderer.render( this, orthoCamera );

  renderer.setViewport( viewport.x, viewport.y, viewport.z, viewport.w );

};

2.6 废置对象

废置对象有提高性能,并避免程序中的内存泄露,可见官网相关api。 three.js不能够自动废置对象,需要我们手动在组件卸载时废置。

ts 复制代码
this.dispose = function () {}

2.7 动画的开关

baseViewHelper通过animating布尔值来设定是否开始动画:

  1. 默认animating=false
  2. 用户点击画面时,若当前正在动画,不作处理
  3. 用户点击方位点时,开启动画if ( intersects.length > 0 ) {this.animating = true;}
  4. 动画旋转到目标位置时,关闭动画if(q1.angleTo( q2 ) === 0) { this.animating = false; }

2.8 如何控制每次旋转动画物体的大小不变

通过radius这个变量来控制:

ts 复制代码
let radius = 0;

function prepareAnimationData( object, focusPoint ) {
  radius = camera.position.distanceTo( focusPoint );
	targetPosition.multiplyScalar( radius ).add( focusPoint );
}

this.update = function ( delta ) {
  camera.position.set( 0, 0, 1 ).applyQuaternion( q1 ).multiplyScalar( radius ).add( this.center );
}

三、问题记录

  1. 添加viewHelper之后 viewHelper.render()和renderer.render()发生冲突,哪个在后面就渲染谁,要么场景全黑有helper,要么没有helper? 解决方法:添加mark0标记处那句代码。 原理:执行渲染器WebGLRenderer的渲染方法.render(),本质上就相当于调用绘制函数gl.drawArrays(),每执行一次渲染方法.render()帧缓冲区都会得到新的片元(像素)数据,覆盖帧缓冲区原来的像素数据,或者说每执行一次渲染方法.render(),canvas画布更新一次,canvas画布上上次执行.render()得到像素被覆盖。
相关推荐
WebGISer_白茶乌龙桃36 分钟前
Cesium实现“悬浮岛”式,三维立体的行政区划
javascript·vue.js·3d·web3·html5·webgl
烛阴12 小时前
拒绝配置地狱!5 分钟搭建 Three.js + Parcel 完美开发环境
前端·webgl·three.js
WebGISer_白茶乌龙桃1 天前
Vue3 + Mapbox 加载 SHP 转换的矢量瓦片 (Vector Tiles)
javascript·vue.js·arcgis·webgl
XiaoYu20024 天前
第9章 Three.js载入模型GLTF
前端·javascript·three.js
XiaoYu20025 天前
第8章 Three.js入门
前端·javascript·three.js
ThreePointsHeat5 天前
Unity WebGL打包后启动方法,部署本地服务器
unity·游戏引擎·webgl
林枫依依7 天前
电脑配置流程(WebGL项目)
webgl
冥界摄政王8 天前
CesiumJS学习第四章 替换指定3D建筑模型
3d·vue·html·webgl·js·cesium
温宇飞10 天前
高效的线性采样高斯模糊
javascript·webgl
冥界摄政王11 天前
Cesium学习第一章 安装下载 基于vue3引入Cesium项目开发
vue·vue3·html5·webgl·cesium