材质系统是3D资产视觉表现的灵魂,它决定了物体如何与光线交互,如何呈现质感,以及如何传达情感与氛围。在游戏资产设计中,材质不仅要追求物理准确性,更要服务于艺术表达和游戏性需求。本章将深入探讨从基于物理的渲染(PBR)到风格化着色器的完整材质设计体系,重点关注如何将技术工具转化为艺术表现力。我们将学习如何突破PBR的限制进行艺术化创作,掌握程序化材质的参数控制技巧,理解次表面散射等高级光照模型,设计富有想象力的能量体材质,并开发独特的风格化着色器。通过本章学习,你将能够为各种游戏资产创建既真实又富有艺术感的材质系统。
基于物理的渲染(Physically Based Rendering, PBR)已成为现代游戏引擎的标准,但”物理正确”不应成为创意的枷锁。PBR的核心在于能量守恒原理:
\[E_{reflected} + E_{absorbed} = E_{incident}\]其中反射能量进一步分解为漫反射(Diffuse)和镜面反射(Specular):
\[E_{reflected} = E_{diffuse} + E_{specular}\]在实际应用中,我们使用BRDF(双向反射分布函数)来描述材质的光照响应:
\[L_o(\omega_o) = \int_{\Omega} f_r(\omega_i, \omega_o) L_i(\omega_i) (\omega_i \cdot n) d\omega_i\]艺术化突破点:
超现实金属度:虽然PBR规定金属度应为0或1,但在魔法武器设计中,我们可以使用0.3-0.7的中间值创造”半金属”效果,表现能量侵蚀或魔法附着的视觉。
roughness = base_roughness * (1 + sin(UV.x * frequency) * amplitude)
金属工作流创新:
metallic = lerp(0, 1, pow(gradient_mask, 2.2))
非金属工作流拓展:
// 湿润效果的程序化实现
wetness_mask = saturate(dot(world_normal, float3(0,1,0)) + noise);
roughness = lerp(dry_roughness, 0.0, wetness_mask);
albedo = lerp(dry_color, dry_color * 0.5, wetness_mask);
环境反射是PBR材质真实感的关键,但也是艺术化的重要手段。标准的环境BRDF积分:
\[L_{indirect} = \int_{\Omega} L_{IBL}(\omega_i) f_r(\omega_i, \omega_o) (\omega_i \cdot n) d\omega_i\]艺术化环境反射技术:
// 魔法材质的反射扭曲
float3 reflection_vector = reflect(-view_dir, normal);
float distortion = sin(time * 2.0 + world_pos.y * 3.0) * 0.1;
reflection_vector.xy += distortion;
float3 ibl_color = texCUBE(env_map, reflection_vector);
现代材质系统采用节点图(Node Graph)架构,提供直观的可视化编程环境。核心设计原则:
输入参数 → 数学变换 → 纹理调制 → 混合运算 → 输出通道
时间驱动参数:
// 呼吸效果
float breath = sin(time * breath_speed) * 0.5 + 0.5;
emissive_intensity = lerp(min_glow, max_glow, breath);
// 脉动效果
float pulse = pow(sin(time * pulse_speed), 8.0);
scale_factor = 1.0 + pulse * pulse_amplitude;
空间驱动参数:
// 基于世界位置的渐变
float height_gradient = saturate((world_pos.y - min_height) / (max_height - min_height));
albedo = lerp(bottom_color, top_color, height_gradient);
// 基于到中心点距离的效果
float distance = length(world_pos.xz - center.xz);
float radial_mask = 1.0 - saturate(distance / radius);
游戏状态驱动:
// 生命值影响的材质变化
float damage_level = 1.0 - (current_hp / max_hp);
roughness = lerp(pristine_roughness, damaged_roughness, damage_level);
albedo = lerp(healthy_color, wounded_color, damage_level);
// 等级/稀有度的视觉表现
float rarity_factor = item_level / max_level;
emissive_color = lerp(common_glow, legendary_glow, rarity_factor);
rim_light_intensity = rarity_factor * 2.0;
基础混合方法:
线性插值(Lerp): \(Result = A \times (1 - factor) + B \times factor\)
float height_a = tex2D(height_map_a, uv).r;
float height_b = tex2D(height_map_b, uv).r;
float blend = saturate((height_a - height_b + blend_contrast) / blend_contrast);
float3 blend_weights = abs(world_normal);
blend_weights = pow(blend_weights, blend_sharpness);
blend_weights /= dot(blend_weights, 1.0);
float4 tex_x = tex2D(texture, world_pos.yz) * blend_weights.x;
float4 tex_y = tex2D(texture, world_pos.xz) * blend_weights.y;
float4 tex_z = tex2D(texture, world_pos.xy) * blend_weights.z;
float4 result = tex_x + tex_y + tex_z;
高级混合技术:
// 基于曲率的磨损
float curvature = compute_curvature(normal_map);
float wear_mask = pow(abs(curvature), wear_power);
// 基于AO的污垢积累
float dirt_mask = 1.0 - pow(ambient_occlusion, dirt_power);
// 组合掩码
float final_mask = saturate(wear_mask + dirt_mask * dirt_weight);
次表面散射(Subsurface Scattering, SSS)描述光线进入半透明材质内部,经过多次散射后从其他位置射出的现象。其物理模型基于辐射传输方程:
\[L(x, \omega) = L_s(x, \omega) + \int_V \sigma_s(x') P(\omega' \to \omega) L(x', \omega') e^{-\sigma_t ||x-x'||} dx'\]其中:
实时近似方法:
扩散剖面(Diffusion Profile): 描述光线在材质内部的扩散范围,常用高斯分布近似: \(R(r) = \sum_{i=1}^{n} w_i \cdot \frac{1}{2\pi\sigma_i^2} e^{-r^2/2\sigma_i^2}\)
可分离次表面散射(Separable SSS): 将3D卷积分解为屏幕空间的两次1D卷积,大幅提升性能。
皮肤材质的多层模型:
// 三层皮肤模型
struct SkinLayers {
float3 epidermis; // 表皮层:黄色调
float3 dermis; // 真皮层:红色调(血液)
float3 hypodermis; // 皮下层:深层散射
};
// 不同波长的散射距离
float3 scatter_distance = float3(0.4, 0.15, 0.05); // R, G, B
float3 absorption = float3(0.02, 0.08, 0.16);
// 背光透射
float3 transmittance = exp(-thickness * absorption);
蜡质材质特征:
// 蜡烛材质
float3 wax_scatter = float3(2.0, 1.8, 1.5);
float3 wax_absorption = float3(0.001, 0.002, 0.003);
float translucency = 0.8;
玉石材质的层次感:
// 玉石的次表面实现
float3 jade_color = float3(0.3, 0.6, 0.4);
float3 scatter_color = jade_color * float3(1.2, 1.0, 0.8);
float depth_fade = exp(-thickness * 0.5);
float3 sss_result = scatter_color * depth_fade;
前向散射与后向散射:
float forward_scatter = pow(saturate(dot(view_dir, light_dir)), forward_power);
float back_scatter = pow(saturate(dot(view_dir, -light_dir)), back_power);
薄膜透射模型: 适用于树叶、纸张等薄片材质:
// 叶片透射
float3 light_behind = -light_dir;
float VdotL = dot(view_dir, light_behind);
float transmission = pow(saturate(VdotL), transmission_power);
float3 transmitted_light = transmission * light_color * leaf_color;
// 添加脉络细节
float veins = tex2D(vein_mask, uv).r;
transmitted_light *= lerp(1.0, vein_darkness, veins);
体积材质的光线步进:
// 简化的体积光线步进
float3 ray_march_volume(float3 start, float3 end, int steps) {
float3 accumulated = 0;
float3 step_vec = (end - start) / steps;
for(int i = 0; i < steps; i++) {
float3 pos = start + step_vec * i;
float density = sample_density(pos);
float3 scattered = density * scatter_coefficient;
accumulated += scattered * exp(-accumulated);
}
return accumulated;
}
自发光(Emissive)材质是创造科幻、魔法效果的核心技术。与传统光源不同,自发光材质可以有复杂的空间分布和时间变化。
基础发光模型:
// HDR发光强度
float3 emissive = base_color * emissive_intensity;
// 考虑曝光的最终输出
float3 final_color = albedo_lighting + emissive * exposure_multiplier;
动态发光图案:
// 沿UV坐标的能量流动
float energy_flow = frac(uv.y - time * flow_speed);
float pulse = smoothstep(0.0, pulse_width, energy_flow) *
smoothstep(pulse_width * 2.0, pulse_width, energy_flow);
emissive *= pulse * pulse_intensity;
// 基于纹理的选择性发光
float rune_mask = tex2D(rune_texture, uv).r;
float glow_pulse = sin(time * glow_frequency) * 0.5 + 0.5;
emissive = rune_color * rune_mask * (base_glow + glow_pulse * pulse_amount);
// Voronoi裂纹发光
float2 voronoi = voronoi_noise(uv * crack_scale);
float crack = smoothstep(crack_threshold - 0.05, crack_threshold, voronoi.y - voronoi.x);
emissive = crack_color * crack * crack_intensity;
等离子体球效果:
// 3D噪声场
float3 sample_pos = world_pos * plasma_scale + time * turbulence_speed;
float plasma = fbm_3d(sample_pos, 4);
// 球体衰减
float sphere_mask = 1.0 - saturate(length(local_pos) / radius);
sphere_mask = pow(sphere_mask, falloff_power);
// 颜色映射
float3 plasma_color = lerp(cold_color, hot_color, plasma);
emissive = plasma_color * sphere_mask * intensity;
能量护盾材质:
// 菲涅尔边缘发光
float fresnel = pow(1.0 - saturate(dot(normal, view_dir)), fresnel_power);
// 六边形网格
float2 hex_uv = world_pos.xy * hex_scale;
float hex_pattern = hexagon_grid(hex_uv);
// 扰动波纹
float ripple = sin(length(hit_point - world_pos) * ripple_frequency - time * ripple_speed);
ripple = saturate(ripple) * exp(-length(hit_point - world_pos) * ripple_decay);
// 组合效果
float3 shield_glow = shield_color * (fresnel + hex_pattern * 0.3 + ripple);
量子涨落效果:
// 量子噪声
float quantum_noise = random(world_pos + time) * 2.0 - 1.0;
quantum_noise *= quantum_amplitude;
// 概率云
float probability = exp(-length(local_pos) * density);
probability *= (1.0 + quantum_noise);
// 相位变化
float phase = atan2(local_pos.y, local_pos.x) + time * phase_speed;
float3 quantum_color = hsv_to_rgb(float3(phase / (2.0 * PI), 0.8, probability));
全息扫描线效果:
// 扫描线
float scan_line = frac(uv.y * scan_line_count + time * scan_speed);
scan_line = smoothstep(0.0, scan_width, scan_line) *
smoothstep(scan_width * 2.0, scan_width, scan_line);
// 数字故障
float glitch = step(0.99, random(floor(time * glitch_rate)));
uv.x += glitch * (random(time) - 0.5) * glitch_strength;
// 色差分离
float3 holo_color;
holo_color.r = tex2D(hologram_tex, uv + float2(chromatic_offset, 0)).r;
holo_color.g = tex2D(hologram_tex, uv).g;
holo_color.b = tex2D(hologram_tex, uv - float2(chromatic_offset, 0)).b;
// 组合效果
float3 final_holo = holo_color * (base_brightness + scan_line * scan_brightness);
final_holo *= (1.0 + glitch * glitch_intensity);
数据流材质:
// 矩阵雨效果
float2 cell = floor(uv * grid_size);
float random_speed = random(cell.x) * 0.5 + 0.5;
float stream = frac(time * random_speed + random(cell));
// 字符采样
float char_index = floor(random(cell + stream) * char_count);
float char_mask = sample_character(char_index, frac(uv * grid_size));
// 渐变衰减
float fade = pow(stream, fade_power);
float3 data_color = lerp(fade_color, bright_color, fade) * char_mask;
数字崩解效果:
// 体素化分解
float3 voxel_pos = floor(world_pos / voxel_size) * voxel_size;
float dissolution = noise_3d(voxel_pos * dissolution_scale + time);
dissolution = smoothstep(dissolve_threshold - 0.1, dissolve_threshold, dissolution);
// 边缘发光
float edge_glow = smoothstep(0.0, edge_width, dissolution) *
smoothstep(1.0, 1.0 - edge_width, dissolution);
// 数字粒子
if(dissolution > 0.5) {
float3 particle_vel = random_direction(voxel_pos) * particle_speed;
float3 particle_pos = voxel_pos + particle_vel * (dissolution - 0.5) * 2.0;
// 渲染粒子...
}
经典Cel Shading:
// 阶梯化光照
float NdotL = dot(normal, light_dir);
float toon_shading = smoothstep(shadow_threshold - shadow_smoothness,
shadow_threshold + shadow_smoothness, NdotL);
// 多级阴影
float shadow_bands = 3.0;
toon_shading = floor(toon_shading * shadow_bands) / shadow_bands;
// 轮廓线(Outline)
// 方法1:法线外扩(几何方法)
vertex_pos += normal * outline_width;
// 方法2:深度边缘检测(后处理)
float depth_edge = sobel_filter(depth_buffer, uv);
outline = step(edge_threshold, depth_edge);
风格化高光:
// 各向异性高光条纹
float3 tangent = normalize(cross(normal, float3(0, 1, 0)));
float TdotH = dot(tangent, half_vector);
float anisotropic = pow(sqrt(1.0 - TdotH * TdotH), anisotropic_power);
// 星形高光
float star_angle = atan2(reflected.y, reflected.x);
float star = sin(star_angle * star_points) * 0.5 + 0.5;
star = pow(star, star_sharpness);
水彩效果着色器:
// 边缘扩散
float edge_bleed = sample_noise(uv * bleed_scale) * bleed_amount;
uv += edge_bleed * ddx(uv) + edge_bleed * ddy(uv);
// 颜料密度变化
float density_variation = fbm(uv * density_scale, 3);
float3 watercolor = base_color * (0.7 + 0.3 * density_variation);
// 湿边效果
float wetness = 1.0 - smoothstep(0.0, wet_edge_width, distance_to_edge);
watercolor *= 1.0 + wetness * wet_edge_darkness;
// 纸张纹理
float paper_texture = tex2D(paper_normal, uv * paper_scale).a;
watercolor *= paper_texture;
油画笔触效果:
// Kuwahara滤波(油画效果)
float3 kuwahara_filter(sampler2D tex, float2 uv, float radius) {
float3 mean[4];
float variance[4];
// 计算四个象限的均值和方差
for(int k = 0; k < 4; k++) {
mean[k] = 0;
float3 sum2 = 0;
int count = 0;
for(int i = -radius; i <= radius; i++) {
for(int j = -radius; j <= radius; j++) {
if(in_quadrant(i, j, k)) {
float3 color = tex2D(tex, uv + float2(i, j) / resolution);
mean[k] += color;
sum2 += color * color;
count++;
}
}
}
mean[k] /= count;
variance[k] = (sum2 / count - mean[k] * mean[k]).r;
}
// 选择方差最小的区域
int min_idx = 0;
for(int k = 1; k < 4; k++) {
if(variance[k] < variance[min_idx]) min_idx = k;
}
return mean[min_idx];
}
// 笔触纹理
float brush_stroke = tex2D(brush_texture, uv * brush_scale).r;
float3 oil_paint = kuwahara_filter(input_tex, uv, filter_radius);
oil_paint *= 0.8 + 0.2 * brush_stroke;
像素艺术风格:
// 降低分辨率
float2 pixel_uv = floor(uv * pixel_resolution) / pixel_resolution;
// 限制调色板
float3 quantized_color = floor(sample_color * color_levels) / color_levels;
// Bayer抖动
float bayer_pattern = get_bayer_matrix(pixel_uv * resolution);
quantized_color += (bayer_pattern - 0.5) / color_levels;
// 像素完美边缘
float edge = step(0.5, fwidth(pixel_uv) * pixel_resolution);
程序化装饰图案:
// 八角星图案
float islamic_star(float2 uv, float n) {
float angle = atan2(uv.y, uv.x);
float r = length(uv);
float star = cos(angle * n) * 0.5 + 0.5;
return smoothstep(0.4, 0.41, r * star);
}
// 阿拉伯花纹
float arabesque = 0;
for(int i = 0; i < 6; i++) {
float2 offset = float2(cos(i * PI / 3), sin(i * PI / 3)) * 0.5;
arabesque += islamic_star(uv - offset, 8);
}
// 编织图案生成
float celtic_knot(float2 uv) {
float2 id = floor(uv);
float2 gv = frac(uv) - 0.5;
// 交织规则
float weave = mod(id.x + id.y, 2.0);
float over = step(0.5, weave);
// 绳索曲线
float rope1 = sdBox(rotate2D(gv, PI/4), float2(0.7, 0.1));
float rope2 = sdBox(rotate2D(gv, -PI/4), float2(0.7, 0.1));
return lerp(rope1, rope2, over);
}
// Mandala生成器
float mandala(float2 uv, int iterations) {
float result = 0;
float2 c = uv;
for(int i = 0; i < iterations; i++) {
// 径向对称
float angle = atan2(c.y, c.x);
angle = mod(angle, TWO_PI / symmetry) * symmetry;
c = length(c) * float2(cos(angle), sin(angle));
// 分形迭代
c = abs(c) - float2(0.5, 0.5);
c *= 1.5;
// 累积图案
result += circle(c, 0.1 * pow(0.8, i));
}
return result;
}
本章深入探讨了材质系统与着色器设计的艺术与技术融合。我们学习了如何突破PBR的物理限制进行艺术化创作,包括超现实金属度、非物理粗糙度梯度和创意性的环境反射处理。程序化材质的参数控制让我们能够创建动态、响应式的材质系统,通过时间、空间和游戏状态驱动材质变化。
次表面散射技术的掌握使我们能够创建真实的皮肤、蜡质和玉石材质,理解了光线在半透明材质中的传播原理。能量体与发光材质的设计技巧让我们能够创造科幻感十足的等离子体、全息投影和数字化效果。最后,风格化着色器的开发打开了艺术表现的新维度,从卡通渲染到水彩油画,从像素艺术到程序化装饰图案。
关键公式回顾:
练习16.1:PBR材质调试 创建一个调试着色器,能够可视化PBR材质的各个组成部分(基础色、金属度、粗糙度、法线、AO)。
练习16.2:简单卡通着色 实现一个基础的三色调卡通着色器(亮部、中间调、暗部)。
练习16.3:基础发光材质 创建一个脉动发光效果,模拟能量核心的呼吸感。
练习16.4:混合材质系统 设计一个材质混合系统,能够基于顶点色或纹理遮罩混合4种不同的PBR材质。
练习16.5:程序化冰冻效果 创建一个可以逐渐冰冻任何物体的着色器,包括冰晶生长、霜花图案和次表面散射。
练习16.6:全息故障效果 实现一个包含扫描线、色差、数字噪声和间歇性故障的全息投影材质。
练习16.7:适应性风格化 创建一个能够根据观察距离自动在写实和卡通风格间过渡的着色器。
练习16.8:材质预览球系统 设计一个材质预览系统,能在球体上展示材质的不同属性和光照条件。
能量不守恒:创建艺术化材质时仍需注意基本的能量守恒,避免过亮或能量凭空产生。
法线混合错误:直接平均法线向量会导致长度不一致,应使用专门的法线混合技术。
精度问题:在移动平台上,半精度浮点可能导致着色器出现条带或噪点。
采样过多:复杂的程序化材质可能需要大量纹理采样,注意优化采样次数。
时间精度丢失:长时间运行后,time变量可能失去精度,使用frac()或mod()保持在合理范围。
Z-fighting:多层透明材质容易出现深度冲突,需要合理设置渲染顺序和深度偏移。
平台差异:不同图形API(DirectX/OpenGL/Vulkan)的坐标系和精度可能不同。
Mipmap问题:程序化生成的纹理可能缺少正确的mipmap,导致远处闪烁。