本项目通过 Three.js 实现了建筑群的三维可视化,支持真实阴影、动态日照、丰富的自定义着色效果。本文将深入剖析:建筑物的创建与几何处理、物理光照与阴影系统、着色器效果的数学原理与实现算法。
一、建筑物的创建与几何处理
1. 数据解析与中心化处理
建筑数据采用 GeoJSON 格式,包含多边形(Polygon/MultiPolygon)坐标集。首先需要计算整个建筑群的几何中心,作为坐标系的原点:
function getBuildingsCenter(features) {
let sumX = 0, sumY = 0, count = 0;
features.forEach(feature => {
const geometry = feature.geometry;
const coordinates = geometry.type === 'Polygon' ?
[geometry.coordinates] : geometry.coordinates;
coordinates.forEach(polygon => {
polygon.forEach(ring => {
ring.forEach(point => {
sumX += point[0];
sumY += point[1];
count++;
});
});
});
});
return [sumX / count, sumY / count]; // 返回平均中心点
}
这一步的几何意义在于:通过计算所有顶点的算术平均位置,获得建筑群的质心(centroid)。这种中心化处理确保了场景渲染时建筑群位于视口中央,并为后续的坐标变换提供了参考原点。
2. 经纬度转平面坐标的投影算法
经纬度坐标系(球面坐标)不适合直接用于三维建模,需要转换为平面坐标系:
function lnglat2Map(lnglat, center) {
// 墨卡托投影的简化实现
const R = 6378137; // 地球半径(m)
const x = R * (lnglat[0] - center[0]) * Math.PI / 180;
const y = R * Math.log(Math.tan((90 + lnglat[1]) * Math.PI / 360)) -
R * Math.log(Math.tan((90 + center[1]) * Math.PI / 360));
return new THREE.Vector2(x, y);
}
数学原理解析:
- 这是一种简化的墨卡托投影(Mercator Projection)实现
R * (lnglat[0] - center[0]) * Math.PI / 180
将经度差转换为弧长(米)R * Math.log(Math.tan((90 + lat) * Math.PI / 360))
是纬度的墨卡托映射函数- 投影后的坐标单位为米,便于构建真实比例的三维模型
3. 多边形拉伸为三维建筑的几何算法
每个建筑基于其平面轮廓和高度信息构建:
function createBuildingGeometry(points, height) {
// 1. 创建二维轮廓
const shape = new THREE.Shape(points);
// 2. 处理复杂多边形中的内部孔洞
if (holes && holes.length > 0) {
holes.forEach(hole => {
const holeShape = new THREE.Path(hole);
shape.holes.push(holeShape);
});
}
// 3. 拉伸为三维几何体
const extrudeSettings = {
depth: height, // 拉伸高度
bevelEnabled: false, // 禁用倒角
steps: 1 // 拉伸步数
};
return new THREE.ExtrudeGeometry(shape, extrudeSettings);
}
几何算法分析:
THREE.Shape
创建平面多边形,支持任意复杂形状(凹多边形、带孔多边形)THREE.ExtrudeGeometry
实现了多边形拉伸算法,其核心是:- 复制底面轮廓到目标高度作为顶面
- 连接底面和顶面轮廓对应顶点形成侧面
- 对底面和顶面进行三角剖分(Triangulation)
- 三角剖分采用的是 Earcut 算法,能高效处理复杂多边形
性能优化技术:
// 合并所有建筑几何体以减少绘制调用
const mergedGeometry = BufferGeometryUtils.mergeBufferGeometries(
buildingGeometries, false
);
const buildingsMesh = new THREE.Mesh(mergedGeometry, material);
通过几何体合并,将多个建筑合并为单个 Mesh,显著减少 draw call,提升渲染性能。
二、物理光照系统与阴影算法
1. 方向光(太阳)与级联阴影贴图
// 创建模拟太阳的方向光
const directionalLight = new THREE.DirectionalLight(0xfff8e1, 1.2);
directionalLight.position.set(600, 1200, 800);
directionalLight.castShadow = true;
// 配置阴影贴图
directionalLight.shadow.mapSize.width = 2048; // 阴影贴图分辨率
directionalLight.shadow.mapSize.height = 2048;
directionalLight.shadow.camera.near = 100; // 阴影相机近平面
directionalLight.shadow.camera.far = 3000; // 阴影相机远平面
directionalLight.shadow.camera.left = -1000; // 阴影相机视锥体
directionalLight.shadow.camera.right = 1000;
directionalLight.shadow.camera.top = 1000;
directionalLight.shadow.camera.bottom = -1000;
directionalLight.shadow.bias = -0.0005; // 阴影偏移值,减少阴影失真
// 使用PCF软阴影
directionalLight.shadow.type = THREE.PCFSoftShadowMap;
阴影渲染原理:
- 深度贴图生成:从光源视角渲染场景,记录每个像素的深度值
- 阴影测试:渲染主相机视图时,将每个片元变换到光源空间,比较深度值
- PCF柔化算法:采样多个临近点的深度值并平均,创造柔和阴影边缘
阴影参数调优:
shadow.mapSize
增大提高阴影精度但降低性能shadow.camera
参数应尽量紧凑包围场景,提高阴影贴图的有效分辨率shadow.bias
用于解决阴影失真(shadow acne)问题
2. 动态日照物理模拟
const sunParams = {
angle: 60, // 太阳高度角
azimuth: 45, // 太阳方位角
intensity: 1.2, // 光照强度
color: 0xfff8e1 // 光照色温
};
gui.add(sunParams, 'angle', 0, 90).name('太阳高度角').onChange(updateSunPosition);
gui.add(sunParams, 'azimuth', 0, 360).name('太阳方位角').onChange(updateSunPosition);
function updateSunPosition() {
// 球坐标系转笛卡尔坐标系
const radAzimuth = sunParams.azimuth * Math.PI / 180;
const radAngle = sunParams.angle * Math.PI / 180;
const sunRadius = 1500; // 太阳距离场景中心的半径
// 在球坐标系中,太阳位置由高度角和方位角共同决定
directionalLight.position.x = sunRadius * Math.cos(radAngle) * Math.cos(radAzimuth);
directionalLight.position.y = sunRadius * Math.sin(radAngle);
directionalLight.position.z = sunRadius * Math.cos(radAngle) * Math.sin(radAzimuth);
// 更新阴影相机
directionalLight.shadow.camera.updateProjectionMatrix();
}
数学原理:
- 球坐标系转换:使用高度角(angle)和方位角(azimuth)确定太阳位置
- 高度角:太阳与地平面的夹角,控制光照强度和阴影长度
- 方位角:太阳在水平面投影与正北方向的夹角,定义阴影方向
- 转换公式遵循右手坐标系规则:
- x = r · cos(θ) · cos(φ)
- y = r · sin(θ)
- z = r · cos(θ) · sin(φ)
- 其中 θ 是高度角,φ 是方位角
三、着色器效果的数学原理与视觉算法
着色器(Shader)是现代图形学的核心,通过在 GPU 上并行执行的程序直接控制渲染管线。本项目的自定义着色器实现了多种视觉效果,下面深入解析其实现原理。
1. 自定义 ShaderMaterial 的结构设计
const buildingMaterial = new THREE.ShaderMaterial({
uniforms: {
// 基础属性
diffuse: { value: new THREE.Color(0x4477aa) },
opacity: { value: 1.0 },
// 贴图与混合模式
colorMap: { value: textureLoader.load('facade.jpg') },
colorMapMode: { value: 2 }, // 0:纯色 1:纯贴图 2:混合 3:自定义混合
// 光照相关
topLightColor: { value: new THREE.Color(0xffffaa) },
topLightFading: { value: 2.5 },
useTopLight: { value: true },
bottomLightColor: { value: new THREE.Color(0x4466aa) },
bottomLightFading: { value: 1.8 },
useBottomLight: { value: true },
// 动态效果
time: { value: 0 },
dynamicLightColor: { value: new THREE.Color(0x44aaff) },
dynamicLineWidth: { value: 0.05 },
dynamicSpeed: { value: 1.0 },
useDynamicLight: { value: true },
},
vertexShader: buildingVertexShader,
fragmentShader: buildingFragmentShader,
lights: true, // 启用 Three.js 内置光照
transparent: true,
side: THREE.DoubleSide
});
Shader 结构设计原理:
- Uniforms:CPU 向 GPU 传递的全局参数,包括:
- 材质基础属性(颜色、透明度)
- 特效控制参数(光照色彩、动画速度)
- 全局状态(时间、贴图)
- 顶点着色器:处理几何变换和传递顶点属性
- 片元着色器:计算每个像素的最终颜色
2. 顶点着色器的关键实现
// 顶点着色器
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vViewPosition;
varying float fHeight; // 归一化的高度值
varying float bHigher; // 用于识别不同建筑的标记
void main() {
// 计算归一化高度(基于 Y 坐标)
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
vec3 transformedNormal = normalMatrix * normal;
// 找出建筑物的最高点和最低点
float yMax = 100.0; // 假设建筑最高 100 米
float yMin = 0.0; // 地面高度
// 计算归一化高度 (0.0 - 1.0)
fHeight = (position.y - yMin) / (yMax - yMin);
fHeight = clamp(fHeight, 0.0, 1.0);
// 标记高于特定高度的建筑(用于特效)
bHigher = position.y > 25.0 ? 1.0 : 0.0;
// 传递坐标、法线、纹理坐标给片元着色器
vUv = uv;
vNormal = normalize(transformedNormal);
vViewPosition = -mvPosition.xyz;
gl_Position = projectionMatrix * mvPosition;
}
关键算法解析:
- 高度归一化:将建筑物实际高度映射到 0.0-1.0 范围,便于后续基于高度的着色计算
- 法线变换:通过
normalMatrix
将物体空间的法线变换到视图空间,保证光照计算正确性 - 视图位置计算:记录片元在视图空间的位置,用于视角相关效果
3. 片元着色器的数学原理与视觉算法
片元着色器(Fragment Shader)决定了最终的视觉效果,下面详细剖析关键算法及其图形学原理:
// 片元着色器(简化关键部分)
uniform vec3 diffuse;
uniform sampler2D colorMap;
uniform int colorMapMode;
uniform float opacity;
// 光效参数
uniform vec3 topLightColor;
uniform float topLightFading;
uniform bool useTopLight;
uniform vec3 bottomLightColor;
uniform float bottomLightFading;
uniform bool useBottomLight;
uniform vec3 dynamicLightColor;
uniform float dynamicLineWidth;
uniform float dynamicSpeed;
uniform bool useDynamicLight;
uniform float time;
// 从顶点着色器传入的变量
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vViewPosition;
varying float fHeight;
varying float bHigher;
// Three.js 光照相关
#include <common>
#include <lights_pars_begin>
void main() {
// 1. 基础颜色/贴图处理
vec4 diffuseColor = vec4(diffuse, opacity);
vec4 texelColor = texture2D(colorMap, vUv);
// 颜色混合模式
if (colorMapMode == 1) {
diffuseColor = texelColor;
} else if (colorMapMode == 2) {
diffuseColor = vec4(texelColor.rgb, diffuseColor.a);
} else if (colorMapMode == 3) {
// 自定义混合比例
diffuseColor = vec4(diffuseColor.rgb * 0.45 + texelColor.rgb * 0.55, diffuseColor.a);
}
// 2. 光照计算准备
vec3 normal = normalize(vNormal);
vec3 viewDir = normalize(vViewPosition);
float fpos = fHeight;
// 3. 自发光计算(泛光和特效)
vec3 totalEmissiveRadiance = vec3(0.0);
// 顶面高光算法
if (useTopLight) {
float fbase = 1.0 - fpos;
if (fbase < 0.0) fbase = abs(fbase);
// 幂函数控制衰减曲线
float intensity = pow(fbase, topLightFading * 5.0);
totalEmissiveRadiance += topLightColor * intensity;
}
// 底部泛光算法
if (useBottomLight) {
float fbase = fpos;
if (fbase < 0.0) fbase = abs(fbase);
// 幂函数控制衰减曲线
float intensity = pow(fbase, bottomLightFading * 5.0);
totalEmissiveRadiance += bottomLightColor * intensity;
}
// 动态光带算法
if (useDynamicLight && bHigher > 0.0) {
// 动态光带位置(时间驱动的周期函数)
float bandPosition = abs(fract(time * 0.00035 * dynamicSpeed) - 0.5) * 2.0;
// 计算片元到光带中心的距离
float distToBand = abs(fHeight - bandPosition);
// 平滑过渡的光带强度计算
float factor = 0.0;
if (distToBand < dynamicLineWidth && fHeight <= 0.999) {
factor = 1.0 - distToBand / dynamicLineWidth; // 线性衰减
// 可选:使用平方函数实现更自然的过渡
// factor = 1.0 - (distToBand * distToBand) / (dynamicLineWidth * dynamicLineWidth);
}
totalEmissiveRadiance += dynamicLightColor * factor;
}
// 4. Three.js 标准光照模型计算
ReflectedLight reflectedLight = ReflectedLight(vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0));
#include <lights_phong_fragment>
#include <lights_fragment_begin>
#include <lights_fragment_maps>
#include <lights_fragment_end>
// 5. 最终颜色合成
vec3 outgoingLight = reflectedLight.directDiffuse +
reflectedLight.indirectDiffuse +
totalEmissiveRadiance;
gl_FragColor = vec4(outgoingLight, diffuseColor.a);
}
3.1 顶面高光算法(屋顶泛光)的数学原理
if (useTopLight) {
float fbase = 1.0 - fpos;
if (fbase < 0.0) fbase = abs(fbase);
float intensity = pow(fbase, topLightFading * 5.0);
totalEmissiveRadiance += topLightColor * intensity;
}
深度解析:
- 反转高度值:
fbase = 1.0 - fpos
将归一化高度反转,使得顶部为 0,底部为 1 - 幂函数效应:
pow(fbase, n)
是图形学中常用的衰减函数- 数学本质:y = x^n(n > 1 时)在 x 接近 0 时,y 迅速趋近于 0
- 视觉效果:形成从顶部向下快速衰减的平滑过渡
- 控制参数:n 值越大,衰减越陡峭,高光区域越集中
以 topLightFading = 2.5
为例,实际幂指数为 12.5:
- 在 fpos = 0.95(接近顶部)时:intensity = pow(0.05, 12.5) ≈ 0.0000000001
- 在 fpos = 0.99(非常接近顶部)时:intensity = pow(0.01, 12.5) ≈ 0.0000000000001
这种极度非线性的衰减创造了真实的"建筑物顶部受光"效果,模拟了现实世界中高层建筑顶部通常受光更强的现象。
3.2 底部泛光算法的数学原理
if (useBottomLight) {
float fbase = fpos;
if (fbase < 0.0) fbase = abs(fbase);
float intensity = pow(fbase, bottomLightFading * 5.0);
totalEmissiveRadiance += bottomLightColor * intensity;
}
深度解析:
- 直接使用高度值:
fbase = fpos
保持原始高度映射(底部为 0,顶部为 1) - 幂函数特性:
- 当 x 接近 0 时(接近底部),pow(x, n) 快速趋近于 0
- 这创造了从底部向上逐渐消失的光效
物理现象模拟:
- 这种效果模拟了城市环境中常见的"底部照明"现象
- 实际城市中,建筑物底部通常由于街灯、商店灯光等产生额外照明
- 通过调整
bottomLightFading
参数,可以控制这种效果的衰减速率
3.3 动态光带算法的图形学原理
if (useDynamicLight && bHigher > 0.0) {
// 动态位置计算
float bandPosition = abs(fract(time * 0.00035 * dynamicSpeed) - 0.5) * 2.0;
// 距离计算与平滑过渡
float distToBand = abs(fHeight - bandPosition);
float factor = 0.0;
if (distToBand < dynamicLineWidth && fHeight <= 0.999) {
factor = 1.0 - distToBand / dynamicLineWidth;
}
totalEmissiveRadiance += dynamicLightColor * factor;
}
核心算法解析:
-
周期性移动函数:
float bandPosition = abs(fract(time * 0.00035 * dynamicSpeed) - 0.5) * 2.0;
fract(x)
获取 x 的小数部分,创造循环效果time * 0.00035 * dynamicSpeed
控制光带移动速度abs(fract(...) - 0.5) * 2.0
将范围调整为 0.0 到 1.0 的来回运动
数学原理:
- 假设 t = time * 0.00035 * dynamicSpeed
- fract(t) 在 [0,1] 之间循环
- fract(t) - 0.5 在 [-0.5,0.5] 之间循环
- abs(fract(t) - 0.5) 在 [0,0.5] 之间来回运动
- abs(fract(t) - 0.5) * 2.0 在 [0,1] 之间来回运动
-
平滑过渡光带:
float distToBand = abs(fHeight - bandPosition); if (distToBand < dynamicLineWidth) { factor = 1.0 - distToBand / dynamicLineWidth; }
实现原理:
- 计算当前片元高度与光带中心的距离
- 仅当距离小于预设宽度时激活光效
- 线性插值
1.0 - distToBand / dynamicLineWidth
创造平滑渐变边缘
光带内部亮度分布:
- 中心点(distToBand = 0):factor = 1.0(最亮)
- 边缘点(distToBand = dynamicLineWidth):factor = 0.0(无光效)
- 中间区域:亮度线性过渡
-
增强版平滑函数(可选优化):
// 替换线性插值为平方函数,获得更自然的过渡 factor = 1.0 - (distToBand * distToBand) / (dynamicLineWidth * dynamicLineWidth);
- 平方函数创造更平滑的光带边缘过渡
- 视觉上更接近真实光线扩散效果
3.4 颜色混合模式的图形学原理
// 颜色混合模式
if (colorMapMode == 1) {
diffuseColor = texelColor; // 纯贴图
} else if (colorMapMode == 2) {
diffuseColor = vec4(texelColor.rgb, diffuseColor.a); // 保留原透明度
} else if (colorMapMode == 3) {
// 线性插值混合
diffuseColor = vec4(diffuseColor.rgb * 0.45 + texelColor.rgb * 0.55, diffuseColor.a);
}
颜色混合原理:
- 线性插值:
color1 * weight1 + color2 * weight2
是计算机图形学中最基本的颜色混合算法 - 权重总和为 1:确保混合后的颜色仍在有效范围内(0.45 + 0.55 = 1.0)
- 保留 Alpha 通道:混合仅影响 RGB 颜色通道,保留原始透明度
这种混合模式实现了类似"覆盖混合"(Overlay Blend)的效果,让材质与贴图自然融合,既保留了贴图的细节,又调整了整体色调。
4. 完整的光照模型与阴影集成
// 计算 Three.js 内置光照模型
ReflectedLight reflectedLight = ReflectedLight(vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0));
#include <lights_phong_fragment>
#include <lights_fragment_begin>
#include <lights_fragment_maps>
#include <lights_fragment_end>
// 最终颜色合成
vec3 outgoingLight = reflectedLight.directDiffuse +
reflectedLight.indirectDiffuse +
totalEmissiveRadiance;
gl_FragColor = vec4(outgoingLight, diffuseColor.a);
光照计算原理:
- Phong 光照模型:结合环境光、漫反射和镜面反射三种光照
- 环境光(Ambient):模拟间接光照,为阴影区域提供基础照明
- 漫反射(Diffuse):基于法线与光线夹角计算直接照明强度
- 镜面反射(Specular):基于视角、法线和光线计算高光
- 自发光(Emissive)集成:
- 自发光不受外部光源影响,直接添加到最终颜色
totalEmissiveRadiance
包含了所有自定义光效(顶部高光、底部泛光、动态光带)
- 阴影集成:
lights_fragment_maps
包含了阴影贴图采样和软阴影计算- 阴影信息自动影响
reflectedLight.directDiffuse
分量 - 自发光不受阴影影响,保证特效在阴影区域仍然可见
四、性能优化与拓展技术
1. 渲染性能优化技术
- 几何体合并:使用
BufferGeometryUtils.mergeBufferGeometries()
减少绘制调用 - 自适应阴影贴图:根据视距动态调整阴影贴图分辨率
- 视锥体剔除:仅渲染视野内的建筑,减轻 GPU 负担
- Level of Detail (LOD):根据距离使用不同精度的模型
2. 着色器优化技术
- 分支优化:减少或重组条件分支,避免 GPU 内的分支预测失败
- 精度控制:适当使用 lowp/mediump 精度限定符,提升性能
- 数学函数优化:用多项式近似替代复杂函数,如用 Horner 方法优化幂函数
- 纹理查找表(LUT):将复杂函数预计算到纹理中,通过采样获取结果
3. 可视化增强技术
- 环境光遮蔽(AO):增强建筑几何细节的空间感
// 在片元着色器中添加简化版AO效果
float calculateAO() {
// 基于高度和法线计算简化的环境光遮蔽
float aoFactor = 1.0;
// 凹角处增强遮蔽
float concavity = max(0.0, 1.0 - dot(normal, vec3(0.0, 1.0, 0.0)));
aoFactor *= 1.0 - concavity * 0.5;
// 接近地面处增强遮蔽
float groundProximity = 1.0 - smoothstep(0.0, 0.15, fHeight);
aoFactor *= 1.0 - groundProximity * 0.4;
return aoFactor;
}
// 在最终着色时应用
reflectedLight.directDiffuse *= calculateAO();
reflectedLight.indirectDiffuse *= calculateAO();
- 边缘增强:突出建筑轮廓,增强视觉清晰度
// 边缘检测算法
float calculateEdgeFactor() {
// 基于法线与视线夹角计算边缘因子
float NdotV = max(0.0, dot(normal, viewDir));
float edgeFactor = 1.0 - pow(NdotV, edgeSharpness);
return edgeFactor;
}
// 在最终颜色合成时应用
vec3 edgeColor = vec3(0.0, 0.0, 0.0); // 边缘颜色
float edgeStrength = 0.2; // 边缘强度
outgoingLight = mix(outgoingLight, edgeColor, calculateEdgeFactor() * edgeStrength);
五、数学函数与图形学原理深度解析
为了更深入理解本项目的shader实现,这一节将详细阐述关键数学函数与图形学原理。
1. 非线性映射函数原理
着色器中大量使用的幂函数(如 pow(x, n)
)具有显著的非线性特性,这对于视觉效果至关重要:
不同 n 值的幂函数曲线比较:
- n = 1.0:线性映射,亮度均匀过渡
- n = 2.0:轻度非线性,中间区域过渡自然
- n = 5.0:中度非线性,边缘过渡锐化
- n = 12.5:强非线性,创造尖锐的边缘过渡
数学公式与视觉映射关系:
当我们使用 intensity = pow(fbase, n)
计算光照强度时,实际是应用了如下映射:
- 对于 n > 1(如本项目的
topLightFading * 5.0
):- 接近 1 的输入值仍然接近 1
- 中间值被"压缩"向 0
- 接近 0 的值更接近 0
- 这种非线性映射在视觉上的意义:
- 创造更锐利的过渡边缘
- 减少"灰色"中间区域
- 增强对比度和清晰度
实际应用案例:顶部高光效果中,使用 pow(1.0 - fpos, 12.5)
确保只有最接近顶部的区域有明显亮度,创造聚焦、精确的光效。
2. 周期函数与动画原理
动态光带效果使用的周期函数是图形学动画的经典实现:
float bandPosition = abs(fract(time * 0.00035 * dynamicSpeed) - 0.5) * 2.0;
这个表达式可分解为几个关键步骤:
-
线性时间函数:
t = time * 0.00035 * dynamicSpeed
- 随时间线性增长,速率由
dynamicSpeed
控制
- 随时间线性增长,速率由
-
循环映射:
fract(t)
- 仅保留 t 的小数部分,创造 0→1 循环
- 数学上等价于
t - floor(t)
或mod(t, 1.0)
-
双向往复:
abs(fract(t) - 0.5) * 2.0
fract(t) - 0.5
将范围移到 [-0.5, 0.5]abs()
函数创造对称的 [0, 0.5] 范围* 2.0
缩放到 [0, 1] 完整范围
这种复合函数创造了平滑的双向往复运动,避免了简单循环可能带来的突变。其数学图形如下:
从图中可以看出,该函数创造了平滑的三角波,比正弦波计算效率更高,且没有突变点。
3. 空间变换与坐标系统
在着色器管线中,多个坐标系统转换是理解整个渲染过程的关键:
-
模型空间 → 世界空间:
- 变换矩阵:
modelMatrix
- 作用:将建筑从其局部坐标系放置到世界中
- 变换矩阵:
-
世界空间 → 相机空间:
- 变换矩阵:
viewMatrix
- 作用:从观察者视角看世界
- 变换矩阵:
-
相机空间 → 裁剪空间:
- 变换矩阵:
projectionMatrix
- 作用:透视投影,近大远小
- 变换矩阵:
-
模型-视图组合变换:
- 变换矩阵:
modelViewMatrix = viewMatrix * modelMatrix
- 顶点着色器中的应用:
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
- 效率优势:预乘矩阵减少GPU计算
- 变换矩阵:
-
法线变换特殊性:
- 变换矩阵:
normalMatrix
(模型-视图矩阵的逆转置) - 特殊处理原因:保持法线垂直性和正确的光照计算
- 数学公式:
normalMatrix = transpose(inverse(mat3(modelViewMatrix)))
- 变换矩阵:
图形学原理:法线不能直接用模型-视图矩阵变换,因为非均匀缩放会破坏垂直关系。使用逆转置矩阵是图形学中的标准解决方案。
4. 光照模型的物理基础
本项目综合了多种光照计算模型,结合了传统光照模型与现代物理模拟:
-
Phong 光照模型公式: I = kaIa + kdId*(N·L) + ksIs(R·V)^n 其中:
- I:最终颜色强度
- ka, kd, ks:环境光、漫反射、镜面反射系数
- Ia, Id, Is:环境光、漫反射、镜面反射光源强度
- N:表面法线、L:指向光源的单位向量
- R:反射向量、V:视线向量
- n:高光系数(值越大,高光越集中)
-
基于物理的光照(PBR)元素:
- 能量守恒原则:反射光能量不超过入射光
- 微表面理论:表面由微小镜面组成,粗糙度决定反射散射程度
- 菲涅尔效应:掠射角度反射率增加
实现示例:增强版材质可以引入基于物理的渲染元素:
// PBR参数
uniform float roughness;
uniform float metalness;
uniform float fresnel;
// 微表面分布函数 (GGX)
float D_GGX(float NoH, float roughness) {
float alpha = roughness * roughness;
float alpha2 = alpha * alpha;
float denom = NoH * NoH * (alpha2 - 1.0) + 1.0;
return alpha2 / (PI * denom * denom);
}
// 几何遮蔽函数
float G_Smith(float NoV, float NoL, float roughness) {
float k = (roughness + 1.0) * (roughness + 1.0) / 8.0;
float G1V = NoV / (NoV * (1.0 - k) + k);
float G1L = NoL / (NoL * (1.0 - k) + k);
return G1V * G1L;
}
// 菲涅尔函数
vec3 F_Schlick(float cosTheta, vec3 F0) {
return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}
5. 优化着色器的数学技巧
高性能的着色器实现依赖于多种数学优化技巧:
-
避免条件分支 变换:
// 优化前 if (x < y) { result = a; } else { result = b; } // 优化后 result = mix(b, a, step(x, y));
原理:
step(edge, x)
当 x < edge 时返回 0,否则返回 1,避免GPU分支预测失败 -
快速近似函数
对于 pow、exp、log 等计算密集函数,可使用多项式近似:
// 优化前 float y = pow(x, 5.0); // 优化后 (Horner方法) float y = x * x * x * x * x; // 对于整数幂
对于非整数幂,可以利用指数和对数性质:
// 优化 pow(x, n) float fastPow(float x, float n) { return exp2(n * log2(x)); }
-
向量化计算
同时处理多个通道,利用GPU并行特性:
// 优化前 float r = sqrt(x.r); float g = sqrt(x.g); float b = sqrt(x.b); vec3 result = vec3(r, g, b); // 优化后 vec3 result = sqrt(x.rgb);
六、项目拓展与未来优化
1. 高级效果拓展方向
- 体积云与天空系统:实现基于物理的大气散射与云层
- 水体反射与折射:添加互动水面,支持实时反射与折射
- 场景包围盒反射:基于IBL (Image-Based Lighting) 的环境反射
- 屏幕空间反射 (SSR):增强玻璃建筑的真实感
2. 性能与质量平衡技术
- 动态细节级别 (LOD):根据距离自动调整建筑几何精度和着色器复杂度
- 时间分布式阴影:多帧渲染不同区域阴影,减轻单帧负担
- 视锥体自适应渲染:核心视野区域使用高质量渲染,边缘区域降级处理
3. 互动性增强
- 建筑选择与信息展示:点击建筑显示详细数据
- 日照动画:模拟全天日照变化及季节变化
- 城市生长模拟:基于元胞自动机(CA)的城市发展可视化
4. 相关行业应用拓展
- 城市规划与阴影分析:评估新建筑对周边日照影响
- 太阳能潜力评估:基于屋顶朝向与日照分析
- 热岛效应模拟:材质热吸收与反射特性可视化
- 微气候模拟:结合流体动力学模拟城市风场与温度分布
七、总结与核心技术要点
本项目通过 Three.js 和自定义着色器,实现了高性能、视觉丰富的建筑群可视化系统。核心技术要点包括:
-
数据处理与几何优化:
- 中心化处理确保场景布局合理
- 地理坐标投影算法保证空间关系正确
- 几何体合并提升渲染性能
-
物理光照与阴影系统:
- 基于物理的日照模拟
- 高质量阴影贴图及PCF软化算法
- 球坐标系控制太阳轨迹
-
着色器视觉效果及其数学原理:
- 非线性映射函数创造自然光效过渡
- 周期函数实现平滑动画
- 空间变换保证光照计算正确性
- 多层次光照计算增强深度感与真实感
这些技术的结合,不仅创造了视觉上引人入胜的效果,更为城市规划、建筑设计、环境评估等领域提供了强大的可视化工具。通过深入理解其中的数学原理与算法实现,我们能更好地把握计算机图形学在城市可视化领域的应用潜力。