本篇内容
// 计算透射度材质
compute.Dispatch(compute_transmittance, CONSTANTS.TRANSMITTANCE_WIDTH / NUM_THREADS, CONSTANTS.TRANSMITTANCE_HEIGHT / NUM_THREADS, 1);
// 计算没有经过任何散射的地面直接辐照度
compute.Dispatch(compute_direct_irradiance, CONSTANTS.IRRADIANCE_WIDTH / NUM_THREADS, CONSTANTS.IRRADIANCE_HEIGHT / NUM_THREADS, 1);
// 计算3D单次散射纹理
for (int layer = 0; layer < CONSTANTS.SCATTERING_DEPTH; ++layer)
{
// 计算3D texture ScatteringArray
compute.SetInt("layer", layer);
compute.Dispatch(compute_single_scattering, CONSTANTS.SCATTERING_WIDTH / NUM_THREADS, CONSTANTS.SCATTERING_HEIGHT / NUM_THREADS, 1);
}
// 循环计算多次散射
for (int scattering_order = 2; scattering_order <= num_scattering_orders; ++scattering_order)
{
// 计算大气辐射度
for (int layer = 0; layer < CONSTANTS.SCATTERING_DEPTH; ++layer)
{
compute.SetInt("layer", layer);
compute.Dispatch(compute_scattering_density, CONSTANTS.SCATTERING_WIDTH / NUM_THREADS, CONSTANTS.SCATTERING_HEIGHT / NUM_THREADS, 1);
}
// 计算来自地面的辐照度
compute.Dispatch(compute_indirect_irradiance, CONSTANTS.IRRADIANCE_WIDTH / NUM_THREADS, CONSTANTS.IRRADIANCE_HEIGHT / NUM_THREADS, 1);
// 利用上面两项计算出本阶的多次散射
for (int layer = 0; layer < CONSTANTS.SCATTERING_DEPTH; ++layer)
{
compute.SetInt("layer", layer);
compute.Dispatch(compute_multiple_scattering, CONSTANTS.SCATTERING_WIDTH / NUM_THREADS, CONSTANTS.SCATTERING_HEIGHT / NUM_THREADS, 1);
}
}
本篇主要讲解
大气辐射度
地面辐照度
和用以上两个材质计算的多次散射材质
第一步ComputeScatteringDensity
入口函数
#pragma kernel ComputeScatteringDensity
[numthreads(NUM_THREADS,NUM_THREADS,1)]
void ComputeScatteringDensity(uint3 id : SV_DispatchThreadID)
{
id.z = layer;
float3 gl_FragCoord = id + float3(0.5,0.5,0.5);
float3 scattering_density = ComputeScatteringDensityTexture(transmittanceRead, singleRayleighScatteringRead,
singleMieScatteringRead, multipleScatteringRead,
irradianceRead, gl_FragCoord, scatteringOrder);
deltaScatteringDensityWrite[id] = float4(scattering_density, 1);
}
RadianceDensitySpectrum ComputeScatteringDensityTexture(
TransmittanceTexture transmittance_texture,
ReducedScatteringTexture single_rayleigh_scattering_texture,
ReducedScatteringTexture single_mie_scattering_texture,
ScatteringTexture multiple_scattering_texture,
IrradianceTexture irradiance_texture,
float3 gl_frag_coord, int scattering_order)
{
Length r;
Number mu;
Number mu_s;
Number nu;
bool ray_r_mu_intersects_ground;
GetRMuMuSNuFromScatteringTextureFragCoord(gl_frag_coord,
r, mu, mu_s, nu, ray_r_mu_intersects_ground);
return ComputeScatteringDensity(transmittance_texture,
single_rayleigh_scattering_texture, single_mie_scattering_texture,
multiple_scattering_texture, irradiance_texture, r, mu, mu_s, nu,
scattering_order);
}
纹理映射函数GetRMuMuSNuFromScatteringTextureFragCoord上一章已经提到了,从gl坐标可以获取到我们需要的参数r, mu, mu_s, nu。
计算Radiance的原理
第一步,我们需要计算在大气中某一点 q 处,向某个方向 −ω 散射出的辐射度。假设这是第 n 次散射(bounce)。
这个 q 是散射点,最后会沿着 -ω 方向散射到我们的目标点 p 点,之后会对 -ω 这条线上的每一个这样的 q 点做直线积分。
这个 q 点的散射辐射度等于对所有可能的入射方向 ωᵢ 进行积分,也就是球面积分,积分的被积函数是以下的乘积:
从方向 ωᵢ 到达 q 处、经历了 n−1 次散射后的入射辐射度 Lᵢ,这部分包括以下的总和:
第n-1阶预计算纹理给出的散射项
此外,当射线[q, ωᵢ)与地面相交时,还要把这部分光路的贡献加上。因为我们预计算的多次散射纹理只包含在大气中发生的散射,不包括最后一次在地面上的反射;但这里地面反射后又在 q 处发生散射,所以必须单独计算。地面贡献等于:
q 到 相交点 之间的大气透射率;
地面的平均反照率(albedo);
Lambert漫反射BRDF(1/π);
地面在经历 n−2 次散射后接收到的辐照度;为什么是n-2呢,是因为这样说明第n-1次散射就有希望散射到 q 点,进而在第n次散射到目标点p
在 q 处的散射系数;
散射相位函数 p(ωᵢ→−ω);
其中:
实现如下:
这里对 某一个 q 点的 所有 可能是入射方向 ωᵢ (也就是球面)做积分。(但不要忘了 之后还要对 每一个q 和每一个 -ω 做积分)
现在我们还只是单个计算L(q , -w)
RadianceDensitySpectrum ComputeScatteringDensity(
TransmittanceTexture transmittance_texture,
ReducedScatteringTexture single_rayleigh_scattering_texture,
ReducedScatteringTexture single_mie_scattering_texture,
ScatteringTexture multiple_scattering_texture,
IrradianceTexture irradiance_texture,
Length r, Number mu, Number mu_s, Number nu, int scattering_order)
{
// Compute unit direction vectors for the zenith, the view direction omega and
// and the sun direction omega_s, such that the cosine of the view-zenith
// angle is mu, the cosine of the sun-zenith angle is mu_s, and the cosine of
// the view-sun angle is nu. The goal is to simplify computations below.
float3 zenith_direction = float3(0.0, 0.0, 1.0);
float3 omega = float3(sqrt(1.0 - mu * mu), 0.0, mu);
Number sun_dir_x = omega.x == 0.0 ? 0.0 : (nu - mu * mu_s) / omega.x;
Number sun_dir_y = sqrt(max(1.0 - sun_dir_x * sun_dir_x - mu_s * mu_s, 0.0));
float3 omega_s = float3(sun_dir_x, sun_dir_y, mu_s);
const int SAMPLE_COUNT = 16;
const Angle dphi = pi / Number(SAMPLE_COUNT);
const Angle dtheta = pi / Number(SAMPLE_COUNT);
RadianceDensitySpectrum rayleigh_mie = RadianceDensitySpectrum(0,0,0);
// Nested loops for the integral over all the incident directions omega_i.
for (int l = 0; l < SAMPLE_COUNT; ++l)
{
Angle theta = (Number(l) + 0.5) * dtheta;
Number cos_theta = cos(theta);
Number sin_theta = sin(theta);
bool ray_r_theta_intersects_ground = RayIntersectsGround(r, cos_theta);
// The distance and transmittance to the ground only depend on theta, so we
// can compute them in the outer loop for efficiency.
Length distance_to_ground = 0.0 * m;
DimensionlessSpectrum transmittance_to_ground = DimensionlessSpectrum(0,0,0);
DimensionlessSpectrum ground_albedo = DimensionlessSpectrum(0,0,0);
if (ray_r_theta_intersects_ground)
{
distance_to_ground = DistanceToBottomAtmosphereBoundary(r, cos_theta);
transmittance_to_ground = GetTransmittance(transmittance_texture, r, cos_theta, distance_to_ground, true);
}
for (int m = 0; m < 2 * SAMPLE_COUNT; ++m)
{
Angle phi = (Number(m) + 0.5) * dphi;
float3 omega_i = float3(cos(phi) * sin_theta, sin(phi) * sin_theta, cos_theta);
SolidAngle domega_i = (dtheta / rad) * (dphi / rad) * sin(theta) * sr;
// The radiance L_i arriving from direction omega_i after n-1 bounces is
// the sum of a term given by the precomputed scattering texture for the
// (n-1)-th order:
Number nu1 = dot(omega_s, omega_i);
RadianceSpectrum incident_radiance = GetScattering(
single_rayleigh_scattering_texture, single_mie_scattering_texture,
multiple_scattering_texture, r, omega_i.z, mu_s, nu1,
ray_r_theta_intersects_ground, scattering_order - 1);
// and of the contribution from the light paths with n-1 bounces and whose
// last bounce is on the ground. This contribution is the product of the
// transmittance to the ground, the ground albedo, the ground BRDF, and
// the irradiance received on the ground after n-2 bounces.
float3 ground_normal = normalize(zenith_direction * r + omega_i * distance_to_ground);
IrradianceSpectrum ground_irradiance = GetIrradiance(irradiance_texture, bottom_radius, dot(ground_normal, omega_s));
incident_radiance += transmittance_to_ground * ground_albedo * (1.0 / (PI * sr)) * ground_irradiance;
// The radiance finally scattered from direction omega_i towards direction
// -omega is the product of the incident radiance, the scattering
// coefficient, and the phase function for directions omega and omega_i
// (all this summed over all particle types, i.e. Rayleigh and Mie).
Number nu2 = dot(omega, omega_i);
Number rayleigh_density = GetProfileDensity(RayleighDensity(), r - bottom_radius);
Number mie_density = GetProfileDensity(MieDensity(), r - bottom_radius);
rayleigh_mie += incident_radiance * (
rayleigh_scattering * rayleigh_density * RayleighPhaseFunction(nu2) +
mie_scattering * mie_density * MiePhaseFunction(mie_phase_function_g, nu2)) *
domega_i;
}
}
return rayleigh_mie;
}
首先准备几何向量,x、y坐标是水平面上的坐标,z是高度坐标。dω=sinθ dθ dϕ 是立体角微元
得到omega,视线方向的归一化坐标,强制锁定其y坐标为0,作为太阳的参考物。得到omega_s,相对于视线方向的x、y坐标,和他的归一化高度z,也就是cos值mu_s。
球面积分原理
然后做入射方向的球面积分,但一个循环是从0到pi,另外一个是从0到2pi,从哪里看出来是球面?
首先看立体角的原理:
θ是天顶角,φ是方位角(经度)
或者你想想,一个半圆,绕着直径边旋转360°,就形成了一个球。
float3 omega_i = float3(
cos(phi) * sin_theta,
sin(phi) * sin_theta,
cos_theta
);
这两个角通过映射得到单位笛卡尔坐标/向量
domega_i = sin(theta) * dtheta * dphi;
这里得到立体角微元
代码中:
omega是某一个视线方向,对应公式中的 -ω;
omega_s 是太阳方向
omega_i 是积分过程中的某个方向,用于计算从这个方向散射过来的光
地面辐照度 ComputeIndirectIrradiance
这一部分的值被用在上一步,意义是第n-2次散射到地面的Irradiance,之后会在第n-1次被散射到q点
入口函数
#pragma kernel ComputeIndirectIrradiance
[numthreads(NUM_THREADS, NUM_THREADS, 1)]
void ComputeIndirectIrradiance(uint3 id : SV_DispatchThreadID)
{
float2 gl_FragCoord = id.xy + float2(0.5,0.5);
float3 delta_irradiance = ComputeIndirectIrradianceTexture(
singleRayleighScatteringRead,
singleMieScatteringRead, multipleScatteringRead,
gl_FragCoord, scatteringOrder);
float3 irradiance = RadianceToLuminance(delta_irradiance);
deltaIrradianceWrite[id.xy] = float4(delta_irradiance, 1);
irradianceWrite[id.xy] = float4(irradiance, 1);
if(blend[1] == 1)
irradianceWrite[id.xy] += irradianceRead[id.xy];
}
各材质的意义
deltaIrradianceWrite[id.xy] = float4(delta_irradiance, 1);
本轮三个波长的累计地面辐照度。存储本次计算的“光谱辐照度增量”ΔE(λ)(对每组波长的离线果),是地面在当前散射阶中新产生的辐照度。
irradianceWrite[id.xy] = float4(irradiance, 1);
所有波长的累积地面辐照度。存储“累积亮度”Eₗᵤₘ(将 ΔE(λ) 按 CIE 亮度函数或 RGB 加权求和后的结果),并在 blend 开启时把历史结果(irradianceRead)累加进去,得到多阶最终地面亮度。
只有deltaIrradianceWrite会参与后续的计算
理论
对于间接地面辐照度,必须计算半球的积分。我们需要对半球上所有方向 ω 做积分,积分项是各方向上的乘积,即以下两项的乘积:
经过 n 次反射后到达方向 ω 的辐照度,
余弦因子,即 ωz
函数实现
IrradianceSpectrum ComputeIndirectIrradiance(
ReducedScatteringTexture single_rayleigh_scattering_texture,
ReducedScatteringTexture single_mie_scattering_texture,
ScatteringTexture multiple_scattering_texture,
Length r, Number mu_s, int scattering_order)
{
const int SAMPLE_COUNT = 32;
const Angle dphi = pi / Number(SAMPLE_COUNT);
const Angle dtheta = pi / Number(SAMPLE_COUNT);
IrradianceSpectrum result = IrradianceSpectrum(0, 0, 0);
float3 omega_s = float3(sqrt(1.0 - mu_s * mu_s), 0.0, mu_s);
for (int j = 0; j < SAMPLE_COUNT / 2; ++j)
{
Angle theta = (Number(j) + 0.5) * dtheta;
for (int i = 0; i < 2 * SAMPLE_COUNT; ++i)
{
Angle phi = (Number(i) + 0.5) * dphi;
float3 omega = float3(cos(phi) * sin(theta), sin(phi) * sin(theta), cos(theta));
SolidAngle domega = (dtheta / rad) * (dphi / rad) * sin(theta) * sr;
Number nu = dot(omega, omega_s);
result += GetScattering(single_rayleigh_scattering_texture,
single_mie_scattering_texture, multiple_scattering_texture,
r, omega.z, mu_s, nu, false,scattering_order) * omega.z * domega;
}
}
return result;
}
辐照度(Irradiance)E 定义为从半球内各方向入射到点 P 上、乘以入射角余弦并对立体角积分后的辐射度
cosθ=ωz 是方向 ω 与法线方向的夹角余弦。dω=sinθ dθ dϕ 是立体角微元
注意这里第一个循环是 j<SAMPLE_COUNT / 2,是九十度,其实是上半球积分
GetScattering函数:
一阶散射因为比较“简单”,把大气分子(Rayleigh)和气溶胶(Mie)分开存、分开算,再乘相位函数,精度高且贴图小。
多阶散射因为数据量大,直接把“密度×相位”结果烘焙到一个 3D 贴图里,运行时直接查表。
RadianceSpectrum GetScattering(
ReducedScatteringTexture single_rayleigh_scattering_texture,
ReducedScatteringTexture single_mie_scattering_texture,
ScatteringTexture multiple_scattering_texture,
Length r, Number mu, Number mu_s, Number nu,
bool ray_r_mu_intersects_ground,
int scattering_order)
{
if (scattering_order == 1)
{
IrradianceSpectrum rayleigh = GetScattering(
single_rayleigh_scattering_texture, r, mu, mu_s, nu,
ray_r_mu_intersects_ground);
IrradianceSpectrum mie = GetScattering(
single_mie_scattering_texture, r, mu, mu_s, nu,
ray_r_mu_intersects_ground);
return rayleigh * RayleighPhaseFunction(nu) +
mie * MiePhaseFunction(mie_phase_function_g, nu);
}
else
{
return GetScattering(
multiple_scattering_texture, r, mu, mu_s, nu,
ray_r_mu_intersects_ground);
}
}
ComputeMultipleScattering
理论
这一步的核心是在视线方向 ω 上,对“沿射线每一点 q 处的散射贡献”做一次一维积分,这个散射贡献已经在ComputeScatteringDensity函数中计算完了。
入口函数
#pragma kernel ComputeMultipleScattering
[numthreads(NUM_THREADS, NUM_THREADS, 1)]
void ComputeMultipleScattering(uint3 id : SV_DispatchThreadID)
{
id.z = layer;
float3 gl_FragCoord = id + float3(0.5,0.5,0.5);
float nu;
float3 delta_multiple_scattering = ComputeMultipleScatteringTexture(
transmittanceRead, deltaScatteringDensityRead,
gl_FragCoord, nu);
deltaMultipleScatteringWrite[id] = float4(delta_multiple_scattering, 1);
scatteringWrite[id] = float4(RadianceToLuminance(delta_multiple_scattering) / RayleighPhaseFunction(nu), 0.0);
if(blend[1] == 1)
scatteringWrite[id] += scatteringRead[id];
}
具体实现
在ComputeMultipleScatteringTexture中,先进行纹理映射,然后调用ComputeMultipleScattering函数:
RadianceSpectrum ComputeMultipleScatteringTexture(
TransmittanceTexture transmittance_texture,
ScatteringDensityTexture scattering_density_texture,
float3 gl_frag_coord, out Number nu)
{
Length r;
Number mu;
Number mu_s;
bool ray_r_mu_intersects_ground;
GetRMuMuSNuFromScatteringTextureFragCoord(gl_frag_coord,
r, mu, mu_s, nu, ray_r_mu_intersects_ground);
return ComputeMultipleScattering(transmittance_texture,
scattering_density_texture, r, mu, mu_s, nu,
ray_r_mu_intersects_ground);
}
RadianceSpectrum ComputeMultipleScattering(
TransmittanceTexture transmittance_texture,
ScatteringDensityTexture scattering_density_texture,
Length r, Number mu, Number mu_s, Number nu,
bool ray_r_mu_intersects_ground)
{
// Number of intervals for the numerical integration.
const int SAMPLE_COUNT = 50;
// The integration step, i.e. the length of each integration interval.
Length dx = DistanceToNearestAtmosphereBoundary(r, mu, ray_r_mu_intersects_ground) / Number(SAMPLE_COUNT);
// Integration loop.
RadianceSpectrum rayleigh_mie_sum = RadianceSpectrum(0,0,0);
for (int i = 0; i <= SAMPLE_COUNT; ++i)
{
Length d_i = Number(i) * dx;
// The r, mu and mu_s parameters at the current integration point (see the
// single scattering section for a detailed explanation).
Length r_i = ClampRadius(sqrt(d_i * d_i + 2.0 * r * mu * d_i + r * r));
Number mu_i = ClampCosine((r * mu + d_i) / r_i);
Number mu_s_i = ClampCosine((r * mu_s + d_i * nu) / r_i);
// The Rayleigh and Mie multiple scattering at the current sample point.
RadianceSpectrum rayleigh_mie_i =
GetScattering(scattering_density_texture, r_i, mu_i, mu_s_i, nu, ray_r_mu_intersects_ground) *
GetTransmittance(transmittance_texture, r, mu, d_i, ray_r_mu_intersects_ground) * dx;
// Sample weight (from the trapezoidal rule).
Number weight_i = (i == 0 || i == SAMPLE_COUNT) ? 0.5 : 1.0;
rayleigh_mie_sum += rayleigh_mie_i * weight_i;
}
return rayleigh_mie_sum;
}
这是沿着直线做积分,注意上次的ComputeDensity是做球面积分。
最后得到的delta_multiple_scattering 就是rayleigh_mie_sum的值。
预告
下一篇会分享实际的渲染Shader的学习心得,有关如何形成光锥尾影、如何使用这些材质等。