原文参考我的公众号文章 threejs实现右上角小地图

通过三番五次的尝试,总算是实现了小地图功能。尝试了几种形式,然而综合成果和性能来看,独立相机(OrthographicCamera)+独立渲染器(WebGLRenderer)实现小地图是最佳抉择。

这里间接上最佳实际,而后再顺次记录下其它尝试的大抵思路。

最佳实际

独立相机 + 独立渲染器

实现思路其实很简略,植入现有程序也很不便,来看下:

  • 第一步,创立地图像机

个别抉择正交相机,它的最大个性就是所有物体在它的眼里都是一样的显示比例,没有近大远小,很适宜 2D 小地图。像机的前四个参数决定了能看到主场景内多少范畴,越大看到的货色越多。

// 初始化小地图相机const mapSize = 20; //相机看到主场景的内容有多少const mapCamera = new OrthographicCamera(  -mapSize / 2,  mapSize / 2,  mapSize / 2,  -mapSize / 2,  1,  1000);mapCamera.position.set(0, 100, 0);mapCamera.lookAt(new THREE.Vector3(0, 0, 0));
  • 第二步,创立地图渲染器

创立小地图独立渲染器,这样能够和主程序的渲染器和流程解耦,也不便调整小地图在画面上的的显示地位。

// 初始化小地图渲染器const mapRenderSize = 200; //决定了小地图2D立体的css款式大小const mapRenderer = new WebGLRenderer({ alpha: true });mapRenderer.setSize(mapRenderSize, mapRenderSize);mapRenderer.setClearColor(0x7d684f);//上面渲染器相干设置最好搞下,不然显示成果会有点怪!mapRenderer.shadowMap.enabled = true;mapRenderer.shadowMap.type = PCFSoftShadowMap;mapRenderer.physicallyCorrectLights = true;mapRenderer.outputEncoding = sRGBEncoding;// 设置款式,并增加到HTMLmapRenderer.domElement.id = "mapcanvas";mapRenderer.domElement.style.position = "absolute";mapRenderer.domElement.style.right = "5px";mapRenderer.domElement.style.top = "5px";mapRenderer.domElement.style.zIndex = "1001";mapRenderer.domElement.style.border = "1px dashed #000";mapRenderer.domElement.style.transform = `rotateZ(${mapRotateZ}deg)`;mapRenderer.domElement.style.borderRadius = "16px";document.body.appendChild(mapRenderer.domElement);
  • 第三步,更新小地图相机地位与调用小地图渲染器渲染画面

为了让小地图上的画面与主程序画面同步,须要放弃小地图相机的地位与咱们的玩家(或主相机)地位同步。

// 更新地图相机地位和视点function updateMapCameraAndRender(){    let targetPos = player.position; //决定了小地图的观测中心点    mapCamera.position.set(      targetPos.x,      targetPos.y + 100,      targetPos.z    );    mapCamera.lookAt(targetPos.x, 2, targetPos.z);    // 渲染小地图    mapRenderer.render(this.scene, mapCamera);}animate(){    //主程序render...    //小地图render    updateMapCameraAndRender();}

实现以上三步,小地图就退出到了主程序中了,一句话来说就是:用一个独自的相机盯着指标场景,并用一个独立的渲染器实时渲染到一个新的 Dom 节点上。

上面是我封装好的小地图类,不便引入应用。

