全国免费咨询:

13245491521

VR图标白色 VR图标黑色
X

中高端软件定制开发服务商

与我们取得联系

13245491521     13245491521

2025-05-26_我写出了被 Threejs 官推转发的项目!!!

您的位置:首页 >> 新闻 >> 行业资讯

我写出了被 Threejs 官推转发的项目!!! 点击关注公众号,“技术干货” 及时达!前置条件hello! 欢迎阅读本篇文章!这篇文章会探讨如何高定制化地构建一个自己喜欢的 3D 场景。我们将深入探讨Three.js、Shader(GLSL)、Cursor rules & MCP Servers以及2D & 3D 定制化资源获取等技术领域。 在开始之前,请确保您已经具备以下基础知识: 「1. Three.js 基础」 核心概念掌握:场景(Scene):3D 空间的容器相机(Camera):观察场景的视角渲染器(Renderer):将 3D 场景绘制到屏幕几何体(Geometry):物体的形状定义材质(Material):物体的外观特性网格(Mesh):几何体和材质的组合推荐学习资源:Bruno Simon的threejs-journey课程是非常优秀的入门教程。如果您希望了解我的个人学习路径,欢迎在评论区留言,当评论数达到一定程度时,我会专门撰写一篇详细的学习路线指南。「并且这篇文章所展示的作品会参加 Threejs-journey 在今年 5 月的作品挑战,第一名会得到一个免费的 threejs-journey,我相信这个作品会得到好的名次,并且承诺获得的任何奖品将会在下一篇稀土掘金文章或者沸点评论区中抽取一个已关注的读者赠予」 「2. Shader 编程基础」 GLSL(OpenGL Shading Language)基础:顶点着色器(Vertex Shader):处理顶点位置和属性片元着色器(Fragment Shader):处理像素颜色和效果Three.js 中的自定义着色器实现1.Page 预览话说这天老何在准备去就业市场上看看自己几斤几两, 就和以前还有联系的老同学要了下他们公司HR的微信。把简历和个人网站发过去了。在经过 10 分钟的漫长等待后,我得到了以下尴尬的画面 那么让我们进入正题,究竟是什么样的网站能够让 HR 小姐姐对吴彦祖的关切问候置之不理 (指 "还在吗?") 这次展示的内容有点多,请见谅(tips: 靠近带感叹号的物体按下 F 有惊喜哦) PC 端在线预览地址 (需要魔法):island.vercel.app/ PC 端在线调试界面 (需要魔法):island.vercel.app/#debug 源码地址 (需要魔法):github.com/hexianWeb/i… 「Threejs 转发贴」 2.2D & 3D 资源获取如果在以前,很难想象这仅仅是一个对建模以及图形设计一知半解的普通程序员能写出的场景。但现在是 AI 时代 (只不过大部分是 AI 拿鞭子抽我,而不是我拿鞭子抽 AI)。所以在低于平均水平的地方统统由AI接管。 对于简单场景,资源获取类型分为两类,分别是 2D 资源 & 3D 资源。普通通过资源网站上搜索以及下载的方式我不做过多介绍,详细介绍客制化资源获取 & 处理方式。 3D「lowpoly」风格模型网站 market.pmnd.rs/www.kenney.nl/assetszsky2000.itch.io/poly.pizza/sketchfab.com/search?q=lo…2.1 3D 客制化资源的获取与处理现在的3D AI generation技术逐渐趋于成熟,虽然远远达不到工业化以及正规生产环境的水平,但是用来做一些小 demo 还是没问题的。这里附上我经常使用的 AI 3D 模型生成平台 & 生成工作流。 首先我们可以根据Blender资源库中有的模型资源利用任意文生图 or 图生图模型为我们生成一个简易的游戏画面 就比如以下画面: 在确定场景风格和界面 UI 后,我们就可以开始着手场景搭建工作。在这个过程中,经常会遇到资源库中缺少所需元素的情况。对此,我是通过以下方式解决: 先利用如gpt-4o等一些文生图模型来生成一个背景干净无杂物的 2.5 D lowpoly 风格视图。而在这一步给我最大帮助的是awesome-gpt4o-images[13],「这可以帮助您即使不利用节点式图像生成平台也能生成统一风格且稳定的图片方式」 在这里我们参考 [案例 55:低多边形 (Low-Poly) 3D 渲染 (by@azed_ai[14])](github.com/jamez-bondo…[15]) 的提示词生成一个lowpoly风格的马作为使用案例。相应的提示词为: 一个[subject]的低多边形3D 渲染图,由干净的三角形面构成,具有平坦的[color1]和[color2]表面。环境是一个风格化的数字沙漠,具有极简的几何形状和环境光遮蔽效果。随后再将这个对应的马导入任意的AI 3D generation平台 (这里是后期的何贤: 您现在可以尝试混元 3D V2.5[, 他提供较多的免费额度 每日 20 次) 随后你可以很快的到一个lowpoly风格的小马雕塑 这样一来,您就可以在确立统一风格的前提下获取自己需要的 3D 资源,而不是场景中充斥着各种风格迥异的 3D 模型 2.2 2D 客制化资源的获取与处理相信大家 GPT-4o 有着出色的利用 4o 固有的知识库和聊天上下文(包括转换上传的图像或将其用作视觉灵感)统一风格客制化图片生成以及输出能力,这也是我经常使用的统一风格 UI 生成工具。 以下是我生成 UI 素材的工作流: 首先我会确定我想要 UI 素材的风格,这里当然也可以通过awesome-gpt4o-images生成。让我们先假设我很想要的是pixel像素化的 UI,那么我们可以根据以下这张图片作为参考让gpt-4o生成图片。 很明显的表述了我们想要的 UI 风格:多色像素化规范大小。 随后我们可以输入以下提示词让gpt 4o image生成图标墙 Style theiconin the second photo in the same pixelated style as the icons in theleftimage. Give it the same style as the image on theright. Solid blackbackground ?? 图标墙规划设计尺寸建议:正方形或横向16:9比例背景:纯黑色(#000000)风格:参考右侧图片作为色系参考 (右侧图片就是我们一开始让 GPT4o 设计的 游戏 UI 画面) ?? 图标排列(4x4 格式)照相机 ?? 对话框 ?? 爱心 ?? 数字1??数字2?? 数字3?? 数字4?? 数字5??数字6?? 数字8??数字7?? 数字9??加号 ? 减号 ? 斜杠 / 乘号 X ? 图标样式细节建议照相机图标:小巧有镜头感,可添加一点反光效果对话框图标:经典漫画气泡形状,边缘加亮色描边爱心图标:红紫渐变,带像素锯齿感数字:3D立体像素字体风,配亮色阴影加减号/斜杠:对称结构,颜色统一为亮青/蓝紫过渡现在就生成了如上的图表墙,这个时候只要去除背景就可以使用了。可以使用如 网站名称网站功能网站地址Remove.bg操作简单,专注于背景去除www.remove.bg/Photoshop专业级处理,灵活性高需本地安装Adobe Express集成度高,功能全面www.adobe.com/express/我这里就使用remove bg来实现这个需求 这样就获得了对应的像素风格UI。 当然如果说不想要图标墙或者使用雪碧图来使用图标的话可以使用Aspose PNG Splitter[19]等工具网站来拆分图标。这里就看个人喜好。这些方法特别适用于需要少量定制化 UI 元素的项目,如数据可视化、游戏界面等场景。 3.场景搭建在场景搭建环境环境,我无法给出过多建议,因为这需要即使您已经会使用如hyper3D或者混元3D等AI 3D Generation工具,但您仍需要掌握一定程度的blender基本使用能力。 3.1 使用blender-mcp快速辅助 3D 建模、场景创建和操作blender-mcp,他通过 API 添加了对 Poly Haven 资产的支持,并且已经接入了hyper3d,意味着我们可以借用支持MCP Servers的任何工具 (如 Cursor、Claude Desktop 以及最近刚刚支持 MCP 的 Trae)。 Blender MCP提供很多 tool, 不仅可以通过其get_scene_infotool 来获取当前 Blender 场景的详细信息,还可以通过execute_blender_code来在 Blender 中执行任意 Python 代码。这意味我们可以在场景物体中较多的情况下利用Cursor批量的放置 & 调整物体的位置以及大小,并规范管理所有命名。 除了这些基本功能外,blender-mcp拥有 tool 如generate_hyper3d_model_via_imagesgenerate_hyper3d_model_via_text。意味着他可以直接通过终端传入图片或者文字来生成模型: 随后可以在 Blender 里直接获得 看起来还不错,但我仍然只推荐你把这个模型作为一个参考来构建自己的模型,或者单纯的让他充当一个地建的作用帮你获取构建一个场景的参考素材。 很遗憾目前为止没有万能的银弹能够支持我们不需要任何学习成本就可以构建一个完整的可用模型所以你仍然需要能靠自己走到这一步 4.核心代码部分在我今年的第一篇Threejs文章2025 年了,我不允许有前端不会用 Trae 让页面 Hero Section 变得高级!!!(Threejs)中提到过 所以这篇文章我也只会将一部分核心功能是如何实现写出来,而不会从模型导入,光线 & 色彩管理等等功能怎么实现的一一往下叙述。那么 现在让我们进入本项目的代码核心片段。 4.1 相机 以及 后处理让我们现将对应的模型简单的使用gltf Viewer进行查看 但要要实现呈现 2.5D 效果视角的来说,我们需要使用到正交相机而不是透视相机 此时我们将相机切换为正交相机 constaspect =this.sizes.aspect this.frustumSize =8 this.orthographicCamera = new THREE.OrthographicCamera( -this.frustumSize * aspect, this.frustumSize * aspect, this.frustumSize, -this.frustumSize, -50, 100, ) this.scene.add(this.orthographicCamera)此时场景呈现效果变为 随后使用合适的后处理优化场景呈现效果。这里我们追求展现出比较老旧游戏机风格的画面,所以我们选择使用RenderPixelatedPass后处理效果来处理画面 this.renderPass = new RenderPass(this.scene,this.camera.instance) // 创建像素化Pass this.pixelPass = new RenderPixelatedPass(3,this.scene,this.camera.instance) this.pixelPass.normalEdgeStrength =0.53 this.pixelPass.depthEdgeStrength =0.4 // 创建输出Pass用于提亮画面 this.outputPass = new OutputPass() this.outputPass.exposure =1.2// 增加曝光度 this.outputPass.toneMapping = THREE.ReinhardToneMapping// 使用Reinhard色调映射 this.outputPass.toneMappingExposure =1.2// 色调映射曝光度 this.composer = new EffectComposer(this.renderer) this.composer.addPass(this.renderPass) this.composer.addPass(this.pixelPass) // 此时像素化效果已经生效,但画面比较灰暗 需要提亮 this.composer.addPass(this.outputPass)// 添加输出Pass此时画面就已经有点复古GBA彩色游戏机画面的感觉了 4.2 角色控制 以及 Octree在实现类似宝可梦城镇的经典四方向移动体验时,常规的 3D 角色控制器如ecctrl往往难以满足需求,这是因为它们通常提供的是自由的全方位移动控制。而要实现这种复古风格的受限移动,关键在于对键盘输入的特殊处理和移动方向的精确控制。 传统 3D 控制器允许 360 度自由移动,而经典 RPG 需要限制为四个基本方向常规控制器通常处理的是连续输入,而我们需要离散的方向切换「其实是我将角色的移动方向限制在了 "上下左右" 四个方向」。那么我是如何做到的呢? 首先我们需要让键盘能在合适的时候相应对应的行为 // 按键按下事件 window.addEventListener('keydown', (e) = { switch (e.code) { case'ArrowUp': case'KeyW': this.actions.up =true this.keys.w =true this.keys.arrowUp =true break case'ArrowDown': case'KeyS': this.actions.down =true this.keys.s =true this.keys.arrowDown =true break case'ArrowLeft': case'KeyA': this.actions.left =true this.keys.a =true this.keys.arrowLeft =true break case'ArrowRight': case'KeyD': this.actions.right =true this.keys.d =true this.keys.arrowRight =true break case'Space': this.actions.brake =true this.keys.space =true // 跳跃逻辑 if(e.code ==='Space'this.playerOnFloor && !this.character.isSitting) { this.jump() } break case'KeyR': this.actions.reset =true break case'KeyZ': this.toggleSit() break } })在这里有一个小插曲来解释我「为什么使用 event.code 而不是 event.key」 对于欧洲的玩家比如法国来说他们使用的键盘布局被称为AZERTY layout,实际上对应键位如下 「所以我们需要根据,键位在键盘上的物理位置来确定用户输入的指令。」 物理键位能确保玩家获得相同的控制体验,无论使用什么语言的键盘。不论是AZERTY布局,或者我们常用的QWERTY布局都可以让方向控制保持一致。 在能够精确的控制当前相应不同地域键盘的指令之后,我们来实现角色的移动逻辑 移动逻辑相关flowmap如下: ┌───────────────┐ │ 1. 是否坐下? │ │ isSitting? │ └──────┬────────┘ | │是 | ▼ | [直接返回] │否 ▼ ┌────────────────────────────┐ │ 2. 初始化 moveX, moveZ, │ │ newDirection │ └────────────┬───────────────┘ ▼ ┌────────────────────────────┐ │ 3. 是否在地面? │ │ !playerOnFloor │ └──────┬─────────────┬───────┘ │否 │是 │ ▼ │ playerVelocity.y -= GRAVITY * dt ▼ ┌────────────────────────────┐ │ 4. 计算 speedDelta │ │ (地面上快, 空中慢) │ └────────────┬───────────────┘ ▼ ┌────────────────────────────┐ │ 5. 检查按键输入 │ │ (WASD/方向键) │ └────────────┬───────────────┘ ▼ ┌────────────────────────────┐ │ 6. 有移动输入? │ │ (moveX ≠ 0 || moveZ ≠ 0) │ └──────┬─────────────┬───────┘ │否 │是 │ ▼ │ updateCharacterRotation(调整朝向) | │ 行走动画 │ playAnimation('walk') │ │ playerVelocity.x/z += moveX/Z ▼ ▼ ┌────────────────────────────┐ │ 7. 没有移动且在地面? │ │ (!isSitting && onFloor) │ └──────┬─────────────┬───────┘ │计算完毕 │是 │ ▼ │ playAnimation('idle') ▼ ┌────────────────────────────┐ │ 8. 速度阻尼 │ │ playerVelocity *= damping │ └────────────┬───────────────┘ ▼ ┌────────────────────────────┐ │ 9. 计算位移 │ │ deltaPosition = │ │ playerVelocity * dt │ └────────────┬───────────────┘ ▼ ┌────────────────────────────┐ │ 10. 移动碰撞体 │ │ playerCollider.translate │ └────────────┬───────────────┘ ▼ ┌────────────────────────────┐ │ 11. 碰撞检测与修正 │ │ playerCollisions() │ └────────────┬───────────────┘ ▼ ┌────────────────────────────┐ │ 12. 动画状态更新 │ │ updateAnimationState() │ └────────────┬───────────────┘ ▼ ┌────────────────────────────┐ │ 13. 同步模型与碰撞体 │ │ updateModelFromCollider() │ └────────────────────────────┘现在我们需要引入Octree了,他算是在threejs中常见的碰撞检测方法。但这里我建议不要直接使用场景模型作为Octree,Octree适合稀疏三维空间,层级分明,而在动态物体多时重建开销大 。我非常推荐你构建一个专属于Octree用的碰撞检测用Object 3D网格基本体,他要做的事情很简单,只是尽可能的将那些场景中的 "实体" 包裹住,比如当前 ** 视觉场景 (图 1)** 如下: 那么对应的 ** 碰撞用网格基本体 (图 2)** 为: 简单的网格基本体能够降低用户设备的性能需求!毕竟不可能要求大家的电脑都是 4090。那么「用户操控的角色实际上更像是在图 2 中的碰撞专用基本体中移动,然后将位置实时映射到视觉场景中的详细模型上。当用户在 "Octree" 的世界中碰到障碍时,则会 “阻碍” 其继续进行移动」。 现在先让我们看看角色控制代码, 从一个tick内需要执行那些逻辑我想会更好理解一点 (你可以理解一个tick就是画面上的 “一帧”,但这种从物理层面是错误的) // 判断是否有移动动作 constisAnyMovementKeyPressed =this.actions.up ||this.actions.down ||this.actions.left ||this.actions.right // 角色移动 if(!this.character.isSitting) { this.moveCharacter(deltaTime) } elseif(!isAnyMovementKeyPressed this.playerOnFloor) { // 阻尼 constdamping = Math.exp(-10* deltaTime) -1 this.playerVelocity.addScaledVector(this.playerVelocity, damping) // 计算位移 constdeltaPosition =this.playerVelocity.clone().multiplyScalar(deltaTime)//计算实际位移 this.playerCollider.translate(deltaPosition) //应用位置更新 } this.playerCollisions()//碰撞检测与修正 this.updateModelFromCollider()//同步模型和碰撞体我们可以看到,当用户在没有坐着时触发移动指令则会调用moveCharacter相关逻辑,而当用户停止移动时则会使用户缓慢停下来,这里使用Math.exp(-10 * deltaTime) - 1来模拟阻尼效果,使得damping得以迅速降为 -1。 而在之前的角色控制流程图中提到移动的逻辑说白了就是计算位移,移动碰撞体,碰撞检测与修正最后同步模型与碰撞体。 4.2.1 计算位移位移计算逻辑很简单,就像我们正常去操控一个网格基本体在三维空间的位置,速度 X 时间 = 位移 (标量), 确定方向将位移应用到该方向上构成矢量。 moveCharacter(deltaTime) { if(this.character.isSitting) return // 计算移动方向 let moveX =0 let moveZ =0 let newDirection =null // 重力 if(!this.playerOnFloor) { this.playerVelocity.y -=this.GRAVITY * deltaTime } // 速度 constspeedDelta = deltaTime * (this.playerOnFloor ?25:8) // 用 actions 判断移动 if(this.actions.up) { moveZ = -speedDelta newDirection = new THREE.Vector3(0,0,1)// 朝向-Z } elseif(this.actions.down) { moveZ = speedDelta newDirection = new THREE.Vector3(0,0, -1)// 朝向+Z } elseif(this.actions.left) { moveX = -speedDelta newDirection = new THREE.Vector3(1,0,0)// 朝向-X } elseif(this.actions.right) { moveX = speedDelta newDirection = new THREE.Vector3(-1,0,0)// 朝向+X } // 添加速度 if(moveX !==0|| moveZ !==0) { // 更新角色朝向 this.updateCharacterRotation(newDirection) // 只有在地面且不是跳跃时才播放行走动画 if(this.playerOnFloor this.currentAnimation !==this.animations.jump) { this.playAnimation('walk') } // 添加速度 if(moveX !==0) { this.playerVelocity.x += moveX } if(moveZ !==0) { this.playerVelocity.z += moveZ } } elseif(this.playerOnFloor) { // 没有移动时播放待机动画 if(!this.character.isSitting this.currentAnimation !==this.animations.jump) { this.playAnimation('idle') } } // 阻尼 constdamping = Math.exp(-4* deltaTime) -1 this.playerVelocity.addScaledVector(this.playerVelocity, damping) // 位置更新 constdeltaPosition =this.playerVelocity.clone().multiplyScalar(deltaTime) this.playerCollider.translate(deltaPosition) // 动画状态更新 this.updateAnimationState() }4.2.2 避免多转半圈「这里唯一要注意的地方就是更新角色朝向我单独拿出来写了」,一般的思路不应该是在明确WSAD对应当前角色的东西南北面朝向前提下将代码写成以下形式吗? if(this.actions.up) { moveZ = -speedDelta newDirection=newTHREE.Vector3(0,0,1)// 朝向-Z newRotation = Math.PI/2//更新旋转 } elseif(this.actions.down) { moveZ =speedDelta newDirection=newTHREE.Vector3(0,0, -1)// 朝向+Z newRotation = -1* Math.PI/2//更新旋转 } elseif(this.actions.left) { moveX = -speedDelta newDirection=newTHREE.Vector3(1,0,0)// 朝向-X newRotation =0//更新旋转 } elseif(this.actions.right) { moveX =speedDelta newDirection=newTHREE.Vector3(-1,0,0)// 朝向+X newRotation = Math.PI //更新旋转 }但这种代码会在用户角色在从+Z轴转向-X轴时出现「多转半圈」的问题, 因为我们转向用程序写出来就是从newRotation = -1 * Math.PI/2状态渐变到newRotation = Math.PI状态 ,不经过特殊处理他一定会有一种情况如下图 先从 -π/2 到 0 再到 π。 实际画面为 为了避免这种情况我们需要将转向限制在[-PI, PI]之间 updateCharacterRotation(newDirection) { if(!newDirection ||this.character.currentDirection.equals(newDirection)) return // Store new direction this.character.currentDirection= newDirection // Calculate the appropriate rotation based on direction lettargetRotation =0 if(newDirection.z=== -1) { targetRotation =0// Facing -Z } elseif(newDirection.z===1) { targetRotation =Math.PI// Facing +Z } elseif(newDirection.x=== -1) { targetRotation =Math.PI/2// Facing -X } elseif(newDirection.x===1) { targetRotation = -Math.PI/2// Facing +X } // Get the current rotation constcurrentRotation =this.character.instance.rotation.y // Calculate the difference between the current rotation and the target rotation letdeltaRotation = targetRotation - currentRotation // 归一化 deltaRotation 到 [-PI, PI] 区间 deltaRotation = ((deltaRotation +Math.PI) % (2*Math.PI) +2*Math.PI) % (2*Math.PI) -Math.PI // Calculate the new target rotation constnewTargetRotation = currentRotation + deltaRotation // Animate rotation gsap.to(this.character.instance.rotation, { y: newTargetRotation, duration:0.2, ease:'power1.out', }) }这样一来就不会多转半圈了 4.2.3 碰撞检测和修正_移动碰撞体_没什么好讲的,仅仅只是应用矢量到对应的对象上,让我们集中在碰撞检测上。首先我们得使用Octree基于我们创建的碰撞专用基本体构建八叉树 Octree提供给我们fromGraphNode从 three.js 的 Object3D(通常是 Mesh 或 Group)中提取所有三角形,构建八叉树。 import{Octree}from'three/addons/math/Octree.js' this.worldOctree=newOctree() setupCollider() { // Initialize octree from the collision model if("碰撞专用网格基本体") { this.worldOctree=newOctree() this.worldOctree.fromGraphNode("碰撞专用网格基本体") } }随后为用户角色创建胶囊体 import{Capsule}from'three/addons/math/Capsule.js'this.playerCollider=newCapsule( newTHREE.Vector3(0,2.35,0), newTHREE.Vector3(0,3,0), 0.35,)此时场景实际上在Octree那边可能是这样的 用户角色的碰撞体积以胶囊体为准,而场景的碰撞体则以图中黑色建筑为准。 而碰撞检测则需要用到Octree提供的另一个方法capsuleIntersect: 检测胶囊体与八叉树内所有三角形的碰撞,返回碰撞法线和深度。 playerCollisions() { constresult =this.worldOctree.capsuleIntersect(this.playerCollider)//检测胶囊体与八叉树内所有三角形的碰撞,返回碰撞法线和深度。 this.playerOnFloor =false if(result) { this.playerOnFloor = result.normal.y 0 // Adjust position to prevent clipping if(result.depth =1e-10) { this.playerCollider.translate(result.normal.multiplyScalar(result.depth)) } } }那么他是如何起作用的呢? 正如前面提到capsuleIntersect: 检测胶囊体与八叉树内所有三角形的碰撞,返回碰撞法线和深度,则「当有返回法线矢量,则证明胶囊体与墙壁碰撞。接着让角色朝法线方向移动来抵消掉用户向墙里走的位移, 从而避免穿模的出现」 this.playerCollider.translate(result.normal.multiplyScalar(result.depth))如果你想了解更多的Octree碰撞检测逻辑,我推荐你看threejs的这个官方用例[23] 4.3. 岩浆与海最后是场景中的水体,比如说岩浆和海,这里我简单拿岩浆举例 通常这种风格的水体在行业内被称为 “风格化水体 (stylized water | toon water) ”。你可以通过搜索这些关键词来获取不一样的水体风格,让我们来分析做这样一个水体需要经理那些步骤。 Step1: 我们需要一个水面纹理 Step2: 在靠近水体边缘的地方加入渐变浮沫 Step3: 让水面真正流动起来 4.3.1 水面纹理首先我们需要让一个平平无奇的平面看起来像是水面 实现这种水面的方式由很多种,比如你可以在网上寻找这种水面遮罩素材 或者自己选择一种噪声来模拟水面,比如这篇文章[24]使用柏林噪声来模拟水面效果 而我使用的则是「蜂窝噪声 (Cellular Noise)」, 也被称为网格噪声。网格噪声基于距离场,这里的距离是指到一个特征点集最近的点的距离。这里同样为你奉上由ShaderBook写的蜂窝噪声相关文章[25], 但我仍会简单阐述其实现原理。 首先我们需要知道什么是距离场,即为 (SDF),定义说有符号距离场 (SDF) 是计算机图形学中常用于渲染的形状的数学表示。它是一个函数,接收空间中的一个点,并返回到该形状表面上最近点的距离,并用符号表示该点位于形状内部还是外部: 如果值为负,则该点位于形状内部。如果值为零,则该点位于形状的表面上。如果值为正,则该点位于形状之外。让我们来看以下代码, floatsdfCircle(vec2 center,floatr, vec2 pos){returndistance(center, pos) - r;} voidmain(){ vec2 uv = gl_FragCoord.xy; floatt = sdfCircle(iResolution *0.5, iResolution.y *0.4, uv); gl_FragColor = vec4(vec3(t),1.0);}其中iResolution * 0.5就是画布中心的位置,而iResolution.y * 0.4则代表值为当前画布高度的0.4。 distance(center, pos) - r就是计算当前画布上的每一个点pos到画布中心center的举例跟iResolution.y * 0.4谁大谁小,举例 当点pos的距离小于iResolution.y * 0.4时,则t为负数,此时gl_FragColor = vec4(vec3(t), 1.0);就是gl_FragColor = vec4(vec3(负数), 1.0);显示成黑色。当点pos的距离大于iResolution.y * 0.4时,则t为正数,此时gl_FragColor = vec4(vec3(t), 1.0);就是gl_FragColor = vec4(vec3(正数), 1.0);显示成灰色或白色。最后得出的效果如下: 这是个很简单的理论,但也是蜂窝噪声的基石。再让我们会看这句话「计算它们与当前像素的距离并存储最接近的值」 假设现在我们平面上有 N 个 "中心点",那么我们现在要做的事分别求出单个片元到所有 "中心点" 的距离并求出最小值 uniform vec2 iResolution;uniformfloatiTime; vec2 points[9]; voidinit(){ points[0] = vec2(0.05,0.15); points[1] = vec2(0.35,0.27); points[2] = vec2(0.78,0.04); points[3] = vec2(0.25,0.46); points[4] = vec2(0.50,0.55); points[5] = vec2(0.91,0.37); points[6] = vec2(0.28,0.67); points[7] = vec2(0.53,0.76); points[8] = vec2(0.73,0.75);} vec2getPoint(intindex){returnsin(points[index] *6.28+ iTime /3.0) *0.5+0.5;} voidmain(){init(); vec2 uv = gl_FragCoord.xy / iResolution.xy;floatm_dist =1.0; for(inti =0; i 9; i++) { floatdist = distance(uv, getPoint(i)); m_dist = min(m_dist, dist); } gl_FragColor = vec4(0.0, m_dist *2.25,0.0,1.0);}那么随着特征点越来越多,水面的效果也就随之显现了 随后可以使用灰度作为混合因子,混合水面颜色和浮沫颜色即可,这点也非常简单 uniform vec3 color1; // 水色 uniform vec3 color2; // 白色片元着色器 voidmain(){ init(); vec2 uv = gl_FragCoord.xy / iResolution.xy; floatm_dist =1.0; // 计算点的效果 for(inti =0; i 11; i++) { if(float(i) = numPoints)break; floatdist = distance(uv, getPoint(i)); m_dist = min(m_dist, dist); }floatfactor = smoothstep(0.05,0.3, m_dist); // 使用 factor 作为混合因子, 混合两种颜色 vec3 waterColor = mix(color1, color2,factor); gl_FragColor = vec4(waterColor,1.0); }「当然你现在可以增加多个特征点或者操控 uv 做出水面蠕动特效」,但我们后续会使用flowmap就不在这里操作了。 4.3.2 边缘浮沫在水面边缘存在一圈白色的浮沫,这是一种hack手段的模拟菲涅尔现象。我不推荐你学这一块,因为他的应用场景仅仅只在这个案例中,所以我只贴上基本的shader代码 「顶点着色器」 // Calculate edge glow effect floatgetEdgeGlow(vec2 uv){ // Calculate distance to edge floatdistToEdge = min(min(uv.x,1.0- uv.x), min(uv.y,1.0- uv.y)); // Use smooth step function to create a soft transition return1.0- smoothstep(0.0, edgeWidth, distToEdge); }「片元着色器」 voidmain(){ init(); vec2uv=gl_FragCoord.xy / iResolution.xy; floatm_dist=1.0; // Calculate point effect for(inti=0; i 9; i++) { if(float(i) = numPoints)break; floatdist=distance(uv, getPoint(i)); m_dist = min(m_dist, dist); } // Calculate base color vec3baseColor=vec3(m_dist * colorIntensity); // Add edge glow floatedgeGlow=getEdgeGlow(uv) * edgeIntensity; // 获取边缘强度做混合因子 // Mix base color and edge glow vec3finalColor=mix(baseColor, vec3(1.0), edgeGlow); gl_FragColor = vec4(finalColor,1.0); }4.3.3 流动水面这里我要向你介绍游戏中流动水体或者模拟流体的一种常见方法:flowmap。这里的flowmap指代的并不是程序中的流程图,而是「一张记录了 2D 向量信息的纹理 Flow map 上的颜色(通常为 RG 通道)记录该处向量场的方向,让模型上某一点表现出定量流动的特征」 接下来我会向你解释flowmap是如何工作的,以及如何低成本的构建一个flowmap。最后我会想你讲述如何将flowmap使用在threejs中 4.3.3.1flowmap的原理** 将2D向量场信息编码到纹理的红色通道 & 绿色通道中,每个纹素代表一个流动方向向量,通过在 shader 中偏移 uv 再对纹理进行采样,来模拟流动效果。** 这就是flowmap的作用。 通俗地说,当片元着色器读取 Flowmap 上某一点的颜色值时: 红色分量 (R) 代表 X 轴方向的流动绿色分量 (G) 代表 Y 轴方向的流动颜色值的大小决定了流动的强度接下来让我们来理解这段话。我们使用一个如图所示的flowmap,观察水面的流动方向。 可以看到水面为橙色的会向右移动,水面为绿的则会向左移动. 4.3.3.2 低成本的构建一个flowmap如果要构建一个正规的工业化flowmap,其实最好选用如Houdini[26]等一些专业的软件,但是如果不想学习Houdini或者,需要再最短时间内拿出一个可运行的demo,可以试试以下两个工具中的一个。 cables.gl[27]- 基于节点的可视化编程工具FlowMap Painter[28]- 专门的 Flowmap 绘制工具这里我简单使用cables.gl[29], 构建一个flowmap。 就比如这样 得到可用的效果后将Flowmap Visualize的值降低为0,随后点击下载就获得你想要的flowmap了。 4.3.3.3 如何在Threejs中使用flowmap第一种方法是使用Threejs中自带的Water2类,详情可以参考Threejs Flowmap 官网案例[30]。 核心代码如下: // 创建水面几何体constwaterGeometry=newTHREE.PlaneGeometry(10,10); // 加载Flowmap纹理constflowMap= textureLoader.load('textures/water/flowmap.png'); // 创建水面效果water =newWater(waterGeometry, { scale:1, textureWidth:1024, textureHeight:1024, flowMap: flowMap}); // 设置位置和旋转water.position.y =1;water.rotation.x = Math.PI * -0.5;scene.add(water);随后就会在平面上生成一个带有 flowmap 流动效果的透明平面 但我们这里使用的是第二种方法, 即为实打实的利用fragment shader操控uv来达到流动效果 // 从Flowmap获取流动向量(值范围从[0,1]映射到[-1,1])vec2flow=texture2D(flowMap, vUv).rg *2.0-1.0; // 计算时间相关的流动偏移量vec2flowOffset=flow * flowSpeed * iTime; // 应用偏移到UV坐标vec2uv=vUv + flowOffset;最后来的效果为 具体效果还需要根据你自己的个人喜好来调整。完整实现代码可参考项目源码库。通过调整 Flowmap 纹理和着色器参数,您可以创建从平静水面到湍急河流等各种水体效果。 5.心路历程在过去几个月的持续创作中,我收获了来自多个平台的关注和鼓励,甚至包括一些我曾视为行业偶像的人士的认可。这些支持让我既感到荣幸,也时常感到惶恐。 但我时常看着平台消息通知倍感压力,我认为我远远没有达到能够 “系统化教授别人”threejs的地步。 我必须坦诚地承认: 我并非科班出身的专业开发者我的 Three.js 知识体系还存在许多不足持续的高质量输出对我而言是巨大的挑战这也是我想说的,我认为后续我会暂停目前 Three.js 技术文章的定期更新计划。没准会变回曾经的年更博主。但这并不意味着我会停止创作: 我的 GitHub 仓库仍会持续更新有趣的项目社交媒体账号会分享新的探索和发现当有真正值得分享的内容时,我依然会撰写文章5.最后的一些话技术的未来与前端迁移随着 AI 技术的快速发展,各类技术的门槛正在大幅降低,以往被视为高门槛的3D技术也不例外。与此同时,过去困扰开发者的数字资产构建成本问题,也正在被最新的3D generation技术所攻克。这意味着,在不久的将来,前端开发将迎来一次技术迁移,开发者需要掌握更新颖的交互方式和更出色的视觉效果。 本专栏的愿景本专栏的愿景是通过分享Three.js的中高级应用和实战技巧,帮助开发者更好地将3D技术应用到实际项目中,打造令人印象深刻的Hero Section。我们希望通过本专栏的内容,能够激发开发者的创造力,推动Web3D技术的普及和应用。 ?此外,如果您很喜欢Threejs又在烦恼其原生开发的繁琐,那么我诚邀您尝试「Tresjs」和「TvTjs」, 他们都是基于Vue的Threejs框架。「TvTjs」也为您提供了大量的可使用案例,并且拥有较为活跃的开发社区,在这里你能碰到志同道合的朋友一起做开源! ?关注更多AI编程资讯请去AI Coding专区:https://juejin.cn/aicoding 点击"阅读原文"了解详情~ 阅读原文

上一篇:2023-09-26_国家杰青!清华教授,加盟母校! 下一篇:2023-12-21_4090成A100平替?token生成速度只比A100低18%,上交大推理引擎火了

TAG标签:

18
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设网站改版域名注册主机空间手机网站建设网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。
项目经理在线

相关阅读 更多>>

猜您喜欢更多>>

我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!

不达标就退款

高性价比建站

免费网站代备案

1对1原创设计服务

7×24小时售后支持

 

全国免费咨询:

13245491521

业务咨询:13245491521 / 13245491521

节假值班:13245491521()

联系地址:

Copyright © 2019-2025      ICP备案:沪ICP备19027192号-6 法律顾问:律师XXX支持

在线
客服

技术在线服务时间:9:00-20:00

在网站开发,您对接的直接是技术员,而非客服传话!

电话
咨询

13245491521
7*24小时客服热线

13245491521
项目经理手机

微信
咨询

加微信获取报价