threejs渲染高级感可视化涡轮模型

好久没写可视化方面的帖子了,最近在网上找灵感,发现一张图个人感觉不错,正好现有技术也可以实现,使用到的技术vite + typescript + threejs

灵感图

准备工作

找模型

扒拉半天的模型网站,找到一个差不多的模型。都是涡轮、部件、机械等关键词模型地址(turbine-01 - Download Free 3D model by Karan.Dhindsa (@Karan.Dhindsa) e03a3c7 (sketchfab.com))

主要工作

加载模型

文中使用到的很多方法是之前文章有提过的,感兴趣的同学可以翻一下,使用之前写的gltfloader方法将下载的模型加载出来,并使用模型中自带场景scene渲染3d世界

typescript 复制代码
async function loadModel() {
    const res = await loadGltf('../src/assets/models/机械零件/1/scene.gltf')
    // 使用模型原有场景
    scene = res.scene

    init()
    animate();
}

加载效果

镜头、灯光等参数

灯光采用的AmbientLight环境光和DirectionalLight平行光,将平行光位置设置在相机所在位置,并且在镜头转动时,实时修改平行光位置,这样可以保证模型始终能够被光线照到

typescript 复制代码
const ambientLight = new AmbientLight(0xffffff, 40);
scene.add(ambientLight)

directionalLight = new THREE.DirectionalLight(0xffffff, 40);
directionalLight.position.copy(camera.position)
scene.add(directionalLight);

