阅读视图

发现新文章,点击刷新页面。

Three.js 工业 3D 可视化:生产线状态监控系统实现方案

在工业数字化转型过程中,3D 可视化监控系统凭借直观、沉浸式的优势,成为车间管理的重要工具。本文将详细介绍如何使用 Three.js 构建一套生产线 3D 状态监控系统,实现设备状态展示、产能数据可视化、交互式操作等核心功能

联想截图_20251106173926.jpg

一、项目背景与技术选型

1. 项目需求

  • 3D 可视化展示生产线布局及设备状态
  • 实时显示生产线运行参数(产能、产量、状态等)
  • 支持多生产线切换查看
  • 设备状态可视化(运行 / 维护 / 停机)
  • 交互式操作(视角旋转)

2. 技术栈选型

  • 3D 核心库:Three.js(Web 端 3D 图形渲染引擎)

  • 辅助库

    • GLTFLoader(3D 模型加载)
    • OrbitControls(相机控制)
    • CSS3DRenderer/CSS2DRenderer(3D/2D 标签渲染)
  • UI 框架:Element UI(进度条、样式组件)

  • 动画库:animate-number(数值动画)

  • 样式预处理:SCSS(样式模块化管理)

二、核心功能实现

1. 3D 场景基础搭建

场景初始化是 Three.js 项目的基础,需要完成场景、相机、渲染器三大核心对象的创建。

init() {
  // 1. 创建场景
  this.scene = new THREE.Scene();

  // 2. 创建网格模型(生产线底座)
  const geometry = new THREE.BoxGeometry(640, 1, 70);
  const material = new THREE.MeshLambertMaterial({
    color: 0xffffff,
    transparent: true,
    opacity: 1
  });
  this.mesh = new THREE.Mesh(geometry, material);
  this.mesh.position.set(0, -140, 0);
  this.scene.add(this.mesh);

  // 3. 光源设置(点光源+环境光)
  const pointLight = new THREE.PointLight(0xffffff, 0.5);
  pointLight.position.set(0, 200, 300);
  this.scene.add(pointLight);
  
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.3);
  this.scene.add(ambientLight);

  // 4. 相机设置(正交相机,适合工业场景展示)
  const container = document.getElementById("container");
  const width = container.clientWidth;
  const height = container.clientHeight;
  const aspectRatio = width / height;
  const scale = 230; // 场景显示范围系数

  this.camera = new THREE.OrthographicCamera(
    -scale * aspectRatio,
    scale * aspectRatio,
    scale,
    -scale,
    1,
    1000
  );
  this.camera.position.set(-100, 100, 500);
  this.camera.lookAt(this.scene.position);

  // 5. 渲染器设置
  this.renderer = new THREE.WebGLRenderer({
    antialias: true, // 抗锯齿
    preserveDrawingBuffer: true // 保留绘制缓存
  });
  this.renderer.setSize(width, height);
  this.renderer.setClearColor(0xffffff, 0); // 透明背景
  container.appendChild(this.renderer.domElement);

  // 6. 控制器设置(支持鼠标交互)
  this.controls = new OrbitControls(this.camera, this.renderer.domElement);
  this.controls.addEventListener("change", () => {
    this.renderer.render(this.scene, this.camera);
  });

  // 初始渲染
  this.renderer.render(this.scene, this.camera);
}

2. 3D 模型加载与生产线构建

(1)外部模型加载

使用 GLTFLoader 加载生产线设备 3D 模型(glb 格式),并设置模型位置:

loadGltf() {
  const loader = new GLTFLoader();
  loader.load("../model/cj.glb", (gltf) => {
    gltf.scene.position.set(16, -139, 140); // 调整模型位置适配场景
    this.scene.add(gltf.scene);
    this.renderer.render(this.scene, this.camera);
  });
}

(2)生产线围墙构建

通过 BufferGeometry 自定义几何体,创建生产线边界围墙,并用纹理贴图美化:

