ShaderToy学习笔记 08.阴影
1. 阴影
阴影是3D渲染中一个重要的视觉效果,它能够增强场景的深度感和真实感。在着色器中,阴影主要分为以下几种:
- 硬阴影 (Hard Shadow)
边缘清晰的阴影,没有过渡区域
更容易实现,但不够真实
主要通过光线追踪(Ray Marching)实现 - 软阴影 (Soft Shadow)
边缘有渐变过渡的阴影
更接近现实中的阴影效果
通常使用阴影贴图或光线追踪配合柔化算法实现 - 实现原理
阴影的基本原理是通过检测从表面点到光源的路径是否被其他物体遮挡:
float shadowRay(vec3 ro, vec3 rd, float mint, float maxt) {// ro: 射线起点(表面点)// rd: 射线方向(指向光源)float t = mint;float res = 1.0; // 1.0表示完全照亮,0.0表示完全阴影for(int i = 0; i < MAX_STEPS; i++) {vec3 p = ro + rd * t;float h = scene(p); // 场景SDF函数if(h < EPSILON) {return 0.0; // 完全阴影}t += h;if(t >= maxt) break;}return res;
}4. 阴影的作用
**增强深度感**
- 帮助观察者理解物体之间的空间关系
- 提供场景中物体的位置信息
**提升真实感**
- 模拟现实世界中光照的自然效果
- 增强场景的视觉效果
**强化氛围**
- 可以用于创造特定的视觉效果和情绪
- 对场景的戏剧性表现很重要
5. 注意事项
- 阴影计算通常比较消耗性能
- 需要在效果和性能之间找到平衡
- 软阴影虽然效果更好,但计算开销更大
- 阴影质量直接影响整体渲染效果
1.1. 设置基础场景
const int MAX_MARCHING_STEPS = 255;
const float MIN_DIST = 0.0;
const float MAX_DIST = 100.0;
const float PRECISION = 0.001;
const float EPSILON = 0.0005;float sdSphere(vec3 p, float r, vec3 offset)
{return length(p - offset) - r;
}float sdFloor(vec3 p) {return p.y + 1.;
}float scene(vec3 p) {float co = min(sdSphere(p, 1., vec3(0, 0, -2)), sdFloor(p));return co;
}float rayMarch(vec3 ro, vec3 rd) {float depth = MIN_DIST;float d; // distance ray has travelledfor (int i = 0; i < MAX_MARCHING_STEPS; i++) {vec3 p = ro + depth * rd;d = scene(p);depth += d;if (d < PRECISION || depth > MAX_DIST) break;}d = depth;return d;
}vec3 calcNormal(in vec3 p) {vec2 e = vec2(1, -1) * EPSILON;return normalize(e.xyy * scene(p + e.xyy) +e.yyx * scene(p + e.yyx) +e.yxy * scene(p + e.yxy) +e.xxx * scene(p + e.xxx));
}void mainImage( out vec4 fragColor, in vec2 fragCoord )
{vec2 uv = (fragCoord-.5*iResolution.xy)/iResolution.y;vec3 backgroundColor = vec3(0);vec3 col = vec3(0);vec3 ro = vec3(0, 0, 3); // ray origin that represents camera positionvec3 rd = normalize(vec3(uv, -1)); // ray directionfloat sd = rayMarch(ro, rd); // signed distance value to closest objectif (sd > MAX_DIST) {col = backgroundColor; // ray didn't hit anything} else {vec3 p = ro + rd * sd; // point discovered from ray marchingvec3 normal = calcNormal(p); // surface normalvec3 lightPosition = vec3(cos(iTime), 2, sin(iTime));vec3 lightDirection = normalize(lightPosition - p);float dif = clamp(dot(normal, lightDirection), 0., 1.); // diffuse reflection clamped between zero and onecol = vec3(dif);}fragColor = vec4(col, 1.0);
}
1.2. 基本阴影
下图展示光线追踪中阴影的计算过程。
所以我们只需要在光线追踪中,我们检测到球体或地面的点,以该点为原点,向光源方向发射一条射线,如果这条射线与场景中的物体相交,那么就说明该点被遮挡,否则就是完全可见的。
核心代码
vec3 newOrigin=p;float newDepth=rayMarch(newOrigin,lightDirection);float shadow = shadowRay(newOrigin,lightDirection);dif *= shadow;col = vec3(dif);
但这样将会得到下图所示的结果:
因此我们需要将p点向表面法线移动一定的距离,以模拟阴影的厚度,代码如下:
vec3 newOrigin=p+normal*0.01; float newDepth=rayMarch(newOrigin,lightDirection);float shadow = shadowRay(newOrigin,lightDirection);dif *= shadow;col = vec3(dif);
下图展示阴影的效果:
完整代码
const int MAX_MARCHING_STEPS = 255;
const float MIN_DIST = 0.0;
const float MAX_DIST = 100.0;
const float PRECISION = 0.001;
const float EPSILON = 0.0005;float sdSphere(vec3 p, float r, vec3 offset)
{return length(p - offset) - r;
}float sdFloor(vec3 p) {return p.y + 1.;
}float scene(vec3 p) {float co = min(sdSphere(p, 1., vec3(0, 0, -2)), sdFloor(p));return co;
}float rayMarch(vec3 ro, vec3 rd) {float depth = MIN_DIST;float d; // distance ray has travelledfor (int i = 0; i < MAX_MARCHING_STEPS; i++) {vec3 p = ro + depth * rd;d = scene(p);depth += d;if (d < PRECISION || depth > MAX_DIST) break;}d = depth;return d;
}float shadowRay(vec3 ro, vec3 rd) {// ro: 射线起点(表面点)// rd: 射线方向(指向光源)float t = MIN_DIST;float res = 1.0; // 1.0表示完全照亮,0.0表示完全阴影for(int i = 0; i < MAX_MARCHING_STEPS; i++) {vec3 p = ro + rd * t;float h = scene(p); // 场景SDF函数if(h < PRECISION) {return 0.0; // 完全阴影}t += h;if(t >= MAX_DIST) break;}return res;
}vec3 calcNormal(in vec3 p) {vec2 e = vec2(1, -1) * EPSILON;return normalize(e.xyy * scene(p + e.xyy) +e.yyx * scene(p + e.yyx) +e.yxy * scene(p + e.yxy) +e.xxx * scene(p + e.xxx));
}void mainImage( out vec4 fragColor, in vec2 fragCoord )
{vec2 uv = (fragCoord-.5*iResolution.xy)/iResolution.y;vec3 backgroundColor = vec3(0);vec3 col = vec3(0);vec3 ro = vec3(0, 0, 3); // ray origin that represents camera positionvec3 rd = normalize(vec3(uv, -1)); // ray directionfloat sd = rayMarch(ro, rd); // signed distance value to closest objectif (sd > MAX_DIST) {col = backgroundColor; // ray didn't hit anything} else {vec3 p = ro + rd * sd; // point discovered from ray marchingvec3 normal = calcNormal(p); // surface normalvec3 lightPosition = vec3(cos(iTime), 2, sin(iTime));vec3 lightDirection = normalize(lightPosition - p);float dif = clamp(dot(normal, lightDirection), 0., 1.); // diffuse reflection clamped between zero and one//vec3 newOrigin=p; // 从p点向光源发射一条光线,计算这条光线与场景的交点,如果交点在阴影内,则dif=0,否则dif=1 ,但这样会显示基本全黑// 从p点向光源发射一条光线,计算这条光线与场景的交点,如果交点在阴影内,则dif=0,否则dif=1,需要将光线起点向法线方向移动0.01vec3 newOrigin=p+normal*0.01; float newDepth=rayMarch(newOrigin,lightDirection);float shadow = shadowRay(newOrigin,lightDirection);dif *= shadow;col = vec3(dif);}fragColor = vec4(col, 1.0);
}
1.3. 彩色物体
只需要在之前彩色物体的基础上加上阴影即可,代码如下:
#define PIXW (1./iResolution.y)const int MAX_STEPS = 100;
const float START_DIST = 0.001;
const float MIN_DIST = 0.0;
const float MAX_DIST = 100.0;
const float EPSILON = 0.0001;struct Material {vec3 ambientColor; // k_a * i_avec3 diffuseColor; // k_d * i_dvec3 specularColor; // k_s * i_sfloat alpha; // shininess
};struct SDFResult
{float d;Material mat;
};Material gold()
{return Material(vec3(0.7,0.5,0.)*0.5,vec3(0.7, 0.7, 0.),vec3(1),5.);
}
Material silver()
{return Material(vec3(0.8)*0.4,vec3(0.7)*0.5,vec3(1.)*0.6,5.);
}Material checkerboard(vec3 p)
{return Material(vec3(1. + 0.7*mod(floor(p.x) + floor(p.z), 2.0)) * 0.3,vec3(0.3),vec3(0),1.);
}mat4 rotationX(float theta)
{return mat4(1.0, 0.0, 0.0, 0.0,0.0, cos(theta), -sin(theta), 0.0,0.0, sin(theta), cos(theta), 0.0,0.0, 0.0, 0.0, 1.0);
}mat4 rotationY(float theta)
{return mat4(cos(theta), 0.0, sin(theta), 0.0,0.0, 1.0, 0.0, 0.0,-sin(theta), 0.0, cos(theta), 0.0,0.0, 0.0, 0.0, 1.0);
}
mat4 rotationZ(float theta)
{return mat4(cos(theta), -sin(theta), 0.0, 0.0,sin(theta), cos(theta), 0.0, 0.0,0.0, 0.0, 1.0, 0.0,0.0, 0.0, 0.0, 1.0);
}SDFResult sdBox( vec3 p, vec3 b,vec3 offset,Material mat )
{vec3 q = abs(p-offset) - b;return SDFResult(length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0),mat);
}vec3 getBackgroundColor(vec2 uv)
{
//uv.y [-1,1]
//y: [0,1] float y=(uv.y+1.)/2.; return mix(vec3(1,0,1),vec3(0,1,1),y);
}SDFResult sdSphere(vec3 p, float r,vec3 offset,Material mat)
{return SDFResult(length(p-offset)-r,mat);
}
SDFResult sdFloor(vec3 p,Material mat)
{float d=p.y+1.;return SDFResult(d,mat);
}SDFResult minWithColor(SDFResult a,SDFResult b)
{if (a.d<b.d){return a;}return b;
}mat3 camera(vec3 cameraPos, vec3 lookAtPoint, vec3 upVector) {vec3 cd = normalize(lookAtPoint - cameraPos); // camera directionvec3 cr = normalize(cross(upVector, cd)); // camera rightvec3 cu = normalize(cross(cd, cr)); // camera upreturn mat3(-cr, cu, -cd); //转换为x轴向右,y轴向上,z轴向屏幕外边的右手坐标系
}vec3 phongLighting(vec3 lightDir,vec3 normal,vec3 rd,vec3 viewDir,Material material)
{vec3 ambientColor = material.ambientColor;//计算漫反射vec3 diffuse = material.diffuseColor * max(dot(normal, lightDir), 0.0);//计算镜面反射vec3 reflectDir = reflect(-lightDir, normal);float specular = pow(max(dot(reflectDir, viewDir), 0.0), material.alpha);vec3 specularColor = material.specularColor * specular;return ambientColor + diffuse + specularColor;// return diffuse + specularColor;
}SDFResult sdScene(vec3 p)
{SDFResult result1=sdSphere(p,0.5,vec3(-0.6,0.,0.),gold());SDFResult result2=sdSphere(p,0.5,vec3(0.6,0.,0.),silver());//SDFResult result3=sdBox(p,vec3(1.,1.0,1.),vec3(4,0.2,-4),vec3(0.,0.,1.));SDFResult result=minWithColor(result1,result2);//result=minWithColor(result,result3);result=minWithColor(result, sdFloor(p,checkerboard(p)));return result;
}
//法线计算
vec3 calcNormal(vec3 p) {vec2 e = vec2(1.0, -1.0) * 0.0005; // epsilonfloat r = 1.; // radius of spherereturn normalize(e.xyy * sdScene(p + e.xyy).d +e.yyx * sdScene(p + e.yyx).d +e.yxy * sdScene(p + e.yxy).d +e.xxx * sdScene(p + e.xxx).d);
}SDFResult rayMarch(vec3 ro, vec3 rd,float start,float end)
{float d=start;float r=1.0;SDFResult result;for(int i=0;i<MAX_STEPS;i++){vec3 p=ro+rd*d;result=sdScene(p);d+=result.d;if(result.d<EPSILON || d>end) break;}result.d=d;return result;
}
float shadowRay(vec3 ro, vec3 rd) {// ro: 射线起点(表面点)// rd: 射线方向(指向光源)float t = MIN_DIST;float res = 1.0; // 1.0表示完全照亮,0.0表示完全阴影for(int i = 0; i < MAX_STEPS; i++) {vec3 p = ro + rd * t;float h = sdScene(p).d; // 场景SDF函数if(h < EPSILON) {return 0.0; // 完全阴影}t += h;if(t >= MAX_DIST) break;}return res;
}void mainImage( out vec4 fragColor, in vec2 fragCoord )
{// Normalized pixel coordinates (from -1 to 1)//vec2 uv = (2.0*fragCoord-iResolution.xy)/iResolution.xx;vec2 uv = (fragCoord-0.5*iResolution.xy)/iResolution.y;//vec3 backgroundColor = vec3(0.835, 1, 1);vec3 backgroundColor = mix(vec3(1, .341, .2), vec3(0, 1, 1), uv.y) * 1.6;//vec3 c=getBackgroundColor(uv);vec3 c=backgroundColor;vec3 lp=vec3(0,0.,0.);vec3 ro = vec3(0, 0, 3); // ray origin that represents camera position//float theta=iTime*0.5;//float cameraRadius=10.;//ro.x=cameraRadius*cos(theta)+lp.x;//ro.z=cameraRadius*sin(theta)+lp.z;vec3 rd = camera(ro,lp,vec3(0,1,0))*normalize(vec3(uv, -1)); // ray directionSDFResult result=rayMarch(ro,rd,START_DIST,MAX_DIST);float d=result.d;if(d<MAX_DIST){vec3 p=ro+rd*d;vec3 n=calcNormal(p);//vec3 lightPosition=vec3(2,2,7);//vec3 lightPosition1=vec3(-8,-6,-5);vec3 lightPosition1 = vec3(cos(iTime), 2, sin(iTime));vec3 lightDirection1 = normalize(lightPosition1 - p);c=phongLighting(lightDirection1,n,rd,normalize(ro-p),result.mat);// vec3 lightPosition2=vec3(1,1,1);// c+=phongLighting(normalize(lightPosition2-p),n,rd,normalize(ro-p),result.mat);vec3 newOrigin=p+n*0.01; float shadow = shadowRay(newOrigin,lightDirection1);c*=shadow;}// Output to screenfragColor = vec4(vec3(c),1.0);
}
1.4. Gamma 校正
Gamma校正是图像处理中的一种技术,用于调整图像的亮度,使其更符合人眼的感知特性。人眼对亮度的感知是非线性的,对暗部细节更敏感,而对亮部细节不敏感。Gamma校正通过应用一个非线性函数,将图像的亮度调整为更适合人眼观看的效果。
Gamma校正公式
Gamma校正通常使用以下公式:
I c o r r e c t e d = I o r i g i n a l 1 γ I_{corrected} = I_{original}^{\frac{1}{\gamma}} Icorrected=Ioriginalγ1
( I o r i g i n a l ) 是原始图像的像素值(通常在 [ 0 , 1 ] 范围内)。 ( γ ) 是 G a m m a 值,通常大于 1 。 ( I c o r r e c t e d ) 是校正后的像素值。 ( I_{original} ) 是原始图像的像素值(通常在 [0, 1] 范围内)。 \\ ( \gamma ) 是 Gamma 值,通常大于 1。 \\ ( I_{corrected} ) 是校正后的像素值。 (Ioriginal)是原始图像的像素值(通常在[0,1]范围内)。(γ)是Gamma值,通常大于1。(Icorrected)是校正后的像素值。
// 假设 gamma = 2.2
vec3 gammaCorrectedColor = pow(color, vec3(1.0 / 2.2));
fragColor = vec4(gammaCorrectedColor, 1.0);
1.5. 非全黑阴影
之前的阴影效果是全黑的。我们可以使用一个简单的技巧来改善阴影效果,使其不那么黑。如下图所示,我们可以将阴影的亮度设置为 0.5,而不是全黑。这样可以使阴影看起来更加自然,同时保持阴影的深度感。
代码实现
float shadow = shadowRay(newOrigin,lightDirection1);
if (shadow < 1.) {//c = mix(c, vec3(0.0), 0.5); // Apply shadow effectc *=0.5;
}
else
{c*=shadow;
}
1.6. Soft Shadows 柔和阴影
b1a6597 (第八章:阴影)
阴影有多个部份,包括
- 本影
- 半影
详见 Umbra, penumbra and antumbra - Wikipedia
柔和阴影 (Soft Shadows) 是一种更接近现实的阴影效果,其特点是阴影边缘具有渐变过渡,而不是硬边界。这种效果模拟了光源的大小和形状对阴影的影响。
代码实现
float softShadow(vec3 ro, vec3 rd, float mint, float tmax) {float res = 1.0;float t = mint;for(int i = 0; i < 16; i++) {float h = sdScene(ro + rd * t).d;res = min(res, 8.0*h/t);t += clamp(h, 0.02, 0.10);if(h < 0.001 || t > tmax) break;}return clamp( res, 0.0, 1.0 );
}
关键点解释
阴影强度计算
- 8.0*h/t: 这是核心公式
- h: 当前点到场景的距离
- t: 当前射线行进的总距离
- 8.0: 软阴影的柔和度系数
步进距离控制
clamp(h, 0.02, 0.10): 限制每次步进的距离
软阴影效果
- 当物体完全遮挡时,h接近0,res趋近0(完全阴影)
- 当物体部分遮挡时,根据距离比例产生渐变的阴影效果
- 最终通过clamp(res, 0.0, 1.0)确保值在[0,1]范围内
与硬阴影相比,软阴影能产生更自然的渐变效果,使场景看起来更真实
完整代码见 github
1.7. fog
远处的地板背面看起来有点奇怪,我们可以通过应用雾来隐藏背景的任何不规则性
在 Shader 中,雾 (Fog) 是一种常见的视觉效果,用于模拟远处物体因大气散射或吸收而变得模糊或颜色变化的现象。它可以增强场景的深度感和真实感,同时隐藏远处的渲染瑕疵。
雾的原理
雾的效果基于以下两个核心原理:
- 距离衰减:
- 随着物体距离观察者的距离增加,雾的影响逐渐增强。
- 近处物体几乎不受雾的影响,而远处物体会被雾覆盖。
- 颜色混合:
- 物体的颜色会与雾的颜色进行混合,最终呈现出一种过渡效果。
- 混合比例由物体到观察者的距离决定。
雾的颜色计算公式通常如下:
c o l o r = m i x ( c o l o r , f o g C o l o r , f o g F a c t o r ) color = mix(color, fogColor, fogFactor) color=mix(color,fogColor,fogFactor)
fogFactor :雾的影响因子,通常是物体距离 ( d ) 的函数,通常使用以下公式:
雾的类型 | 影响因子公式 | 描述 |
---|---|---|
线性雾 (Linear Fog) | f = (d_max - z) / (d_max - d_min) | 雾浓度在 d_min 和 d_max 之间线性变化,z 是物体到相机的距离。 |
指数雾 (Exponential Fog) | f = exp(-density * z) | 雾浓度随距离指数衰减,density 控制衰减速度。 |
反指数雾 (Inverse Exponential Fog) | f = 1.0 - exp(-density * z) | 雾浓度随距离递增,适合某些特殊效果。 |
指数平方雾 (Exponential Squared Fog) | f = exp(-density * z * z) | 雾浓度随距离平方指数衰减,比普通指数雾衰减更快。 |
平滑混合雾 | f = smoothstep(d_min, d_max, z) | 在 d_min 和 d_max 之间使用平滑插值过渡。 |
距离平方雾 | f = (z / d_max)^2 | 雾浓度与距离平方成正比,远距离物体快速被雾覆盖。 |
自定义曲线雾 | f = pow(z / d_max, exponent) | 使用任意幂次控制雾的衰减曲线。 |
代码实现
c = mix(c, backgroundColor, 1.0 - exp(-0.0002 * d * d * d)); // fog
1.8. 参考
- 13.1 阴影 | Shadertoy中文教程
- Gamma correction - Wikipedia
- Umbra, penumbra and antumbra - Wikipedia
- Raymarching Primitives Commented
- 本章所有源代码及示例图