镜头采用的是 正交相机(OrthographicCamera,一般独立展示小型模型并且没有空间关系的时候使用,正交相机和透视相机相比,通过不同的矩阵,实现不同的透视关系

typescript 复制代码
const axesHelper = new THREE.AxesHelper(100);
const width = window.innerWidth, height = window.innerHeight
const offset = 500

camera = new THREE.OrthographicCamera(width / - offset, width / offset, height / offset, height / - offset, 0.001, 100000);
camera.position.copy(new THREE.Vector3(0, 0, -3))

控制器使用轨迹球控制器(TrackballControls),可以随意的操控场景,操控场景时监控相机位置,将平行光位置设置为相机位置

typescript 复制代码
controls = new TrackballControls(camera, renderer.domElement);
    // controls.zoom = 100
    controls.rotateSpeed = 4.0;
    controls.zoomSpeed = 1.2;
    controls.panSpeed = 0.8;

    controls.addEventListener('change', () => {
        console.log(camera.position)
        // 灯光位置跟随镜头改变
        directionalLight.position.copy(camera.position)
    })
阶段代码

以上代码地址:整理代码 v1.0.2

创建线条

接下来的工作就是按照灵感图上的效果,做出线条,具体使用的api包含# 边缘几何体(EdgesGeometry),# 线段(LineSegments),# 基础线条材质(LineBasicMaterial),在loadModel方法获取到模型后遍历模型,获取到组成大模型的零部件,

typescript 复制代码
const lineGroup = new THREE.Group()
Sketchfab_model.traverse((mesh: THREE.Object3D) => changeModelMaterial(mesh, lineGroup))
scene.add(lineGroup)
遍历模型

遍历到每一个模型后调用changeModelMaterial方法,这里具体的工作就是,获取模型的世界维度的信息,包含坐标,缩放比例以及四元数,为之后创建出来的线条使用,

typescript 复制代码
export const changeModelMaterial = (object: THREE.Object3D,lineGroup: THREE.Group): any => {
const mesh: THREE.Mesh = object as any
    if (mesh.isMesh) {
        const quaternion = new THREE.Quaternion()
        const worldPos = new THREE.Vector3()
        const worldScale = new THREE.Vector3()
        // 获取四元数
        mesh.getWorldQuaternion(quaternion)
        // 获取位置信息
        mesh.getWorldPosition(worldPos)
        // 获取缩放比例
        mesh.getWorldScale(worldScale)

        mesh.material.transparent = true
        mesh.material.opacity = 0.4
        // 以模型顶点信息创建线条
         const line = getLine(mesh, 30, undefined, 1)
        // 给线段赋予模型相同的坐标信息
        line.quaternion.copy(quaternion)
        line.position.copy(worldPos)
        line.scale.copy(worldScale)

        lineGroup.add(line)

    }
}

由于traverse的api的会将模型本身传到callback中,而咱们的模型是一个组,所以需要过滤一下,只按照Mesh模型创建线条

.traverse ( callback : Function ) : undefined

callback - 以一个object3D对象作为第一个参数的函数。

创建线条
typescript 复制代码
let color = new THREE.Color('#0fb1fb')

const material = new THREE.LineBasicMaterial(
    {
        color: new THREE.Color(color),
        depthTest: true,
        transparent: true
    }
)
export const getLine = (object: THREE.Mesh, thresholdAngle = 1, color = new THREE.Color('#ff0ff0'), opacity = 1): THREE.LineSegments => {
    // 创建线条,参数为 几何体模型,相邻面的法线之间的角度,
    var edges = new THREE.EdgesGeometry(object.geometry, thresholdAngle);
    var line = new THREE.LineSegments(edges);
    material.opacity = opacity
    line.material = material
    return line;
}

将线条材质写在方法的外部,统一调用,这样会保证每一个模型都复用同一个材质,不会创建新的,同时也会节省一点内存,能够优化一下性能,不过也有一个缺点,就是改变其中一个模型的样式,那么其他的都会被改变,下面做个实验

typescript 复制代码
// 创建完线条,两秒后改变其中一个线条的颜色
setTimeout(()=>{
    lineGroup.children[0].material.color = new THREE.Color(0x22ff00)
},2000)

刷新后,模型从蓝色 经过两秒变为绿色,从代码可以看出来,我只改变了lineGroup的第一个元素

阶段代码

以上代码地址:创建线条模型 v1.0.3

模型发光

按照灵感图的样式,有模型本体时,线条不发光或者光比较弱,没有模型本体的时候,线条的光比较强,首先要先把线条搞出发光效果,提取官网提供的案例中的发光部分的代码,接下来开始操作

引用插件
typescript 复制代码
// 渲染器通道
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass';
// 效果器
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer';
// 发光通道
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass';
// 输出通道
import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass';
// 着色器通道
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass';
添加通道和渲染器

这里大部分代码都是整理的官方案例的,具体详情可以查看源码,我其实也没整明白,帮不到大家

typescript 复制代码
const params = {
    threshold: 1,
    strength: 1, // 强度
    radius: 0.84,// 半径
    exposure: 1.55 // 扩散
};
export const unreal = (scene:THREE.Scene,camera:THREE.OrthographicCamera,renderer:THREE.WebGLRenderer,width:number,height: number) => {
    // 渲染器通道,将场景全部加入渲染器
    const renderScene = new RenderPass(scene, camera);
    // 添加虚幻发光通道
    const bloomPass = new UnrealBloomPass(new THREE.Vector2(width, height), 1.5, 0.4, 0.85);
    bloomPass.threshold = params.threshold;
    bloomPass.strength = params.strength;
    bloomPass.radius = params.radius;

    // 创建合成器
   const bloomComposer = new EffectComposer(renderer);
    bloomComposer.renderToScreen = false;
    // 将渲染器和场景结合到合成器中
    bloomComposer.addPass(renderScene);
    bloomComposer.addPass(bloomPass);

    // 着色器通道
    const mixPass = new ShaderPass(
        // 着色器
        new THREE.ShaderMaterial({
            uniforms: {
                baseTexture: { value: null },
                bloomTexture: { value: bloomComposer.renderTarget2.texture }
            },
            vertexShader: `
            
            varying vec2 vUv;
    
            void main() {
    
                vUv = uv;
    
                gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
    
            }
            
            `,
            fragmentShader: `
            
            uniform sampler2D baseTexture;
            uniform sampler2D bloomTexture;
    
            varying vec2 vUv;
    
            void main() {
    
                gl_FragColor = ( texture2D( baseTexture, vUv ) + vec4( 1.0 ) * texture2D( bloomTexture, vUv ) );
    
            }
    
            `,
            defines: {}
        }), 'baseTexture'
    );
    mixPass.needsSwap = true;

    // 合成器输出通道
    const outputPass = new OutputPass();

    const finalComposer = new EffectComposer(renderer);
    finalComposer.addPass(renderScene);
    finalComposer.addPass(mixPass);
    finalComposer.addPass(outputPass);

    return {
        finalComposer,
        bloomComposer,
        renderScene,
        bloomPass
    }
}
通道的使用及渲染

在init初始化方法中调用unrela方法,将不同的通道和渲染器返回,并在render函数中调用

typescript 复制代码
 // 获取发光场景必要元素
const { finalComposer: F,
    bloomComposer: B,
    renderScene: R, bloomPass: BP } = unreal(scene, camera, renderer, width, height)
finalComposer = F
bloomComposer = B
renderScene = R
bloomPass = BP

bloomPass.threshold = 0
    
在render中调用

每一个通道的渲染,都需要在render 中循环调用

typescript 复制代码
// 渲染函数
function render() {
    controls && controls.update()
    controls && controls.zoomCamera()
    renderer.render(scene, camera);
    // 渲染发光通道
    if (bloomComposer) {
        scene.traverse(darkenNonBloomed.bind(this));
        bloomComposer.render();
    }
    // 渲染输出通道
    if (finalComposer) {
        scene.traverse(restoreMaterial.bind(this));
        finalComposer.render();
    }
}
typescript 复制代码
function darkenNonBloomed(obj: THREE.Mesh) {
    if (bloomLayer) {
        if (obj.isMesh && bloomLayer.test(obj.layers) === false) {
            materials[obj.uuid] = obj.material;
            obj.material = darkMaterial;

        }
    }

}

function restoreMaterial(obj: THREE.Mesh) {
    if (materials[obj.uuid]) {
        obj.material = materials[obj.uuid];
        // 用于删除没必要的渲染
        delete materials[obj.uuid];

    }

}
辅助元素gui

添加gui辅助观察具体参数是如何改变发光体的,从而找到合适的参数

typescript 复制代码
function gui() {
    const gui = new GUI();

    const bloomFolder = gui.addFolder('bloom');

    bloomFolder.add(params, 'threshold', 0.0, 1.0).onChange((value: number) => {

        bloomPass.threshold = Number(value);

    });
    // 强度
    bloomFolder.add(params, 'strength', 0.0, 3.0).onChange((value: number) => {

        bloomPass.strength = Number(value);

    });
    // 半径
    gui.add(params, 'radius', 0.0, 1.0).step(0.01).onChange((value: number) => {

        bloomPass.radius = Number(value);

    });

}
发光效果图

通过不同的参数调整,最终效果如下:

在之前的参数和配置中修改了相机,改为透视相机,看起来舒服点,符合人眼观察物体的近大远小关系

阶段代码

以上代码地址:发光效果图 v1.0.3

涡轮动起来

模型本身是带涡轮的动画剪切的,但是好像绑定了模型主体位置,加载自带的动画后,模型不在原本位置,所以舍弃了原有的动画

从模型自身找到涡轮,并在render函数中修改涡轮的rotation.x

typescript 复制代码
// 涡轮
hull_turbine = scene.getObjectByName('hull_turbine')
hull_turbine_line = lineGroup.getObjectByName('hull_turbine_line')

// 涡轮的线
blades_turbine_003 = scene.getObjectByName('blades_turbine_003')
blades_turbine_003_line = scene.getObjectByName('blades_turbine_003_line')
ini 复制代码
let rotationX = 0.03

render函数中

typescript 复制代码
 // 旋转涡轮
if (hull_turbine && hull_turbine_line) {
    hull_turbine.rotation.x += rotationX;
    hull_turbine_line.rotation.x += rotationX;
}
if (blades_turbine_003) {
    blades_turbine_003.rotation.x += rotationX;
}
blades_turbine_003_line && (blades_turbine_003_line.rotation.x += rotationX)

这个不是自己的模型,所以有很多不完美的地方,比如涡轮转的时候,有一根线一直没转,找了半天也没找到具体是那个元素的,所以就放着了,还有模型的中部,有特别密集的顶点,所以创建出来的线条也不是很美观,都变成一坨了,如果是自己的模型,完全可以规避这些问题,并且顶点信息太多,对页面的渲染也是一种压力

阶段代码

以上代码地址:涡轮转动 v1.0.5

进阶功能

比如这真是一个涡轮监控系统,从物联网模型获得数据后,能够第一时间在模型上进行预警,好多大片里面都有这种物联网模型的监控,所以咱们设定一下,某个时间段,或者某个循环点,让模型的某个零件报警,以红色材质表现

纯线稿风格,有点赛博朋克的味道了~

完整代码

历史文章

# 写一个高德地图巡航功能的小DEMO

# Javascript基础之有趣的文字效果

# Javascript基础之写一个好玩的点击效果

# Javascript基础之鼠标拖拉拽

# three.js 打造游戏小场景(拾取武器、领取任务、刷怪)

# threejs 打造 world.ipanda.com 同款3D首页

# three.js------物理引擎

# three.js------镜头跟踪

# threejs 笔记 03 ------ 轨道控制器

相关推荐
imbackneverdie13 小时前
告别 Copilot?Codex 本地化部署指南
人工智能·ai·aigc·数据可视化·本地化·codex
databook17 小时前
Python 常用统计图表实战指南
python·数据分析·数据可视化
躺柒20 小时前
读数据可视化33生命科学(下)
信息可视化·可视化·数据可视化·医学·临床医学·生命科学
Q26433650232 天前
【有源码】基于 Hadoop 的强化学习智能体轨迹数据挖掘与可视化分析系统大数据毕设项目
大数据·hadoop·机器学习·数据挖掘·spark·课程设计·数据可视化
敲敲敲敲暴你脑袋2 天前
地图瓦片批量改色来啦!
node.js·gis·数据可视化
柳杉2 天前
从调研到闭环,我用 GPT-Image-2.5 和 GPT-6 Astra 把智慧厂房 3D 大屏这条链路走完了
前端·ai编程·数据可视化
Patrick在香港3 天前
Python 分析香港 AQHI 归档 1644 天:同一天的空气,早上 8 点报 3,下午 5 点报 10
开发语言·python·数据分析·api·restful·数据可视化·开放数据
2601_962078193 天前
一句话出图:WorkBuddy 如何把数据可视化变成“说话就行”
数据可视化·图表制作·效率革命·workbuddy·指令模板
冰帆<4 天前
DBViewer — 把数据库工作台,安装进浏览器
数据库·数据可视化·数据同步
Highcharts4 天前
常见报错排雷指南3:兼容性问题的官方解法
javascript·数据可视化