addWall() {
  // 围墙顶点坐标
  const vertices = [-320, 35, 320, 35, 320, -35, -320, -35, -320, 35];
  const geometry = new THREE.BufferGeometry();
  const posArr = [];
  const uvArr = [];
  const height = -40; // 围墙高度

  // 构建围墙三角面
  for (let i = 0; i < vertices.length - 2; i += 2) {
    // 两个三角形组成一个矩形面
    posArr.push(
      vertices[i], vertices[i+1], -140,
      vertices[i+2], vertices[i+3], -140,
      vertices[i+2], vertices[i+3], height,
      vertices[i], vertices[i+1], -140,
      vertices[i+2], vertices[i+3], height,
      vertices[i], vertices[i+1], height
    );
    // UV贴图坐标
    uvArr.push(0,0, 1,0, 1,1, 0,0, 1,1, 0,1);
  }

  // 设置几何体属性
  geometry.attributes.position = new THREE.BufferAttribute(new Float32Array(posArr), 3);
  geometry.attributes.uv = new THREE.BufferAttribute(new Float32Array(uvArr), 2);
  geometry.computeVertexNormals(); // 计算法线

  // 加载纹理并创建材质
  this.texture = new THREE.TextureLoader().load("../images/linearGradient.png");
  this.mesh = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({
    color: this.dict_color[this.progress.state],
    map: this.texture,
    transparent: true,
    side: THREE.DoubleSide, // 双面渲染
    depthTest: false
  }));
  this.mesh.rotation.x = -Math.PI * 0.5; // 旋转适配场景
  this.scene.add(this.mesh);
}

4. 状态可视化与数据面板

(1)多状态颜色映射

定义生产线三种状态(运行中 / 维护中 / 停机中)的颜色映射,实现状态可视化:

dict_color: {
  运行中: "#32e5ad", // 绿色
  维护中: "#fb8d1c", // 橙色
  停机中: "#e9473a"  // 红色
}

(2)数据面板设计

通过 CSS2DRenderer 将数据面板作为 2D 标签添加到 3D 场景中,实时展示生产线参数:

<div id="tooltip">
  <div class="title">DIP 2-1涂覆线</div>
  <div class="progress">
    <p class="state">
      <span class="icon" :style="{ backgroundColor: dict_color[progress.state] }"></span>
      {{ progress.state }}
    </p>
    <p class="value">
      <animate-number
        from="0"
        :key="progress.value"
        :to="progress.value"
        duration="2000"
        easing="easeOutQuad"
        :formatter="formatter"
      ></animate-number>
      %
    </p>
    <el-progress :percentage="progress.value" :show-text="false" :color="dict_color[progress.state]"></el-progress>
  </div>
  <ul class="infoList">
    <li v-for="(item, index) in infoList" :key="index">
      <label>{{ item.label }}:</label>
      <span>{{ item.value }}</span>
    </li>
  </ul>
</div>
addTooltip() {
  const tooltipDom = document.getElementById("tooltip");
  const tooltipObject = new CSS2DObject(tooltipDom);
  tooltipObject.position.set(0, 120, 0); // 面板在3D场景中的位置
  this.scene.add(tooltipObject);
  this.labelRenderer2D.render(this.scene, this.camera);
}

5. 多生产线切换功能

支持切换查看多条生产线状态,通过点击标签切换数据和状态颜色:

changeType(index) {
  this.typeIndex = index;
  // 根据索引切换不同生产线的状态数据
  if (index % 3 === 0) {
    this.progress = this.progress1; // 运行中
  } else if (index % 3 === 1) {
    this.progress = this.progress2; // 维护中
  } else {
    this.progress = this.progress3; // 停机中
  }
}

// 监听progress变化,更新3D模型颜色
watch: {
  progress: {
    handler() {
      this.mesh.material.color.set(this.dict_color[this.progress.state]);
      this.renderer.render(this.scene, this.camera);
    },
    deep: true
  }
}

6. 响应式适配

处理窗口大小变化,确保 3D 场景自适应调整:

