本文介紹了如何使用 React Three Fiber(R3F)和 Three.js 實現一個從中心向外擴散的光圈特效(DiffuseAperture 組件),并將其集成到城市 3D 模型(CityModel 組件)中。該特效通過動態調整圓柱幾何體的大小和透明度,模擬出類似水波擴散的視覺效果,可用于增強 3D 場景的交互反饋或氛圍營造。我們將從功能原理、實現細節和集成方式三個方面,用通俗易懂的語言講解技術要點。
?更多該系列更新請參考
Three.js 如何控制 GLB 模型的內置屬性實現精準顯示-CSDN博客
React Three Fiber 實現 3D 模型點擊高亮交互的核心技巧-CSDN博客
在 React Three Fiber 中實現 3D 模型點擊擴散波效果-CSDN博客
?什么是 “擴散光圈”?
想象一下,當你往平靜的湖面扔一顆石子,水波會從落點向四周一圈圈擴散,逐漸變大并消失 —— 我們實現的 “擴散光圈” 就是這個效果的 3D 版本。在 CityModel 城市模型中,這個特效表現為:從城市中心(原點)向外擴散的環形光圈,隨著時間推移,光圈半徑不斷增大,同時逐漸變得透明,最終消失;隨后又從中心重新開始擴散,形成循環動畫。
這個特效可以為 3D 城市模型增添動態感,比如:
- 作為場景加載完成的 “開場動畫”
- 作為用戶點擊城市中心的交互反饋
- 模擬信號覆蓋、能量擴散等業務場景
?核心原理:用 “空心圓柱” 模擬光圈
實現這個特效的核心思路很簡單:用一個 “沒有上下底的空心圓柱” 作為光圈的載體,通過動態改變它的大小和透明度,讓它看起來像在 “擴散消失”。
為什么用圓柱?
- 圓柱的側面是環形,天然適合模擬 “光圈”
- 可以通過縮放控制半徑(大小),通過透明度控制可見度
- 隱藏上下底面后,只剩側面,視覺上更像 “一圈光”
?實現細節:讓光圈 “動” 起來的關鍵
1. 基礎結構:構建空心圓柱
// 核心代碼片段:創建圓柱幾何體
<cylinderGeometryargs={[initialRadius, // 頂部半徑(初始大小)initialRadius, // 底部半徑(和頂部相同,保證是正圓)height, // 圓柱高度(光圈的“厚度”,越薄越像2D光圈)64, // 徑向分段數(數值越大,光圈邊緣越平滑)1, // 高度分段數(固定為1即可)true, // 開口(關鍵!讓圓柱沒有上下底,只剩側面)]}
/>
通俗解釋:就像用一張紙條卷成一個空心圓筒,然后把上下兩個口剪掉,只剩中間的環形側面 —— 這就是我們的 “光圈” 雛形。
2. 動態變大:讓光圈 “擴散”
光圈的 “擴散” 效果,本質是讓圓柱的半徑隨時間不斷增大:
// 核心代碼片段:每幀更新半徑
useFrame(() => {// 1. 讓半徑隨時間增大(expandSpeed 控制擴散快慢)radiusRef.current += expandSpeed * 0.016;// 用縮放控制大小(x和z軸同時放大,保證是正圓)apertureRef.current.scale.set(radiusRef.current, // x軸縮放1, // y軸不縮放(保持厚度不變)radiusRef.current, // z軸縮放);
});
通俗解釋:想象圓筒被吹氣球一樣慢慢變大,而且是均勻地向四周擴張,就像水波越來越大。這里的?
0.016
?是為了適配屏幕刷新率(約 60 幀 / 秒),讓不同設備上的擴散速度一致。
3. 逐漸消失:讓光圈 “淡化”
光擴散的同時會慢慢變淡,通過降低材質的透明度實現:
// 核心代碼片段:控制透明度
useFrame(() => {// 2. 讓透明度隨時間降低(fadeSpeed 控制消失快慢)opacityRef.current -= fadeSpeed * 0.016;material.opacity = Math.max(opacityRef.current, 0); // 透明度不小于0
});
通俗解釋:就像用半透明的紙做的圓筒,隨著擴散,紙張越來越薄,直到完全看不見。
4. 循環動畫:讓光圈 “重復擴散”
當光圈擴散到最大范圍或完全消失后,需要重置狀態重新開始:
// 核心代碼片段:重置條件
if (radiusRef.current > maxRadius || opacityRef.current <= 0) {radiusRef.current = initialRadius; // 半徑變回初始大小opacityRef.current = 1; // 透明度重置為完全可見
}
通俗解釋:這就像設置了一個循環播放的 “水波” 動畫,一波消失后,新的一波從中心重新開始擴散。
5. 紋理增強:讓光圈更生動(可選)
如果想讓光圈有紋路(比如光斑、條紋),可以添加紋理貼圖:
// 核心代碼片段:添加紋理
if (textureUrl) {const texture = textureLoader.load(textureUrl); // 加載紋理圖片materialParams.map = texture; // 應用到材質上
}
通俗解釋:就像給圓筒的側面貼上帶花紋的貼紙,讓光圈看起來更有細節(比如模擬雷達掃描的波紋)。
光波組件代碼(完整)
import { useFrame } from '@react-three/fiber'
import * as THREE from 'three'
import { useRef, useMemo } from 'react'// 擴散光圈組件
export const DiffuseAperture = ({color = ' 0x4C8BF5', // 光圈顏色initialRadius = 0.5, // 初始半徑maxRadius = 10, // 最大擴散半徑expandSpeed = 2, // 擴散速度(半徑增長速率)fadeSpeed = 0.8, // 淡出速度(透明度降低速率)textureUrl, // 側面紋理貼圖URL(可選)height = 1.5, // 從 0.1 增大到 1.5(根據場景比例調整)radialSegments = 128, // 徑向分段數(從64增至128,邊緣更平滑)heightSegments = 3, // 高度分段數(從1增至3,厚度方向有細節)
}: {color?: stringinitialRadius?: numbermaxRadius?: numberexpandSpeed?: numberfadeSpeed?: numberheight?: numbertextureUrl?: string
}) => {const apertureRef = useRef<THREE.Mesh>(null)const radiusRef = useRef(initialRadius) // 跟蹤當前半徑const opacityRef = useRef(1) // 跟蹤當前透明度// 創建圓柱側面材質(帶紋理支持)const material = useMemo(() => {const textureLoader = new THREE.TextureLoader()const materialParams: THREE.MeshBasicMaterialParameters = {color: new THREE.Color(color),transparent: true, // 啟用透明度side: THREE.DoubleSide, // 確保側面可見}// 若提供紋理URL,加載紋理并應用if (textureUrl) {const texture = textureLoader.load(textureUrl)materialParams.map = texture}return new THREE.MeshBasicMaterial(materialParams)}, [color, textureUrl])// 每幀更新圓柱狀態(半徑增大+透明度降低)useFrame(() => {if (!apertureRef.current) return// 1. 更新半徑(逐漸增大)radiusRef.current += expandSpeed * 0.016 // 基于幀率的平滑增長apertureRef.current.scale.set(radiusRef.current, // X軸縮放(控制半徑)1, // Y軸不縮放(保持高度)radiusRef.current, // Z軸縮放(控制半徑))// 2. 更新透明度(逐漸降低)opacityRef.current -= fadeSpeed * 0.016material.opacity = Math.max(opacityRef.current, 0) // 不小于0// 3. 當完全透明或超出最大半徑時,重置狀態(循環擴散)if (radiusRef.current > maxRadius || opacityRef.current <= 0) {radiusRef.current = initialRadiusopacityRef.current = 1}})return (<mesh ref={apertureRef}>{/* 圓柱幾何體:頂面和底面隱藏,僅保留側面 */}<cylinderGeometryargs={[initialRadius, // 頂部半徑initialRadius, // 底部半徑(與頂部相同,確保是正圓柱)height, // 圓柱高度(厚度)64, // 徑向分段數(越高越平滑)1, // 高度分段數true, // 開口(無頂面和底面)]}/><primitive object={material} /></mesh>)
}
集成到城市模型:讓特效 “服務于場景”
我們的擴散光圈不是孤立存在的,需要和城市模型(CityModel 組件)配合使用,才能發揮最大效果:?
1. 位置對齊:讓光圈從城市中心擴散
// 在 CityModel 組件中使用 DiffuseAperture
return (<>{/* 城市模型(已居中到原點) */}<primitive object={scene} ref={modelRef} />{/* 擴散光圈:放在城市中心 */}<DiffuseAperturecolor="#4C8BF5"initialRadius={0.5}maxRadius={20} // 擴散范圍適配城市大小rotation={[Math.PI/2, 0, 0]} // 旋轉90度,讓光圈與地面平行/></>
);
關鍵細節:城市模型通過?
modelRef.current.position.sub(center)
?已居中到原點(0,0,0),光圈默認也在原點,因此能從城市中心開始擴散。rotation
?是為了讓光圈 “躺平” 在地面上(默認是直立的)。
2. 參數適配:讓特效和場景協調
參數 | 作用 | 適配城市模型的建議值 |
---|---|---|
maxRadius | 光圈最大擴散范圍 | 設為城市寬度的 1.5 倍 |
height | 光圈厚度 | 0.05-0.1(薄一點更自然) |
expandSpeed | 擴散速度 | 2-3(太快看不清,太慢拖沓) |
color | 光圈顏色 | 與城市主色調對比(如藍色) |
3. 增強體驗:多光圈疊加
通過同時渲染多個參數不同的光圈,可以形成更豐富的效果:
// 多光圈疊加示例
<><DiffuseAperture color="#ff6b3b" maxRadius={15} /><DiffuseAperture color="#ffc154" maxRadius={25} expandSpeed={2.5} /><DiffuseAperture color="#609bdf" maxRadius={35} expandSpeed={3} />
</>
效果:就像同時往水里扔三顆石子,形成的水波一圈套一圈,顏色從內到外漸變(紅→黃→藍),增強層次感。
?完整組件引用代碼
注釋掉的部分是模型的點擊高亮,現在為了演示效果,默認是初始化高亮的
import { useGLTF } from '@react-three/drei'
import { useThree } from '@react-three/fiber'
import { useEffect, useRef } from 'react'
import * as THREE from 'three'
import { useModelManager } from '../../../utils/viewHelper/viewContext'
import { DiffuseAperture } from '../../WaveEffect'export const CityModel = ({ url }: { url: string }) => {const { scene } = useGLTF(url)const modelRef = useRef<THREE.Group>(null)const helper = useModelManager()const { camera } = useThree()// const raycaster = useRef(new THREE.Raycaster())// const pointer = useRef(new THREE.Vector2())const highlightedMeshRef = useRef<THREE.Mesh[]>([])// 存儲所有創建的邊緣線對象const edgeLines = useRef<Map<string, THREE.LineSegments>>(new Map())// 添加邊緣高亮效果const addHighlight = (object: THREE.Mesh) => {if (!object.geometry) return// 創建邊緣幾何體const geometry = new THREE.EdgesGeometry(object.geometry)// 創建邊緣線材質const material = new THREE.LineBasicMaterial({color: 0x4c8bf5, // 藍色邊緣linewidth: 2, // 線寬})// 創建邊緣線對象const line = new THREE.LineSegments(geometry, material)line.name = 'surroundLine'// 復制原始網格的變換line.position.copy(object.position)line.rotation.copy(object.rotation)line.scale.copy(object.scale)// 設置為模型的子對象,確保跟隨模型變換object.add(line)edgeLines.current.set(object.uuid, line)}// 移除邊緣高亮效果const removeHighlight = (object: THREE.Mesh) => {const line = edgeLines.current.get(object.uuid)if (line && object.children.includes(line)) {object.remove(line)}edgeLines.current.delete(object.uuid)}// 處理左鍵點擊事件// const handleClick = (event: MouseEvent) => {// // 僅響應左鍵點擊(排除右鍵/中鍵/滾輪)// if (event.button !== 0) return// // 計算點擊位置的標準化設備坐標// pointer.current.x = (event.clientX / window.innerWidth) * 2 - 1// pointer.current.y = -(event.clientY / window.innerHeight) * 2 + 1// // 執行射線檢測,判斷點擊目標// detectClickedMesh()// }// 檢測點擊的Mesh并切換高亮狀態// const detectClickedMesh = () => {// if (!modelRef.current) return// // 更新射線(從相機到點擊位置)// raycaster.current.setFromCamera(pointer.current, camera)// // 檢測與模型的交點(遞歸檢測所有子Mesh)// const intersects = raycaster.current.intersectObject(modelRef.current, true)// if (intersects.length > 0) {// const clickedObject = intersects[0].object as THREE.Mesh// // 僅處理標記為可交互的Mesh// if (// clickedObject instanceof THREE.Mesh &&// clickedObject.userData.interactive// ) {// // 切換高亮狀態:點擊已高亮的Mesh則取消,否則高亮新Mesh// if (highlightedMeshRef.current?.includes(clickedObject)) {// console.log('取消高亮', clickedObject.name)// // 移除邊框高亮// removeHighlight(clickedObject)// const newHighlighted = highlightedMeshRef.current.filter(// (m) => m.name !== clickedObject.name,// )// highlightedMeshRef.current = [...newHighlighted]// } else {// console.log('高亮', clickedObject.name)// // 添加邊框高亮// addHighlight(clickedObject)// highlightedMeshRef.current = [// ...highlightedMeshRef.current,// clickedObject,// ]// }// }// }// }// 模型加載后初始化useEffect(() => {if (!modelRef.current) returnaddModel()const box = new THREE.Box3().setFromObject(modelRef.current)const center = new THREE.Vector3()box.getCenter(center)const size = new THREE.Vector3()box.getSize(size)// 2. 將模型中心移到世界原點(居中)modelRef.current.position.sub(new THREE.Vector3(center.x, 0, center.z)) // 反向移動模型,使其中心對齊原點const maxDim = Math.max(size.x, size.y, size.z)const fov = 100const cameraZ = Math.abs(maxDim / 2 / Math.tan((Math.PI * fov) / 360))camera.position.set(0, maxDim * 0.3, cameraZ * 1)camera.lookAt(0, 0, 0)// 遍歷模型設置通用屬性并標記可交互modelRef.current.traverse((child) => {if (child instanceof THREE.Mesh) {child.castShadow = truechild.receiveShadow = truechild.material.transparent = true// 標記為可交互(后續可通過此屬性過濾)child.userData.interactive = truechild.material.color.setStyle('#040912')addHighlight(child)// 保存原始材質(用于后續恢復或高亮邏輯)if (!child.userData.baseMaterial) {child.userData.baseMaterial = child.material // 存儲原始材質}}})// 綁定點擊事件監聽// window.addEventListener('click', handleClick)// 組件卸載時清理return () => {// window.removeEventListener('click', handleClick)// 移除所有高亮邊緣線highlightedMeshRef.current.forEach((mesh) => {removeHighlight(mesh)})edgeLines.current.clear()highlightedMeshRef.current = []}}, [modelRef.current])// 添加模型到管理器const addModel = () => {if (modelRef.current) {helper.addModel({id: '模型1',name: '模型1',url: url,model: modelRef.current,})}}return (<><primitive object={scene} ref={modelRef} />{/* 擴散光圈:位于模型中心,與模型平面平行 */}{/* 內層光圈:暖紅色系,擴散范圍最小,亮度最高 */}<DiffuseAperturecolor="#ff6b3b" // 內層暖紅(鮮艷)initialRadius={0.1}maxRadius={15} // 最小擴散范圍expandSpeed={2} // 中等擴散速度fadeSpeed={0.6} // 較慢淡出(停留更久)height={0.08}/>{/* 中層光圈:橙黃色系,銜接內外層 */}<DiffuseAperturecolor="#ffc154" // 中層橙黃(過渡色)initialRadius={0.2}maxRadius={20} // 中等擴散范圍expandSpeed={2.5} // 稍快于內層fadeSpeed={0.7} // 中等淡出速度height={0.06}/>{/* 外層光圈:藍紫色系,擴散范圍最大,亮度最低 */}<DiffuseAperturecolor="#609bdf" // 外層淺藍(冷色)initialRadius={0.3}maxRadius={25} // 最大擴散范圍expandSpeed={3} // 最快擴散速度fadeSpeed={0.8} // 最快淡出(快速消失)height={0.04}/></>)
}
?總結:技術點與應用場景
這個擴散光圈特效的核心是 “動態變化”—— 通過 React Three Fiber 的?
useFrame
?實現每一幀的更新,用 Three.js 的圓柱幾何體和材質屬性控制視覺表現。它的優勢在于:
- 性能友好:只用一個簡單的圓柱幾何體,避免復雜計算
- 易于控制:通過參數可輕松調整大小、速度、顏色
- 場景適配:旋轉和位置調整可適配任何 3D 模型
在 CityModel 中,這個特效可用于:
- 城市加載完成的 “開場動畫”
- 用戶點擊城市中心的 “交互反饋”
- 模擬信號覆蓋、能量擴散等業務場景
通過這個小功能,我們可以感受到 3D 特效的魅力 —— 看似復雜的視覺效果,往往是由簡單的幾何變換和動態更新組合而成。