看见反常 · 动一处 · 抓证据
效果为什么贵,循环和缓冲区会直接说出来
只读 · 教学用 HLSL / GLSL
这页解决什么
按传播、透明、深度、渲染路径、体积循环和数值保护阅读。
半径与厚度塑造扩散 profile
mask 只限定材质;depth、normal、material-ID 双边权重阻止跨边界漏色。
// SECTION:sss
float sssProfile(float radius, int samples, float thickness, float materialMask) {
float diffusion = 0.0;
for (int i = 0; i < samples; i++) {
float bilateral = depthWeight * normalWeight * materialIdWeight;
diffusion += profile(radius, thickness, float(i) / float(samples)) * materialMask * bilateral;
}
return diffusion / float(samples);
}
入口、资源和 Pass 各管一件事
手写 shader 时,沿数据流读比背模板更可靠。
// SECTION:shaderlab
Shader "TA/Learning/NormalMapped" {
Properties { _NormalMap("Normal", 2D) = "bump" {} }
SubShader {
Pass { HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDHLSL }
}
}
Coverage 不是 Blend
A2C 依赖 MSAA;没有 MSAA 时 coverage 结果不可依赖。
// SECTION:alpha-coverage
// Teaching estimate only: real A2C sample masks are selected by the GPU.
// A2C has no dependable coverage result when MSAA is disabled.
float coverage = msaaSamples > 1 ? round(alpha * float(msaaSamples)) / float(msaaSamples) : alpha;
float4 color = alphaMode == ATOC
? float4(baseColor, coverage)
: blendSortedLayers(baseColor, alpha);
深度差决定能看见多少
depth prepass 省后续隐藏工作,不提高可见度权重。
// SECTION:occlusion
// sceneDepth and particleDepth are both linear eye-depth meters.
float visibility = alpha * saturate((sceneDepth - particleDepth) / fadeDistance);
// A depth prepass can reject later hidden fragments; it never boosts this weight.
Shader 淡出不是 CPU / Hi-Z 剔除
对象或批次被提交前拒绝,才跳过后续 Vertex 与 Fragment。
// SECTION:culling-stage
// Shader fading keeps an already submitted draw. CPU / Hi-Z culling rejects
// an object or batch before submission, so vertex and fragment work never run.
先统一模块输入
阶段、强度、挂点、随机种子与质量档由同一份上下文提供。
// SECTION:cosmetic-context
// Unity-facing teaching pseudocode. This browser page does not compile it.
struct CosmeticEffectContext {
float phase; // 0..1, dimensionless
float intensity; // dimensionless
float timeSeconds; // seconds
float4x4 attachmentWorld[4];
uint randomSeed;
uint qualityTier;
};
每层只负责一种画面结果
表面、电弧、粒子、深度遮挡与后处理按因果顺序接线。
// SECTION:cosmetic-modules
void RenderCosmeticStack(CosmeticEffectContext context) {
SurfaceResult surface = AnimateSurface(context.phase, context.intensity);
ArcResult arcs = BuildArcs(context.attachmentWorld, context.randomSeed);
TrailResult particles = EmitParticlesAndTrail(arcs, context.phase);
CompositeResult visible = ApplyDepthOcclusion(surface, arcs, particles);
PostProcess(visible); // Bloom amplifies a finished shape; it does not create it.
}
先删装饰,再动主辨识
这里是降级顺序伪代码;阈值仍要用 Unity 真机数据回填。
// SECTION:cosmetic-quality
CosmeticQuality SelectCosmeticQuality(uint tier) {
// Keep surface identity and one primary arc on the lowest tier.
// Remove secondary arcs, particles and screen effects before core timing.
return tier == LOW
? CosmeticQuality(1, 0, false)
: tier == MEDIUM
? CosmeticQuality(2, 24, true)
: CosmeticQuality(5, 48, true);
}
渲染路径在交换资源
灯、G-buffer、透明与带宽共同决定选择。
// SECTION:forward-deferred
// Forward: light work follows each visible material.
// Deferred: write G-buffer, then light screen-space pixels.
float lightingCost = renderPath == FORWARD
? visiblePixels * lightCount
: gbufferBandwidth + visiblePixels;
步数乘噪声层数
天气质量和成本都藏在这段循环里。
// SECTION:volume
for (int step = 0; step < volumeSteps; step++) {
float density = sampleNoise(rayPosition, noiseOctaves);
if (density == 0.0) { rayPosition += rayDirection * stepLength; continue; }
transmittance *= exp(-density * stepLength);
radiance += transmittance * density * exposure;
if (transmittance < earlyOutThreshold || remainingRayIsProvenEmpty(rayPosition)) break;
rayPosition += rayDirection * stepLength;
}
数字坏掉也要变成可见颜色
NaN、Inf 和过曝都需要限制与调试输出。
// SECTION:overflow
float3 safeRadiance = clampHdr ? min(radiance, maxHdr) : radiance;
debugColor = any(isnan(safeRadiance)) || max(safeRadiance) > maxHdr
? float3(1.0, 0.1, 0.1)
: safeRadiance;