onWindowResize() {
  const container = document.getElementById("container");
  const width = container.clientWidth;
  const height = container.clientHeight;

  // 更新渲染器尺寸
  this.renderer.setSize(width, height);
  this.labelRenderer.setSize(width, height);
  this.labelRenderer2D.setSize(width, height);

  // 更新相机参数
  const aspectRatio = width / height;
  const scale = 230;
  this.camera.left = -scale * aspectRatio;
  this.camera.right = scale * aspectRatio;
  this.camera.top = scale;
  this.camera.bottom = -scale;
  this.camera.updateProjectionMatrix();

  // 重新渲染
  this.renderer.render(this.scene, this.camera);
}

三、关键技术

1. 3D 与 2D 融合渲染

通过 CSS3DRenderer 和 CSS2DRenderer 实现 3D 场景与 2DUI 的无缝融合:

  • CSS2DRenderer:用于数据面板等需要始终面向相机的 2D 元素
  • CSS3DRenderer:用于生产线节点标签等需要 3D 空间定位的元素

2. 状态可视化设计

  • 颜色编码:用不同颜色区分设备状态,符合工业监控的视觉习惯
  • 动态更新:状态变化时实时更新 3D 模型颜色和数据面板
  • 图标标识:通过图标和文字结合,增强状态辨识度

3. 性能优化

  • 抗锯齿设置:提升 3D 模型显示清晰度
  • 双面渲染:确保围墙等几何体正反面都能正常显示
  • 纹理复用:减少重复纹理加载,提升性能
  • 事件监听优化:仅在必要时重新渲染场景

🪐 行星科技概念官网!Hero Section 回归!(Three.js ✨)

🪐 行星科技概念官网!Hero Section 回归!(Three.js ✨)

0.好久不见 👋

新老观众老爷们!我鸽子王何贤又双叒叕回归啦!😆

真的是好久不见!记得上一次发文章还是在上一次。一转眼就两个月没更新了。

这期间,我凭借之前的 9 篇文章成功晋级到创作等级 LV.5 🎉(会不会是掘金史上最快传说?)获得了1K+的粉丝。

衷心感谢大家一直以来的支持 ❤️。

不知不觉,今年都快结束了。从最初的「周更博主」到「月更博主」,再到如今的「半年更博主」😂,每次被催更都觉得有点心虚。

不行!这次绝不能再鸽了!🕊️

重新自我介绍一下——我是一名在 Three.js 领域里摸爬滚打的初级玩家。在这里,我会分享自己的所见、所闻与所想。


1.前置条件 ⚙️

欢迎阅读本篇文章!在深入探讨 Three.jsShader (GLSL) 的进阶内容之前,请确保您已经具备以下基础知识:

  1. Three.js 基础 您需要熟悉 Three.js 的基本概念与使用方法,包括场景(Scene)、相机(Camera)、渲染器(Renderer)、几何体(Geometry)、材质(Material)和网格(Mesh)等核心组件。 如果您对这些内容还不熟悉,建议先学习 Three.js 的入门教程。我比较推荐外网知名博主 Bruno Simon 的课程 threejs-journey(B 站上有免费版,但如果条件允许,建议支持正版课程)。 当然,如果您希望我分享自己的学习路径,可以在评论区留言。人数够多的话,我会着手撰写一篇系统的学习路线文章。

  2. Shader 语法 本文将涉及 GLSL(OpenGL Shading Language)的编写,因此您需要了解 GLSL 的基本语法,包括顶点着色器(Vertex Shader)与片元着色器(Fragment Shader)的结构,以及如何在 Three.js 中使用自定义着色器。


2.Hero Section 概览 🌌

“Hero Section” 是网页设计中的一个术语,通常指页面顶部的大型横幅区域。 对于开发者而言,它可以更直观地理解为:用户在访问网站瞬间所感受到的视觉冲击,或促使他们停留在网站的关键视觉因素。

相信大家偶尔也会刷到一些以星球为主题的官网,看起来既梦幻又酷炫。

