- 最终效果图
- 给定html页面,作为tag标签展示
<body style="background-color: black;">
<div id="tag">
<div style="position:relative;width:400px;height:322px;color: #fff;">
<img src="background.png" alt="" style="width:100%;position: absolute;left: 0px;top: 0px;">
<div style="position:absolute;left:48px;top:36px;font-size:16px;">
<div style="font-size:20px;font-weight: 400;">
<span>设备A</span>
</div>
<div style="margin-top: 30px;">
<span style="font-weight: 400;margin-left: 80px;font-size: 40px;color: #00ffff;">test</span>
</div>
<div style="margin-top: 20px;">
<span style="color: #ccc;font-weight: 300;">管理</span><span
style="font-weight: 400;margin-left: 30px;">admin</span>
</div>
<div style="margin-top: 10px;">
<span style="color: #ccc;font-weight: 300;">工号</span><span
style="font-weight: 400;margin-left: 30px;">123456</span>
</div>
</div>
<div style="position:absolute;left:285px;top:35px;">
<span style="color: #ffff00;">异常</span>
</div>
</div>
</div>
<!-- type="importmap"功能:.html文件中也能和nodejs开发环境中一样方式,引入npm安装的js库 -->
<script type="importmap">
{
"imports": {
"three": "../build/three.module.js",
"three/addons/":"../three.js-r167/examples/jsm/"
}
}
</script>
<script src="./index.js" type="module">
</script>
</body>
- mode.js
加载3D模型
// 引入Three.js
import * as THREE from 'three';
// 引入gltf模型加载库GLTFLoader.js
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const loader = new GLTFLoader(); //创建一个GLTF加载器
const model = new THREE.Group(); //声明一个组对象,用来添加加载成功的三维场景
loader.load("test.glb", function (gltf) { //gltf加载成功后返回一个对象
model.add(gltf.scene); //三维场景添加到model组对象中
})
export default model;
// 打印出模型中的所有对象
// gltf.scene.traverse(function (child) {
// if (child.isMesh) {
// // 打印对象名称和类型
// console.log('对象名称:', child.name, ', 对象类型:', child.type);
// }
// });
- tag.js
Three.js 的
WebGLRenderer
只负责渲染 3D 场景,使用 WebGL 在 GPU 上渲染图像。WebGL 渲染和 HTML 的渲染流程是分开的,互相不干扰。HTML 元素是由浏览器的渲染引擎渲染的,和 GPU 渲染的 3D 场景没有直接关联。所以,普通 HTML 标签不会直接参与 Three.js 的场景渲染,也不会随 3D 场景的变化(如相机位置、旋转等)自动更新位置。
因此引入
CSS2DRenderer
专门渲染 HTML/CSS 标签,允许在 WebGL 场景中直接显示和操作 DOM 元素,如显示标注或文本信息。
// 引入CSS2模型对象CSS2DObject
import {
CSS2DObject
} from 'three/addons/renderers/CSS2DRenderer.js';
const div = document.getElementById('tag');
div.style.top = '-161px'; //指示线端点和标注点重合
// HTML元素转化为threejs的CSS2模型对象
const tag = new CSS2DObject(div);
export default tag;
-
index.js
- 将模型渲染到场景中【内容基本不变,一般只改一下光源,相机位置】
const scene = new THREE.Scene();
//模型对象添加到场景中
scene.add(model);
//辅助观察的坐标系
const axesHelper = new THREE.AxesHelper(100);
scene.add(axesHelper);
//光源设置
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5000, 60, 50);
scene.add(directionalLight);
const ambient = new THREE.AmbientLight(0xffffff, 0.9);
scene.add(ambient);
//渲染器和相机
const width = window.innerWidth;
const height = window.innerHeight;
const camera = new THREE.PerspectiveCamera(30, width / height, 1, 3000);
camera.position.set(82, 53, 165);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({
// 抗锯齿优化
antialias:true
});
// 防止输出模糊
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(width, height);
document.body.appendChild(renderer.domElement);
//解决加载gltf格式模型颜色偏差问题
//设置后处理,该方法无效
renderer.outputEncoding = THREE.sRGBEncoding;
// 渲染循环
function render() {
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
// 设置相机控件轨道控制器OrbitControls
const controls = new OrbitControls(camera, renderer.domElement);
// 画布跟随窗口变化
window.onresize = function () {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
};
光源的颜色,强度可以通过GUI来直接控制;
其中DirectionalLight为定向光源,用于模拟太阳光或类似的强烈光源,可以产生阴影,且光线有方向性,适用于需要精确阴影的场景;
AmbientLight环境光用于为场景提供均匀的基础光照,所有物体都被均匀照亮,无阴影效果。
import {GUI} from 'three/addons/libs/lil-gui.module.min.js'
// GUI 设置
const gui = new GUI();
const lightFolder = gui.addFolder('光源设置');
const ambientLightFolder = gui.addFolder('环境光设置');
// 光源控制
const lightParams = {
color: directionalLight.color.getHex(),
intensity: directionalLight.intensity
};
lightFolder.addColor(lightParams, 'color').name('颜色').onChange((value) => {
directionalLight.color.setHex(value);
});
lightFolder.add(lightParams, 'intensity', 0, 2).name('光强').onChange((value) => {
directionalLight.intensity = value;
});
// 环境光控制
const ambientParams = {
color: ambient.color.getHex(),
intensity: ambient.intensity
};
ambientLightFolder.addColor(ambientParams, 'color').name('颜色').onChange((value) => {
ambient.color.setHex(value);
});
ambientLightFolder.add(ambientParams, 'intensity', 0, 2).name('光强').onChange((value) => {
ambient.intensity = value;
});
对应的也可以控制相机的位置(x,y,z轴),视角等参数,方便在camera.position.set(94, 58, 107);设置相机位置的时候给出合适的值
// GUI 设置相机控制
const cameraFolder = gui.addFolder('相机设置');
// 控制相机位置
cameraFolder.add(camera.position, 'x', -500, 500).name('相机 X 轴');
cameraFolder.add(camera.position, 'y', -500, 500).name('相机 Y 轴');
cameraFolder.add(camera.position, 'z', -500, 500).name('相机 Z 轴');
// 控制相机视角 (fov)
cameraFolder.add(camera, 'fov', 1, 100).name('视角').onChange(() => {
camera.updateProjectionMatrix(); // 更新相机投影矩阵
});
// 控制相机的近/远剪裁面
cameraFolder.add(camera, 'near', 0.1, 1000).name('近剪裁面').onChange(() => {
camera.updateProjectionMatrix(); // 更新相机投影矩阵
});
cameraFolder.add(camera, 'far', 1, 5000).name('远剪裁面').onChange(() => {
camera.updateProjectionMatrix(); // 更新相机投影矩阵
});
2. 添加`EffectComposer`后处理
基本渲染:
WebGLRenderer
的render
方法用于将three.js
场景渲染到屏幕上,直接得到渲染结果。renderer.render(scene, camera)
;后期处理的需求: 如果需要对渲染结果应用一些后期处理效果(如模糊、颜色校正、景深等),
EffectComposer
能够在渲染之后对图像进行一系列处理效果。
// 创建后处理对象EffectComposer,WebGL渲染器作为参数
const composer = new EffectComposer(renderer);
// 创建一个渲染器通道,场景和相机作为参数
const renderPass = new RenderPass(scene, camera);
// 设置renderPass通道
composer.addPass(renderPass);
// 渲染循环
function render() {
// 后处理渲染
composer.render();
requestAnimationFrame(render);
}
render();
3. 添加描边光效后处理`OutlinePass`
// 创建outlinePass通道
// OutlinePass第一个参数v2的尺寸和canvas画布保持一致
const v2 = new THREE.Vector2(window.innerWidth, window.innerHeight);
const outlinePass = new OutlinePass(v2, scene, camera);
outlinePass.visibleEdgeColor.set(0x00ffff); //模型描边颜色,默认白色
outlinePass.edgeThickness = 4; //高亮发光描边厚度
outlinePass.edgeStrength = 6; //高亮描边发光强度
outlinePass.pulsePeriod = 2;//模型闪烁频率控制,默认0不闪烁
outlinePass.usePatternTexture = false; // 禁用纹理以获得纯线的效果
composer.addPass(outlinePass);
场景中添加outlinePass之后变得很暗,这是因为当使用threejs后处理功能后,
renderer.outputEncoding = THREE.sRGBEncoding
会失效,出现颜色偏差。 本质上涉及到线性空间
和sRGB 空间
之间的转换问题;gltf模型中包含的颜色被导出一般为sRGB颜色空间,而three.js是在线性空间中渲染模型材质,因为three.js会将sRGB转化为Linear;而为了在显示器上正确显示颜色,不出现偏差,又需要将线性空间转化会sRGB空间;
three.js中可以通过伽马矫正来解决gltf模型后处理时候,颜色偏差的问题
// 伽马校正后处理Shader
import {GammaCorrectionShader} from 'three/addons/shaders/GammaCorrectionShader.js';
// ShaderPass功能:使用后处理Shader创建后处理通道
import {ShaderPass} from 'three/addons/postprocessing/ShaderPass.js';
const gammaPass= new ShaderPass(GammaCorrectionShader);
composer.addPass(gammaPass);
报错:着色器环境中未找到
sRGBTransferOETF
函数,当前版本148,没有去看其余版本是否包含此函数,选择自定义伽马矫正
// 自定义伽马矫正着色器
const gammaCorrectionShader = {
uniforms: {
"tDiffuse": { value: null },
"gammaFactor": { value: 2.2 }
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
varying vec2 vUv;
uniform sampler2D tDiffuse;
uniform float gammaFactor;
void main() {
vec4 tex = texture2D(tDiffuse, vUv);
gl_FragColor = vec4(pow(tex.rgb, vec3(1.0 / gammaFactor)), tex.a);
}
`
};
4. 添加抗锯齿效果(左抗锯齿前,右后)
// SMAA抗锯齿通道
import {SMAAPass} from 'three/addons/postprocessing/SMAAPass.js';
//获取.setPixelRatio()设置的设备像素比
const pixelRatio = renderer.getPixelRatio();
// width、height是canva画布的宽高度
const smaaPass = new SMAAPass(width * pixelRatio, height * pixelRatio);
composer.addPass(smaaPass);//抗锯齿效果
5. 创建CSS2D渲染器,将标签渲染到webgl页面上
// 创建一个CSS2渲染器CSS2DRenderer
const css2Renderer = new CSS2DRenderer();
css2Renderer.setSize(width, height);
// HTML标签<div id="tag"></div>外面父元素叠加到canvas画布上且重合
css2Renderer.domElement.style.position = 'absolute';
css2Renderer.domElement.style.top = '0px';
//设置.pointerEvents=none,解决HTML元素标签对threejs canvas画布鼠标事件的遮挡
css2Renderer.domElement.style.pointerEvents = 'none';
// 将CSS2DRenderer的DOM元素添加到html的body中,显示在3D画布的上方。
document.body.appendChild(css2Renderer.domElement);
// 渲染循环
function render() {
css2Renderer.render(scene,camera);
// renderer.render(scene, camera);
composer.render();
requestAnimationFrame(render);
}
render();
6. 鼠标点击事件
1. `THREE.Raycaster` 是 Three.js 提供的一个用于进行射线投射的工具。用于 3D 空间中检测物体交互的方法,通常用于实现点击、碰撞检测、选择对象等功能。简单来说就是从一个点(发射点)沿某个方向发射射线,并检测射线与场景中物体的交点。
2. 获取罐的模型对象,可以看到下面有两个子节点设备A和设备B
3. 设置mesh对象的ancestors属性为它所在的 Group 对象
4. 创建射线,并且把鼠标点击获取的坐标和相机传入参数,可视化射线,查看射线击中了哪个物体以及交点的位置
5. 后续的就是js的逻辑,判断点击获取到的对象,根据对应名字,找到标注A,标注B,将tag标签添加到模型的相应位置,并添加发光描边效果
// 点击事件
let chooseObj = null;
addEventListener('click', function (event) {
const px = event.offsetX;
const py = event.offsetY;
// 屏幕坐标转标准设备坐标
const x = (px / window.innerWidth) * 2 - 1;
const y = -(py / window.innerHeight) * 2 + 1;
// 创建射线
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(new THREE.Vector2(x, y), camera);
// 获取存储罐模型对象
const tank = model.getObjectByName('罐');
// 遍历并设置所有子对象的祖先属性
for (let i = 0; i < tank.children.length; i++) {
const group = tank.children[i];
group.traverse(function (obj) {
if (obj.isMesh) {
obj.ancestors = group;
}
});
}
// 射线交叉检测
const intersects = raycaster.intersectObjects(tank.children);
if (intersects.length > 0) {
const intersectedObject = intersects[0].object.ancestors;
// 设置发光描边
outlinePass.selectedObjects = [intersectedObject];
// 获取模型对应的标注对象
const labelObject = model.getObjectByName(intersectedObject.name + '标注');
if (labelObject) {
// 将标签添加到模型标注对象上
labelObject.add(tag);
}
// 更新当前选中的对象
chooseObj = intersectedObject;
} else {
// 如果有已经选中的对象,移除发光描边和标签
if (chooseObj) {
outlinePass.selectedObjects = []; // 清除发光描边
const labelObject = model.getObjectByName(chooseObj.name + '标注');
if (labelObject) {
// 从模型的标注对象中移除标签
labelObject.remove(tag);
}
// 重置选中的对象
chooseObj = null;
}
}
});
- CSS3DRenderer
目前的CSS2DRenderer渲染出来的tag标签,无法随着场景相机同步缩放,因此在此基础上,进行3D渲染,基本上把CSS2DRenderer替换成CSS3DRenderer,再设置tag缩放比例,调整位置就行;
tag.scale.set(0.1,0.1,0.1)
给tag加上style:
backface-visibility: hidden;
即可在模型背面不显示该tag
- CSS3DSprite
CSS3DRenderer
是一个普通的 3D HTML 对象,跟随 3D 场景中的旋转和缩放,但不会自动面向相机。CSS3DSprite
会始终面向相机,因此更适合用作 3D 场景中的标签或注释。只需要
const tag = new CSS3DSprite(div);
将模型转化成精灵对象即可