在現代前端開發中,3D可視化已經成為提升用戶體驗的重要手段。然而,許多開發者在實現復雜視覺效果時,往往會首先想到使用Shader(著色器)。雖然Shader功能強大,但學習曲線陡峭,實現復雜度高。本文將介紹一種更簡單高效的替代方案——頂點顏色(Vertex Colors)技術,通過一個彩色二十面體的實現案例,展示如何在不使用Shader的情況下創建令人驚艷的3D效果。
?下圖里每個幾何體有頂點著色的幾何和顯示框線的結合重疊而形成的視覺效果,從左到右分別列舉了三個插值方案
方案1:基于Y軸高度的HSL全色相變化,從底部到頂部呈現彩虹色漸變(圖1)
方案2:固定紅色相,飽和度隨高度變化,從灰到純紅(圖3)
方案3:RGB漸變,從底部的黃色漸變到頂部的紅色(圖2)
本文參考了threejs官網給的下面例子
為什么選擇頂點顏色而非Shader?
Shader無疑是強大的工具,可以實現幾乎任何你能想象到的視覺效果。但對于許多常見的3D可視化需求來說,Shader可能有些"殺雞用牛刀":
學習成本高:GLSL語言和著色器管線對初學者不友好
調試困難:著色器錯誤往往難以定位和修復
性能考量:簡單的頂點顏色渲染通常比復雜著色器更高效
開發效率:使用Three.js內置材質可以快速迭代
在我們的案例中,使用頂點顏色技術完全能夠滿足需求,同時保持了代碼的簡潔性和可維護性。
實現彩色二十面體完整代碼
讓我們通過一個React Three Fiber實現的彩色二十面體組件,來具體看看頂點顏色技術的應用。
import { useRef } from 'react'
import { useFrame } from '@react-three/fiber'
import * as THREE from 'three'type IcosahedronProps = {position: [number, number, number]colorScheme: 1 | 2 | 3
}const Icosahedron = ({ position, colorScheme }: IcosahedronProps) => {const meshRef = useRef<THREE.Group>(null)const radius = 200useFrame(() => {if (meshRef.current) {meshRef.current.rotation.y += 0.005}})// 創建幾何體和顏色const geometry = new THREE.IcosahedronGeometry(radius, 1)const count = geometry.attributes.position.countgeometry.setAttribute('color',new THREE.BufferAttribute(new Float32Array(count * 3), 3),)const positions = geometry.attributes.position as THREE.BufferAttributeconst colors = geometry.attributes.color as THREE.BufferAttributeconst color = new THREE.Color()for (let i = 0; i < count; i++) {const y = positions.getY(i)switch (colorScheme) {case 1: // 基于Y軸高度的HSL顏色color.setHSL((y / radius + 1) / 2, 1.0, 0.5, THREE.SRGBColorSpace)breakcase 2: // 基于Y軸高度的HSL顏色,固定色相color.setHSL(0, (y / radius + 1) / 2, 0.5, THREE.SRGBColorSpace)breakcase 3: // 基于Y軸高度的RGB漸變color.setRGB(1, 0.8 - (y / radius + 1) / 2, 0, THREE.SRGBColorSpace)break}colors.setXYZ(i, color.r, color.g, color.b)}return (<group ref={meshRef} position={position}><mesh geometry={geometry}><meshPhongMaterialcolor={0xffffff}flatShadingvertexColorsshininess={0}/></mesh><mesh geometry={geometry}><meshBasicMaterialcolor={0x000000}wireframetransparentopacity={0.3}/></mesh></group>)
}export const ColorMap = () => {return (<><Icosahedron position={[-400, 0, 0]} colorScheme={1} /><Icosahedron position={[0, 0, 0]} colorScheme={3} /><Icosahedron position={[400, 0, 0]} colorScheme={2} /></>)
}
我們定義了一個
Icosahedron
組件,接受位置和顏色方案作為props。使用useRef
來獲取對3D對象的引用,以便后續動畫控制。
Icosahedron是什么?
Icosahedron(發音:/?a?k?s??hi?dr?n/ 或 /?a?ko?s??hi?dr?n/)是一個幾何術語,源自希臘語:
eíkosi?(ε?κοσι) = "20"
hédra?(?δρα) = "面"或"基面"
指正二十面體——一種由20個完全相同的正三角形面、30條邊和12個頂點組成的柏拉圖立體(Platonic solid)。每個頂點處有5個三角形面相交。對稱性:具有120°旋轉對稱性;結構:所有面、邊、角均全等;可視化:類似足球的經典結構(現代足球的拼合結構即源自截角二十面體)
幾何體創建與顏色設置代碼解析
核心部分在于如何為二十面體的每個頂點設置顏色:
const positions = geometry.attributes.position as THREE.BufferAttribute
const colors = geometry.attributes.color as THREE.BufferAttribute
const color = new THREE.Color()for (let i = 0; i < count; i++) {const y = positions.getY(i)switch (colorScheme) {case 1: // 基于Y軸高度的HSL顏色color.setHSL((y / radius + 1) / 2, 1.0, 0.5, THREE.SRGBColorSpace)breakcase 2: // 基于Y軸高度的HSL顏色,固定色相color.setHSL(0, (y / radius + 1) / 2, 0.5, THREE.SRGBColorSpace)breakcase 3: // 基于Y軸高度的RGB漸變color.setRGB(1, 0.8 - (y / radius + 1) / 2, 0, THREE.SRGBColorSpace)break}colors.setXYZ(i, color.r, color.g, color.b)
}
這段代碼做了以下幾件事:
通過new THREE.IcosahedronGeometry(radius, 1)創建一個二十面體幾何體
為幾何體添加顏色屬性
遍歷所有頂點,根據Y軸坐標和選定的顏色方案為每個頂點設置顏色
三種顏色方案分別展示了不同的著色策略
1. 基本結構
遍歷幾何體所有頂點的循環,對每個頂點:
獲取頂點的Y坐標(
positions.getY(i)
)根據
colorScheme
選擇不同的顏色計算方式將計算好的顏色值設置到頂點顏色屬性中(
colors.setXYZ
)
2. 核心變量
count
: 幾何體的頂點總數
positions
: 包含所有頂點位置數據的BufferAttribute
colors
: 用于存儲頂點顏色數據的BufferAttribute
radius
: 幾何體的半徑(用于標準化Y坐標)
color
: THREE.Color對象,用于臨時存儲計算的顏色值
3. 顏色方案解析
方案1 (case 1): 基于Y軸高度的HSL顏色?
color.setHSL((y / radius + 1) / 2, 1.0, 0.5, THREE.SRGBColorSpace)
?
color.setHSL((y / radius + 1) / 2, 1.0, 0.5, THREE.SRGBColorSpace)
?能實現彩虹漸變效果,核心在于?HSL色彩模型的特性和Y坐標的映射關系。以下是逐層解析:
1.?HSL色彩模型基礎
HSL代表:
H (Hue):色相(0~1循環,紅→黃→綠→青→藍→紫→紅)
S (Saturation):飽和度(0=灰色,1=純色)
L (Lightness):亮度(0=黑,0.5=純色,1=白)
代碼中固定了:
飽和度 S=1.0(最鮮艷)
亮度 L=0.5(不偏白不偏黑)
2.?關鍵公式:
(y / radius + 1) / 2
分步解析:
y / radius
將頂點Y坐標歸一化到?
[-1, 1]
?范圍(假設幾何體中心在原點)底部:
y ≈ -radius
?→ 值接近?-1
頂部:
y ≈ radius
?→ 值接近?1
+ 1
將范圍平移為?
[0, 2]
底部:
-1 + 1 = 0
頂部:
1 + 1 = 2
/ 2
最終映射到?
[0, 1]
?的標準HSL色相范圍底部:
0 / 2 = 0
(紅色)中部:
1 / 2 = 0.5
(青色)頂部:
2 / 2 = 1
(循環回紅色)
3.?彩虹漸變的形成
通過Y坐標與色相H的線性映射:
底部 (y=-radius)?→ H=0 →?紅色
中部偏下 (y≈-0.5radius)?→ H≈0.25 →?黃色
中部 (y=0)?→ H=0.5 →?青色
中部偏上 (y≈0.5radius)?→ H≈0.75 →?藍色
頂部 (y=radius)?→ H=1 →?紅色(循環)
由于色相H在HSL模型中是一個環形光譜(紅→黃→綠→青→藍→紫→紅),這種映射自然形成了連續的彩虹色漸變。
4.?為什么不是從紫色到紅色?
雖然H=1理論上會回到紅色,但在實際渲染中:
頂部頂點通常不會恰好達到H=1(因浮點精度或幾何體細分程度)
人眼對藍-紫色的變化更敏感,視覺上會感覺漸變完整
方案2 (case 2):?基于Y軸高度的HSL顏色,固定色相
color.setHSL(0, (y / radius + 1) / 2, 0.5, THREE.SRGBColorSpace)
固定色相H=0(紅),僅調整飽和度 → 紅-灰漸變?
?方案3 (case 3): 基于Y軸高度的RGB漸變
color.setRGB(1, 0.8 - (y / radius + 1) / 2, 0, THREE.SRGBColorSpace)
?
color.setRGB(1, 0.8 - (y / radius + 1) / 2, 0, THREE.SRGBColorSpace)
?能實現?從黃色到紅色的漸變,核心在于?RGB通道的數學關系和Y坐標的映射。以下是逐層解析:
1.?RGB顏色模型基礎
R (Red):紅色分量(0~1)
G (Green):綠色分量(0~1)
B (Blue):藍色分量(0~1)
組合效果:
(1, 1, 0)
?= 黃色(紅+綠)
(1, 0, 0)
?= 純紅色
(1, 0.5, 0)
?= 橙紅色
2.?關鍵公式解析:
0.8 - (y / radius + 1) / 2
分步計算綠色分量(G):
y / radius
將Y坐標歸一化到?
[-1, 1]
(幾何體中心在原點時)底部:
y ≈ -radius
?→ 值接近?-1
頂部:
y ≈ radius
?→ 值接近?1
+ 1
平移范圍到?
[0, 2]
底部:
-1 + 1 = 0
頂部:
1 + 1 = 2
/ 2
壓縮到?
[0, 1]
底部:
0 / 2 = 0
頂部:
2 / 2 = 1
0.8 - ...
反轉并偏移計算結果
底部:
0.8 - 0 = 0.8
頂部:
0.8 - 1 = -0.2
(實際會被限制為0)
3.?顏色漸變過程
Y坐標位置 綠色分量 (G) 計算 RGB值 顏色表現 底部?(y=-radius) 0.8 - (0)/2 = 0.8
(1, 0.8, 0)
亮黃色 中部偏下 0.8 - (0.5)/2 = 0.55
(1, 0.55, 0)
橙黃色 中部?(y=0) 0.8 - (1)/2 = 0.3
(1, 0.3, 0)
橙紅色 頂部?(y=radius) 0.8 - (2)/2 = -0.2 → 0
(1, 0, 0)
純紅色
4.?為什么是黃→紅?
底部黃色:
R=1(最大紅) + G=0.8(高綠) + B=0 →?接近純黃
過渡階段:
綠色分量從0.8線性減少 → 顏色逐漸偏向紅色
頂部紅色:
G被限制為0 → 僅剩R=1 →?純紅
5.?設計巧思
固定R=1:保持紅色主導,避免顏色跳躍
G的遞減公式:通過Y坐標控制綠色衰減速度
B=0:完全禁用藍色通道,確保暖色漸變
0.8的偏移量:避免底部顏色過暗(若用
1 - (y/radius+1)/2
,底部G=1,頂部G=0,效果類似)
對比其他方案
HSL方案:通過色相H變化實現彩虹漸變
此RGB方案:通過固定R、衰減G,實現暖色系線性過渡,更適合需要單一色調漸變的場景(如溫度可視化、危險等級提示等)。
這種設計以極簡的數學映射,實現了符合直覺的顏色過渡效果。
4. 數學關系可視化
對于Y坐標從-bottom到top的變化:
方案 | 底部顏色(y=-radius) | 頂部顏色(y=radius) | 漸變方向 |
---|---|---|---|
1 | 色相=0(紅) | 色相=1(回到紅) | 彩虹色 |
2 | 飽和度=0(灰) | 飽和度=1(純紅) | 紅漸變 |
3 | RGB(1,0.8,0)黃 | RGB(1,0,0)紅 | 黃到紅 |
5. 實際應用
這種技術常用于:
可視化高度數據
創建彩色3D地形
調試3D模型(查看頂點分布)
藝術化渲染效果
渲染與動畫
return (<group ref={meshRef} position={position}><mesh geometry={geometry}><meshPhongMaterialcolor={0xffffff}flatShadingvertexColorsshininess={0}/></mesh><mesh geometry={geometry}><meshBasicMaterialcolor={0x000000}wireframetransparentopacity={0.3}/></mesh></group>
)
我們使用兩個網格疊加的方式實現最終效果:
第一個使用
meshPhongMaterial
并啟用vertexColors
,顯示彩色表面第二個使用
meshBasicMaterial
的線框模式,添加輪廓增強立體感無邊框的效果
有邊框的效果
動畫通過useFrame
鉤子實現簡單旋轉:
useFrame(() => {if (meshRef.current) {meshRef.current.rotation.y += 0.005}
})
改變顏色插值取值
上面我們使用了模型的坐標Y值進行了從下到上的顏色插值。本質上是根據頂點的某個信息的值在一個區間內映射得到的值進行顏色插值。
根據坐標的X值在[-radius,radius]進行插值
for (let i = 0; i < count; i++) {const y = positions.getX(i)switch (colorScheme) {case 1: // 基于Y軸高度的HSL顏色color.setHSL((y / radius + 1) / 2, 1.0, 0.5, THREE.SRGBColorSpace)breakcase 2: // 基于Y軸高度的HSL顏色,固定色相color.setHSL(0, (y / radius + 1) / 2, 0.5, THREE.SRGBColorSpace)breakcase 3: // 基于Y軸高度的RGB漸變color.setRGB(1, 0.8 - (y / radius + 1) / 2, 0, THREE.SRGBColorSpace)break}colors.setXYZ(i, color.r, color.g, color.b)}
根據坐標的z值進行插值
for (let i = 0; i < count; i++) {const y = positions.getZ(i)switch (colorScheme) {case 1: // 基于Y軸高度的HSL顏色color.setHSL((y / radius + 1) / 2, 1.0, 0.5, THREE.SRGBColorSpace)breakcase 2: // 基于Y軸高度的HSL顏色,固定色相color.setHSL(0, (y / radius + 1) / 2, 0.5, THREE.SRGBColorSpace)breakcase 3: // 基于Y軸高度的RGB漸變color.setRGB(1, 0.8 - (y / radius + 1) / 2, 0, THREE.SRGBColorSpace)break}colors.setXYZ(i, color.r, color.g, color.b)}
由于threejs是默認z軸是從里到外的,可以看到屏幕最近的顏色是顏色插值的最大值?
如何確定某個頂點的顏色?
?首先確定要映射的顏色區間。然后要考慮用頂點的什么屬性值參與映射,比如像CAE仿真中后處理結果中模擬溫度的顏色,就取頂點對應的溫度值在溫度的上下限中的[0,1]標準化區間的值,在結合上面所的比如彩虹映射得到該頂點的顏色值
性能優勢
頂點顏色技術的主要性能優勢在于:
減少繪制調用:所有顏色信息已經包含在頂點數據中,無需額外紋理或uniform
GPU友好:顏色計算在初始化時完成,渲染時直接使用預計算數據
內存高效:相比紋理貼圖,頂點顏色占用內存更少
對于需要渲染大量相似對象的場景(如科學可視化、地圖標記等),這種技術可以顯著提升性能。
適用場景
頂點顏色技術特別適合以下場景:
數據可視化(熱力圖、高度圖等)
簡單的顏色漸變效果
需要高性能的移動端3D應用
快速原型開發,避免復雜著色器編寫
總結
通過這個彩色二十面體的實現,我們展示了頂點顏色技術在3D可視化中的強大能力。相比Shader方案,這種方法:
更易于理解和實現
調試和維護更簡單
性能表現優異
足夠滿足許多常見可視化需求
當你的項目不需要Shader提供的極端靈活性時,考慮使用頂點顏色技術可能會帶來更好的開發體驗和性能表現。Three.js和React Three Fiber提供的豐富API使得這種實現方式既簡單又強大,是前端開發者進入3D可視化世界的理想起點。
下次當你面臨3D可視化需求時,不妨先問問自己:我真的需要Shader嗎?也許頂點顏色就能完美解決問題!