这些网站往往拥有天马行空的页面布局,美丽的星球在画面中静静流动,营造出一种未来科技与宇宙幻想交织的氛围。

于是,我让 GPT 🧠 通过文生图生成了一张原型设计稿:

Page 原型图

hero.png

并尝试将其复原,于是得到了以下结果

Page 静态预览

page.png

Page 动图

由于平台图片体积限制, 动画画质存在大幅抽帧和压缩,还请各位可以在 PC 端自行体验

09.gif

PC端在线预览地址(需要魔法): isgalaxias.vercel.app/

DeBug 在线调试界面: isgalaxias.vercel.app/#debug

Github 仓库地址: github.com/hexianWeb/i…

转发贴环节

关注我的掘友最熟悉的环节了,那么让我来介绍本次项目,它被 Threejs Journey 课程作者 Bruno Simon 转发

x.png

(其实项目 8 月底就写完了,但是我疯狂鸽子,非常抱歉!)

3.场景搭建 🧱

由于本专栏主要聚焦于 Three.js,因此本文不会详细讲解从 0 到 1 的完整页面实现过程,而是重点介绍与 Three.js 相关的实现部分。

首先,让我们分析一下当前场景(Scene)中的主要元素:

  • 最外层的 星云图背景
  • 散布在空间中的 星点中心星环
  • 漂浮在太空中的 三颗行星

3.1 星云图背景 🌌

这一部分的实现非常简单:只需将加载好的星云贴图设置为场景的 background 即可。

在此也顺便推荐一个优秀的 3D 贴图资源站 —— Solar System Scope。 本项目中使用的所有行星与星云贴图,均来自该网站。 这里找到星云图

03.png

但注意这是一个根据 CC Attribution 4.0 许可证提供各种行星纹理的网站,因此需要提供相应的版权信息。如果您将网站上线,请务必在页面上的某个位置显示这些版权信息。

随后,在 Three.js 中加载贴图,并将其赋值为场景背景。 同时可以通过 backgroundIntensity 调整背景亮度,使整体层次更柔和。

this.scene.background = this.resources.items.spaceTexture
this.scene.backgroundIntensity = 0.25

04.png

3.2 星星 与 星环✨

这一部分构成了整个 Hero Section 的核心视觉焦点。我们通过 Points 系统创建了一个由大量粒子组成的螺旋星系,其中包含星星和中心星环。

01.gif

乍一看,当前的实现方式似乎可以分为两个部分,“中心的粒子圆环和外层的粒子群”,以及周围缓慢旋转的粒子群对吗?但是仔细看“中心的粒子圆环和外层的粒子群”,似乎并没有那么强的割裂感,反而更像是粒子圆环带动了整个粒子群一同旋转,对吧?这是怎么做到的?

其实这只是一个非常巧妙的视觉错觉,有点像小时候玩的万花筒。如下图

02.gif

当我们放开控制器、稍微调整视角时,就会发现这个结构实际上只是一个简单的圆柱体。 通过透视营造出“浩瀚星海”的假象。

于是,现在你大概已经能想到几种实现方式了:

  • 利用MeshSurfaceSampler在多个圆柱体表面使用对几何体平面进行采样,以构建粒子位置;
  • 或者在构建 BufferGeometry 时,利用三角函数约束顶点分布,使所有粒子落在圆环轨迹上。
  • 下面我来分享我在项目中采用的具体思路。
3.2.1 实现思路 🌠

可以概括为以下几个步骤:

  1. 生成螺旋分布的粒子位置:基于极坐标系统,沿着多个分支(branches)生成螺旋分布的粒子
  2. 应用圆环约束:使用数学函数将粒子约束在环形区域内,形成星环效果
  3. 添加随机扰动:为每个粒子添加随机偏移,营造自然的星系形态
  4. 着色器动画:在顶点着色器中实现旋转动画,让星系"流动"起来
3.2.2粒子位置生成 💫

setGalaxy() 方法中,我们为每个粒子生成位置数据:

