前言
在之前的文章,也提过很多次物体遮挡的案例,当时用的都是以THREE.Ray
射线检测为基础,通过摄像头到目标体创建一条射线,通过检测射线是否经过被判断物体,从而决定被判断物体是否隐藏
其中这篇文章介绍的比较详细,感兴趣的同学可以看一看# threejs 打造 world.ipanda.com 同款3D首页
最近threejs更新了nodes,提供了WebGPURenderer
,由于没有官方文档,所以相关的用法和api都需要通过阅读源码,增加了学习成本,今天就以nodes提供的一些api写一个另一种判断遮挡的方法。
在 R158版本中,Renderer提供了 isOccluded
方法,这是判断遮挡的核心方法,下面我们就一起实现一下,其中在遇到的坑也一起看一看,本篇文章比较长比较细,涉及大量源码,又不能把源码全部贴上来,所以过程可能有点跳。
PS:如对源码不感兴趣,可直接跳到实践章节
理论
升级
我是从R.155升级到R.157,以下内容就都将以这个版本开始
R.158的问题
源码中node_modules/three/examples/jsm/nodes/accessors/ModelNode.js
js
export const modelViewMatrix = nodeImmutable( ModelNode, ModelNode.VIEW_MATRIX ).temp( 'ModelViewMatrix' );
报temp
这个方法找不到,实在没找到具体怎么解决,有可能是版本太新,这里还有bug,所以退而求其次,降级到R.157了
R.157的问题
threejs这次更新,动作挺大,但是需要适配的地方也挺多的
我用的是vite框架,threejs中有一个源码文件中将await直接暴露在顶层,并没有包裹async,导致框架报错,所以要再安装一个支持await可以暴露再顶层的插件 vite-plugin-top-level-await
csharp
yarn add vite-plugin-top-level-await
vite.config.ts
配置
js
import topLevelAwait from 'vite-plugin-top-level-await'
export default defineConfig({
...
plugins: [topLevelAwait({
promiseExportName: '__tla',
promiseImportName: i => `__tla_${i}`
})],
})
其中用到的有两个地方是直接将await暴露出来的,如果小伙伴有其他好的方式,也可以交流一下。
bash
node_modules/three/examples/jsm/capabilities/WebGPU.js
node_modules/three/examples/jsm/renderers/webgpu/WebGPUBackend.js
ini
if ( navigator.gpu !== undefined ) {
_staticAdapter = await navigator.gpu.requestAdapter();
}
WebGPUBackend中增加了判断gpu适配器核心代码,所以我们在写代码时要提前判断一下,否则会报错。
js
hasFeature( name ) {
const adapter = this.adapter || _staticAdapter;
//
const features = Object.values( GPUFeatureName );
if ( features.includes( name ) === false ) {
throw new Error( 'THREE.WebGPURenderer: Unknown WebGPU GPU feature: ' + name );
}
//
return adapter.features.has( name );
}
实践
遮挡判断/createScene/index.ts文件中提供一个创建基础场景的类T
,其中定义了摄像机、场景、控制器、灯光等
在构造函数中添加判断浏览器是否支持WebGPU
的判断
ts
if (WebGPU.isAvailable() === false && WebGL.isWebGL2Available() === false) {
throw new Error('No WebGPU or WebGL2 support');
}
创建WebGPURenderer
在源码中,WebGPURenderer继承了THREE.Renderer,支持renderer的所有方法,所以可以大胆的使用class WebGPURenderer extends Renderer
typescript
import WebGPURenderer from 'three/examples/jsm/renderers/webgpu/WebGPURenderer';
createRenderer() {
// 渲染器 THREE.WebGLRenderer
this.renderer = new WebGPURenderer({
antialias: true,
});
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.setSize(this.width, this.height);
this.renderer.setAnimationLoop(this.render.bind(this));
this.dom.appendChild(this.renderer.domElement);
this.renderCss2D = new CSS2DRenderer({ element: this.css2dDom });
this.renderCss2D.setSize(this.width, this.height);
}
Renderer.js
和Animation.js
提供了setAnimationLoop
方法,在Animation.js
里调用,这样不用手动写requestAnimationFrame
方法。当然了,实现原理是样的node_modules/three/examples/jsm/renderers/common/Animation.js
中提供的Animation.start()
也是通过调用requestAnimationFrame
来实现的
所以我们将自己写的render方法放在setAnimationLoop
的回调,即可this.renderer.setAnimationLoop(this.render.bind(this));
创建元素
渲染器配置好以后,就可以创建object3d
了,材质要选支持node属性的MeshBasicNodeMaterial
,nodes/materials支持, 值得注意的是MeshPhongNodeMaterial
Phong网格材质在使用过程中也有一个问题,
某一个texture的size不符合预期,导致报错,并且页面不渲染,所以这里先规避一下这个问题,选用其他的材质,或许某个机缘巧合会找到合适的解决方案。
创建网格
ts
// 平面几何体
const planeGeometry = new THREE.PlaneGeometry(2, 2);
// 创建一个平面网格
const plane = new THREE.Mesh(planeGeometry, new MeshBasicNodeMaterial({ color: 0x00ff00}));
T.scene.add(plane)
// 创建一个球几何体
const sphereGeometry = new THREE.SphereGeometry(0.5);
// 创建球的网格
const sphere = new THREE.Mesh(sphereGeometry, new MeshBasicNodeMaterial({ color: 0xffff00 }));
sphere.position.z = -2
T.scene.add(sphere)
使用node材质创建出来的网格比普通网格材质多以下几个可控制参数backdropNode
、colorNode
、opacityNode
、positionNode
等,后续会用到
css2dObject
为了方便,直接拿 官网案例创建的元素,并加入球网格内
ts
// 创建一个div元素
const moonMassDiv = document.createElement('div');
moonMassDiv.className = 'label';
moonMassDiv.textContent = '7.342e22 kg';
moonMassDiv.style.color = '#fff'
moonMassDiv.style.backgroundColor = 'transparent';
moonMassLabel = new CSS2DObject(moonMassDiv);
moonMassLabel.position.set(0, 0.8, 0);
moonMassLabel.layers.set(1);
sphere.add(moonMassLabel)
效果
遮挡判断
创建好需要的元素以后,接下来将已有元素加入nodes,并进行遮挡判断
创建遮挡实例
遮挡实例用nodeObject 创建,export function nodeObject<T extends NodeObjectOption>(obj: T): NodeObject<T>;
,接受一个node实例。
NodeObjectOption:
js
/** anything that can be passed to {@link nodeObject} and returns a proxy */
export type NodeRepresentation<T extends Node = Node> = number | boolean | Node | Swizzable<T>;
/** anything that can be passed to {@link nodeObject} */
export type NodeObjectOption = NodeRepresentation | string;
从node_modules/three/examples/jsm/nodes/core/Node.js
这个文件代码可以看到,node的update方法只打印出一个warn,找到它对应的类型标注文件node_modules/@types/three/examples/jsm/nodes/core/Node.d.ts
,Node实例update方法需要重写
Node.js
javascript
update( /*frame*/ ) {
console.warn( 'Abstract function.' );
}
Node.d.ts
php
/** This method must be overriden when {@link updateType} !== 'none' */
update(frame: NodeFrame): void;
这样的话 我们还需要重写一个类,继承Node并传入到nodeObject
中,有了之前对源码的参考,对以下代码的理解可能更深入一些。
js
class OcclusionNode extends Node {
uniformNode:Swizzable;
testObject:THREE.Object3D;
normalLayer:number;
occludedLayer: number
constructor(testObject:THREE.Object3D, normalLayer:number, occludedLayer:number) {
super('vec3');
/** This method must be overriden when {@link updateType} !== 'none' */
// 必要代码
this.updateType = NodeUpdateType.OBJECT;
// uniform 是 GLSL 着色器中的全局变量。
this.uniformNode = uniform(1);
this.testObject = testObject;
this.normalLayer = normalLayer;
this.occludedLayer = occludedLayer;
}
async update(frame:NodeFrame) {
if(frame.renderer) {
// 更新时通过render判断被检测物品是否被遮挡
const isOccluded = (frame.renderer as Renderer).isOccluded(this.testObject);
// 如果被遮挡,取之前存的被遮挡的值,如果没有被遮挡,取为被遮挡的值
const val = isOccluded ? this.occludedLayer : this.normalLayer
// 修改label的层级位置
moonMassLabel && moonMassLabel.layers.set(val)
}
}
setup( /* builder */) {
return this.uniformNode;
}
}
上面的类定义了一个重写了更新方法,在更新方法中,通过isOccluded
方法判断textObject是否被遮挡,通过new OcclusionNode
时传入的第二和第三个函数,决定最后label的layer取哪个值
ini
// 创建一个遮挡实例
const instanceUniform = nodeObject( new OcclusionNode( sphere, 1, 0 ) );
sphere.material.opacityNode = instanceUniform
sphere.occlusionTest = true;
sphere.material.opacityNode
上述代码用到的node属性是opacityNode,因为这个参数可以接受一个数字变量,而其他比如colorNode接受的是一个THREE.Color
实例,然而我们在update中决定是否对元素进行隐藏的时候,用的是label的layer属性,然而layer并没有提供layerNode,所以这可能是一个小瑕疵,layer的原理就是物体在和当前使用的camera同一个层级即可见。
最终效果
源码地址
历史文章
# threejs开发可视化数字城市效果 # threejs渲染高级感可视化涡轮模型 # 写一个高德地图巡航功能的小DEMO
# three.js 打造游戏小场景(拾取武器、领取任务、刷怪)
# threejs 打造 world.ipanda.com 同款3D首页
结语
相对以前的文章,这次的文章也是一个创新性的方向,并没有期待它有多么的受欢迎。内容没有简单粗暴的直接写页面效果,而是通过一个小小的功能,从源码角度深度剖析实现原理;阅读源码的能力是程序员成神的必经之路,以后出门跟其他前端聊天也有谈资,好了,就到这了,我要摸鱼了~~