跳到主要内容
rendering-effects · 代码
Graphics Learning Lab Rendering Effects / Source

看见反常 · 动一处 · 抓证据

效果为什么贵,循环和缓冲区会直接说出来

只读 · 教学用 HLSL / GLSL

这页解决什么

按传播、透明、深度、渲染路径、体积循环和数值保护阅读。

SSS 传播

半径与厚度塑造扩散 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);
}

ShaderLab 骨架

入口、资源和 Pass 各管一件事

手写 shader 时,沿数据流读比背模板更可靠。

// SECTION:shaderlab
Shader "TA/Learning/NormalMapped" {
  Properties { _NormalMap("Normal", 2D) = "bump" {} }
  SubShader {
    Pass { HLSLPROGRAM
      #pragma vertex vert
      #pragma fragment frag
    ENDHLSL }
  }
}

Alpha / A2C

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);
}

Forward / Deferred

渲染路径在交换资源

灯、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;
}

HDR 溢出

数字坏掉也要变成可见颜色

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;