// 生成在环形区域内的半径
const minRadius = this.parameters.innerRadius * this.parameters.radius
const maxRadius = this.parameters.radius
const radius = minRadius + Math.random() * (maxRadius - minRadius)

// 计算螺旋角度和分支角度
const spinAngle = radius * this.parameters.spin
const branchAngle = (i % this.parameters.branches) / this.parameters.branches * Math.PI * 2

// 生成基础位置
const x = Math.cos(branchAngle + spinAngle) * radius
const z = Math.sin(branchAngle + spinAngle) * radius

这里的关键是:

  • spinAngle:根据半径和旋转参数 spin 计算螺旋角度,离中心越远,旋转角度越大
  • branchAngle:将粒子均匀分配到多个分支上,形成星系的旋臂结构
3.2.3 圆环约束算法 🌌

为了让粒子呈现出星环的效果,我们使用了圆环约束算法

// 计算距离圆环中心的归一化距离(0-1)
const ringCenter = (minRadius + maxRadius) * 0.5
const ringWidth = maxRadius - minRadius
const distanceToRingCenter = Math.abs(radius - ringCenter) / (ringWidth * 0.5)

// 使用帽形函数(反向抛物线)来约束随机扰动
const ringConstraint = (1.0 - distanceToRingCenter ** this.parameters.ringFalloff) * this.parameters.constraintStrength
const effectiveRandomness = this.parameters.randomness * ringConstraint

这个算法的核心思想是:

  • 距离圆环中心越近的粒子,随机扰动越大(约束强度高)
  • 距离边缘越近的粒子,随机扰动越小(约束强度低)
  • 通过 ringFalloff 参数控制衰减曲线,constraintStrength 控制整体约束强度

这样就能形成一条清晰的星环带,而不是均匀分布的粒子。

3.2.4随机扰动 🌀

为了增加星系的自然感,我们为每个粒子的Y轴添加了随机偏移:

const randomY = effectiveRandomness * radius * (Math.random() < 0.5 ? 1 : -0.4) * Math.random() ** this.parameters.randomnessPower * 20

这里可以看到我对于扰动约束存在两种细分情况 Math.random() < 0.5 ? 1 : -0.4

可以理解为粒子 Y轴正向偏移时随机偏移效果为 100%,负向偏移时随机效果只有 40%

当然也可以把这个效果去掉,当前效果会变为下图:

03.gif

或许你有更大胆的创意?在中心部分放上一些特殊的星空元素?比如黑洞,或者一只巨大的“外星之眼” 👁️让整个场景更具神秘感。

3.2.5 粒子圆环带动粒子群 🌀

到目前为止,我们已经定义了粒子在空间中的分布关系,也确定了它们在星环结构中的约束逻辑。接下来要做的,就是让这些粒子 动起来 —— 让星环旋转,从而带动整个星系流动。

动画实现思路

在这里我们通过 顶点着色器(vertex shader) 来实现粒子的动态旋转。相比在 JavaScript 层更新所有粒子坐标,使用 GPU 端的位移计算可以大幅降低性能消耗,并让动画更流畅。

核心逻辑是基于 粒子到中心的距离衰减旋转速度

  • 离中心越近 → 旋转越快
  • 离中心越远 → 旋转越慢

这样能让整个星环在旋转时呈现一种“内圈快、外圈慢”的自然涡旋感。

顶点着色器动画逻辑
float angle = atan(modelPosition.x, modelPosition.z);
float distanceToCenter = length(modelPosition.y);
float offset = (1.0 / distanceToCenter) * uTime * 0.2;
angle += offset;

// 更新位置
modelPosition.x = cos(angle);
modelPosition.z = sin(angle);

计算到中心的距离distanceToCenter = length(modelPosition.y); 用于控制旋转的衰减速率。

如果旋转速度变化过大,可以通过以下方式做平滑限制:

distanceToCenter = clamp(distanceToCenter, 0.0, 10.0);

让距离过近或过远的粒子保持在合理的旋转速率范围内。