import {  OrthographicCamera,  WebGLRenderer,  PCFSoftShadowMap,  sRGBEncoding,  MathUtils,  ACESFilmicToneMapping,  Vector3,} from "three";export class MiniMap {  _miniMapCamera = null;  _miniMapRenderer = null;  _followTarget = null;  /**   * 初始化参数   * @param {Object} options   * @options.scene 主场景   * @options.target 小地图以之为中心点的3D指标   * @options.mapSize 决定了摄像机看到的内容大小,默认10   * @options.mapRenderSize 决定了小地图2D立体的大小,默认120   * @options.mapRotateZ number 小地图沿着Z轴(垂直屏幕)旋转角度,默认0   * @options.mapSyncRotateZ boolean 小地图沿着Z轴(垂直屏幕)是否跟着一起target旋转,默认false   */  constructor(    options = {      scene,      target,      mapSize,      mapRenderSize,      mapRotateZ,      mapSyncRotateZ,    }  ) {    this.scene = options.scene;    this.mapSize = options.mapSize || 10;    this.mapRenderSize = options.mapRenderSize || 120;    this.mapRotateZ = options.mapRotateZ || 0;    this.mapSyncRotateZ = options.mapSyncRotateZ || false;    this._followTarget = options.target;    if (!this.scene) {      throw new Error("scene不能为空");    }    if (!this._followTarget) {      throw new Error("target不能为空,示意小地图画面次要追随对象");    }    this.add();  }  add() {    const { mapSize, mapRenderSize, mapRotateZ } = this;    // 初始化小地图渲染器    const mapRenderer = new WebGLRenderer({ alpha: true });    mapRenderer.setSize(mapRenderSize, mapRenderSize);    // mapRenderer.setClearColor(0x7d684f);    mapRenderer.shadowMap.enabled = true;    mapRenderer.shadowMap.type = PCFSoftShadowMap;    mapRenderer.physicallyCorrectLights = true;    mapRenderer.outputEncoding = sRGBEncoding;    // mapRenderer.toneMapping = ACESFilmicToneMapping; //电影渲染成果    // mapRenderer.toneMappingExposure = 0.6;    this._miniMapRenderer = mapRenderer;    // 设置款式,并增加到HTML    mapRenderer.domElement.id = "mapcanvas";    mapRenderer.domElement.style.position = "absolute";    mapRenderer.domElement.style.right = "5px";    mapRenderer.domElement.style.top = "5px";    mapRenderer.domElement.style.zIndex = "1001";    mapRenderer.domElement.style.border = "1px dashed #000";    mapRenderer.domElement.style.transform = `rotateZ(${mapRotateZ}deg)`;    mapRenderer.domElement.style.borderRadius = "16px";    this._miniMapDomEl = mapRenderer.domElement;    document.body.appendChild(mapRenderer.domElement);    // 初始化小地图相机    const mapCamera = new OrthographicCamera(      -mapSize / 2,      mapSize / 2,      mapSize / 2,      -mapSize / 2,      1,      1000    ); //在这种投影模式下,无论物体间隔相机距离远或者近,在最终渲染的图片中物体的大小都放弃不变。这对于渲染2D场景或者UI元素是十分有用的。    this._miniMapCamera = mapCamera;    // 更新地图相机地位和朝向    this.updateCamera();  }  updateCamera() {    // 更新小地图css旋转角度,与玩家同步    if (this.mapSyncRotateZ) {      let targetRotateY = MathUtils.radToDeg(this._followTarget.rotation.y);      this._miniMapDomEl.style.transform = `rotateZ(${        this.mapRotateZ + targetRotateY      }deg)`;    }    // 更新地图相机地位和朝向    let targetPos = this._followTarget.position;    this._miniMapCamera.position.set(      targetPos.x,      targetPos.y + 10,      targetPos.z    );    this._miniMapCamera.lookAt(targetPos.x, 3, targetPos.z);  }  update() {    // 更新地图相机地位和朝向    this.updateCamera();    // 渲染小地图    this._miniMapRenderer.render(this.scene, this._miniMapCamera);  }}

应用示例:

import { MiniMap } from "./scripts/MiniMap";this.miniMap = new MiniMap({  target: this.player,  scene: this.scene,  mapSize: 12,  mapRenderSize: 160,});animate() {    // 主程序代码....    // 更新小地图    this.miniMap.update();}

其它尝试

  • v1 实现:OrthographicCamera + Canvas 将渲染器的内容以图片的模式绘制到 Canvas 元素上。体验地址 1

毛病:每次绘制都是一次图片加载,页面申请量无限大!而且会有提早,因为图片加载过程是须要网络传输工夫的,这样就导致小地图卡顿!

  • v2 实现:在界面上新建一块区域作为小地图,贴上场景地图的贴纸,再绘制一个标记代表玩家。主场景中的玩家挪动时,将地位同步给小地图上的玩家标记。体验地址 2

长处:能够自定义小地图的款式和玩家标记的款式,因为个别小地图只是事实抽象的地形、物体、玩家地位标记!

毛病:因为小地图的渲染和主业务的渲染是在同一个相机下显示的,当镜头视角切换、缩放等操作会影响小地图的地位和大小,因而须要编写简单的逻辑去实时更新小地图的地位和渲染大小,有肯定难度!

  • v3 实现:独立相机(OrthographicCamera)+独立渲染器(SVGRenderer)

成果也很特地,且因为是以 svg 的模式展现小地图,所以能够通过 js 和 css 管制 svg 节点。只不过比照下来大场景下性能没那么好 体验地址 3