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()得到像素被覆盖。
相关推荐
程序员_三木6 小时前
Three.js入门-Raycaster鼠标拾取详解与应用
开发语言·javascript·计算机外设·webgl·three.js
汪洪墩1 天前
【Mars3d】设置backgroundImage、map.scene.skyBox、backgroundImage来回切换
开发语言·javascript·python·ecmascript·webgl·cesium
m0_748234341 天前
webGL硬核知识:图形渲染管渲染流程,各个阶段对应的API调用方式
图形渲染·webgl
MossGrower2 天前
36. Three.js案例-创建带光照和阴影的球体与平面
3d图形·webgl·three.js·光照与阴影
MossGrower2 天前
34. Three.js案例-创建球体与模糊阴影
webgl·three.js·3d渲染·阴影效果
广东数字化转型3 天前
Three.js相机Camera控件知识梳理
3d·three.js
关山月4 天前
9个学习着色器的GLSL示例
前端·three.js
程序员_三木4 天前
Three.js资源-贴图材质网站推荐
javascript·webgl·three.js·材质·贴图
程序员_三木4 天前
React和Three.js结合-React Three Fiber
前端·javascript·react.js·前端框架·webgl·材质
MossGrower4 天前
37. Three.js案例-绘制部分球体
3d图形·webgl·three.js·球体几何体