最终呈现出的效果,就是一个不断旋转、充满空间层次感的 银河涡旋

04.gif

3.3 星球 🌍

这一部分是整个页面中最具表现力的视觉元素 —— 三颗漂浮在太空中的星球。

06.png

而对于如何在 Threejs里面显示一颗星球,我想已经有太多技术文章来描述这一过程了。这里我也推荐你去看 Threejs jouneryEarth Shader 章节。(这里 B 站链接,但是有条件还是建议补补票,作者会在每个圣诞节给 discount )

这里我主要描述当前页面的星球实现与其余教程的不同点。

星球渲染包含几个关键部分:

  1. 星球主体:使用各向异性过滤提升纹理质量。使用置换贴图增加地形细节,法线贴图增强表面凹凸感
  2. 光照系统:自定义环境光 + 点光源,支持衰减、漫反射和镜面反射
3.3.1 星球主体 🪐

放大星球可以看到表面存在明显的地形起伏,这通常会让人想到 MeshStandardMaterialbumpMapdisplacementMap。 但在本项目中,我并没有直接使用标准物理材质,而是采用了 ShaderMaterial,并在顶点着色器中根据 法线贴图 自行实现位移和光照控制。 这么做的原因是为了后续自定义光照系统做铺垫。

当然条条大路通罗马,你也可以通过

方案一MeshStandardMaterial+ 置换/法线贴图 + 点光源(快速简洁)

方案二:使用 THREE-CustomShaderMaterial 在标准光照的基础上扩展自定义 Shader

这里仅提供我的实现方案

07.png

首先我们先创建一个顶点足够多的球体,再简单的创建ShaderMaterial以及应用baseColor到球体上后有以下几点需要注意

  1. 因为后面置换贴图在顶点着色器中改变顶点位置,形成地形起伏,需要通过操控顶点位置来实现这种凹凸地形,所以我们需要分配足够多的顶点给后续vertexShader假如没有分配足够多的顶点则会导致地形呈现“方块感”:
// 创建星球几何体(增加细分以支持置换)
this.geometry = new THREE.IcosahedronGeometry(this.radius, 64, 64)

较少顶点情况:

08.png

在顶点着色器中,我们使用置换贴图来改变顶点的位置,形成地形起伏:

// 从置换贴图的红色通道读取高度值
float displacement = texture2D(uDisplacementMap, uv).r;// uniform 传入 网站上下载好的贴图即可

// 沿法线方向偏移顶点位置
vec3 displacedPosition = position + normal * displacement * uDisplacementScale;

uDisplacementScale 控制置换强度,数值越大,地形起伏越明显。

现在你应该拥有了一个凹凸不平的星球

05.gif

3.3.2 光照系统

片元着色器负责计算最终的颜色,包含以下几个关键步骤:

1. 法线贴图处理

我们实现了完整的 TBN(切线-副切线-法线)矩阵计算,将法线贴图从切线空间转换到世界空间:

vec3 perturbNormal(vec3 normal, vec3 position, vec2 uv, sampler2D normalMap, float normalScale) {
  // 获取法线贴图值并转换到 -1 到 1 范围
  vec3 normalMapColor = texture2D(normalMap, uv).rgb;
  vec3 normalMapNormal = normalize(normalMapColor * 2.0 - 1.0);

  // 通过屏幕空间导数计算切线和副切线
  vec3 q1 = dFdx(position);
  vec3 q2 = dFdy(position);
  vec2 st1 = dFdx(uv);
  vec2 st2 = dFdy(uv);

  vec3 tangent = normalize(q1 * st2.t - q2 * st1.t);
  vec3 bitangent = normalize(-q1 * st2.s + q2 * st1.s);

  // 构建 TBN 矩阵
  mat3 tbn = mat3(tangent, bitangent, normal);

  // 混合原始法线和扰动后的法线
  vec3 perturbedNormal = normalize(mix(normal, tbn * normalMapNormal, normalScale));

  return perturbedNormal;
}

2. 环境光计算

环境光提供基础照明,让阴影区域也有可见度:

vec3 ambient = uAmbientLight * textureColor * uAmbientLightIntensity;

3. 点光源计算

点光源使用兰伯特漫反射模型,并包含距离衰减:

// 计算光源方向
vec3 lightDirection = uPointLightPosition - vPosition;
float distance = length(lightDirection);
lightDirection = normalize(lightDirection);

// 距离平方衰减(避免近距离过度曝光)
float attenuation = 1.0 / (1.0 + 0.09 * distance + 0.032 * distance * distance);

// 兰伯特漫反射
float lightIntensity = max(dot(normal, lightDirection), 0.0);
vec3 diffuse = uPointLightColor * textureColor * lightIntensity * uPointLightIntensity * attenuation;

4. 镜面反射

我们还实现了简单的镜面高光,模拟金属表面的反射:

// 菲涅尔效应
vec3 viewDirection = normalize(cameraPosition - vPosition);
float fresnel = pow(1.0 - max(dot(normal, viewDirection), 0.0), 2.0);

// 金属度和粗糙度影响
vec3 metallic = mix(textureColor, vec3(1.0), uMetalness);
float roughnessFactor = 1.0 - uRoughness;

// 镜面反射
vec3 reflectDirection = reflect(-lightDirection, normal);
float specular = pow(max(dot(viewDirection, reflectDirection), 0.0), 4.0 * roughnessFactor);
vec3 specularColor = uPointLightColor * specular * metallic * fresnel * attenuation;

最终颜色由环境光、漫反射和镜面反射组合而成:

vec3 finalColor = ambient + diffuse + specularColor * 0.3;

效果如下:

06.gif

这是一套很经典的着色模型(Blinn-Phong Reflectance Model),当然你也可以从我之前推荐的 games101 的第 7 节 第39 分开始看到相关内容。

3.3.3 各向异性过滤

放大后中央模糊?别急!这是采样角度导致的走样问题 📉

12.png

这看齐来是真的挺丑的,接下来我们需要设定一个特殊的texture属性值来解决它。——Anisotropic Filtering 各向异性过滤

听起来有点熟悉对不对?

很多人在做比较大的场景时,会将地板贴图的各向异性过滤值调高,因为这样可以让"让贴图在远处或倾斜的角度下依然保持清晰,不会变糊"。

09.png 但为什么会出现这种走样的情况呢?

原因在于当三维表面与摄像机夹角较大时,屏幕上一个像素在纹理空间中的采样区域会被透视投影拉伸,形成一个长条或椭圆形的采样足迹。传统过滤算法假设采样区域为正方形,因此在这种情况下容易出现模糊或走样。(这里同样也推荐您去看 games101 图形学入门课程中对于走样和采样相关章节)

这里我做简单解释,可能不太准确,最好还是自己系统学习:

首先我们要明确一点,当 GPU 渲染模型时,它会计算屏幕上每个像素的颜色。 要得到颜色,就需要“采样”纹理(texture):

屏幕像素 → 给定一个像素在三维表面上的位置 → 通过 UV 映射 → 找到对应贴图坐标 → 从贴图图像中取出颜色值。

当表面与摄像机有角度、或者表面离得太远时会出现一个屏幕像素对应到贴图上多个像素点(过采样)。

若是最理想的情况下,就是贴图当前与摄像机为正交视图(假设现在一个格子就是一个实际像素点)

10.png

此时每个屏幕像素都对应贴图中 等面积的区域(正交视角,且物体远)此时无论是点采样,还是双线性过滤,三线性过滤。各个区域显示的内容应该都是"一致"的,采样逻辑一致,在正交情况下得到的采样结果就会一致,画面就会一致。

当视角逐渐倾斜时,情况便发生了变化。可以看到,贴图在透视关系下被压缩成一个梯形,而屏幕上的某一个像素点此时对应到纹理空间中的“长条”区域。这意味着在同样的屏幕分辨率下,纹理需要被更密集地采样才能保持清晰。然而,传统的双线性或三线性过滤只会在纹理的相邻像素间进行平均,它并不能感知“方向性”的拉伸。

11.png

于是,当这种拉伸主要沿着某一个方向(例如 V 轴)发生时,原有过滤会出现“模糊一片或锯齿拖影”的情况。

各向异性过滤(AF)正是为了解决这种在倾斜角度观察纹理时的模糊问题而提出的。 各向异性过滤(Anisotropic Filtering)会估算像素在纹理空间中的椭圆采样范围,并在主要拉伸方向上进行多次采样(通常 2 至 16 次),再将结果加权平均。这种方式能在倾斜角度下保持纹理细节清晰。

现在让我们将贴图信息应用上 AF

// 设置主纹理 & 各项异性过滤
const texture = this.resources.items[this.textureName] // 从 Solar System Scope 下载的贴图
texture.generateMipmaps = true// 默认为 true 是否为纹理生成 mipmap 用于后续三线性插值
texture.minFilter = THREE.LinearMipMapLinearFilter // 三线性插值
texture.colorSpace = THREE.SRGBColorSpace// 色彩空间
texture.anisotropy = 8// 设定各向异性值 越大越耗性能!!

当然,如果你想知道当前最大可支持各向异性值是多少。可以通过console.log('Max AF:', renderer.capabilities.getMaxAnisotropy()); 来获取该值可设定的最大值是多少。

但目前来说 8 对我们够用。

现在在看星球最后的问题也解决了!

13.png

当然,我无法通过一篇文章来讲清楚所有的内容,包括 为什么要用TBN?大气层是怎么实现的?场景辉光镜头怎么实现?一切还请观众老爷们去看看源码。

4.最终成果 🎉

细节调整完毕后,网站终于大功告成!🚀

final.png

5.最后的一些话 🗣

人们常说,AI 时代将取代那些碌碌无为的人。 但我更愿意相信,AI 的到来,是在解放那些富有创造力的灵魂—— 让他们得以更轻易地将脑海中的灵感化为现实 💫。

至于 Web3D,这个概念似乎一直徘徊在热潮之外, 既未消逝,也未真正崛起。 然而,随着 AI 的出现技术门槛的逐渐降低,它或许终将迎来属于自己的时刻 🚀。

我已经厌倦了那些平平无奇、千篇一律的网页。 我希望未来的网络世界能拥有—— 更多的色彩 🎨, 更多的想象空间 🌌, 更多被 AI 点燃的创造之光 ✨。

而我愿意,为那些仍然相信创作力的人,铺出一条属于未来的路。 🌈

6.专栏

本专栏的愿景

本专栏的愿景是通过分享 Three.js 的中高级应用和实战技巧,帮助开发者更好地将 3D 技术应用到实际项目中,打造令人印象深刻的 Hero Section。我们希望通过本专栏的内容,能够激发开发者的创造力,推动 Web3D 技术的普及和应用。

加入社区,共同成长

如果您对 Threejs 这个 3D 图像框架很感兴趣,或者您也深信未来国内会涌现越来越多 3D 设计风格的网站,欢迎加入 ice 图形学社区。这里是国内 Web 图形学最全的知识库,致力于打造一个全新的图形学生态体系!您可以在认证达人里找到我这个 Threejs 爱好者和其他大佬。

此外,如果您很喜欢 Threejs 又在烦恼其原生开发的繁琐,那么我诚邀您尝试 TresjsTvTjs, 他们都是基于 VueThreejs 框架。 TvTjs 也为您提供了大量的可使用案例,并且拥有较为活跃的开发社区,在这里你能碰到志同道合的朋友一起做开源!

7.往期回顾

🎮 前端也能造城市?源码公开:那个被外网 2.7 万人围观的 Three.js 小游戏

😲我又写出了被 Three.js 官推转发的项目?!🥳🥳(源码分享)

😮😮😮 我写出了被 Threejs 官推转发的项目🚀✨?!

❌