RayMarching贴图

1,452 阅读30分钟

源码:github.com/buglas/shad…

接下来我想在渲染模型的时候,通过环境贴图给它一个环境光,所以咱们先说一下贴图。

知识点

  • 长方体贴图
  • 球体贴图
  • 立方体环境光
  • 球体环境光

1-贴图的显示

我们先在坐标系里显示一堆贴图看一下。

image-20230924121609097

整体代码如下:

// 贴图
#iChannel0 "file://images/erha.jpg"

// Wrap方式:Clamp Repeat Mirror
#iChannel0::WrapMode "Repeat"

// 采样方式:Nearest Linear NearestMipMapNearest
#iChannel0::MinFilter "NearestMipMapNearest"
#iChannel0::MagFilter "Nearest"


// 坐标系
vec2 Coord(in vec2 coord, in float scale) {
  return scale * 2. * (coord - 0.5 * iResolution.xy) / min(iResolution.x, iResolution.y);
}

// 坐标轴
vec4 AxisHelper(in vec2 coord, in float axisWidth, in vec4 xAxisColor, in vec4 yAxisColor) {
  vec4 color = vec4(0, 0, 0, 0);
  float dx = dFdx(coord.x) * axisWidth;
  float dy = dFdy(coord.y) * axisWidth;
  if(abs(coord.x) < dx) {
    color = yAxisColor;
  } else if(abs(coord.y) < dy) {
    color = xAxisColor;
  }
  return color;
}

// 栅格
vec4 GridHelper(in vec2 coord, in float gridWidth, in vec4 gridColor) {
  vec4 color = vec4(0, 0, 0, 0);
  float dx = dFdx(coord.x) * gridWidth;
  float dy = dFdy(coord.y) * gridWidth;
  vec2 fraction = fract(coord);
  if(fraction.x < dx || fraction.y < dy) {
    color = gridColor;
  }
  return color;
}

// 坐标系辅助对象
vec4 CoordHelper(in vec2 coord, in float axisWidth, in vec4 xAxisColor, in vec4 yAxisColor, in float gridWidth, in vec4 gridColor) {
  // 坐标轴
  vec4 axisHelper = AxisHelper(coord, axisWidth, xAxisColor, yAxisColor);
  // 栅格
  vec4 gridHelper = GridHelper(coord, gridWidth, gridColor);
  // =坐标系
  return bool(axisHelper.a) ? axisHelper : gridHelper;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
  // 坐标
  vec2 coord = Coord(fragCoord, 2.);
  // 背景色
  vec4 backgroundColor = vec4(0, 0, 0, 1);
  // 坐标系辅助对象
  vec4 coordHelper = CoordHelper(coord, 2., vec4(0, 1, 0, 1), vec4(1, 0, 0, 1), 1., vec4(1));
  // texture(iChannel0, uv).rgb
  
  // 最终的颜色
  fragColor = backgroundColor + coordHelper+texture(iChannel0, coord).rgba;
}

解释一下上面的代码。

在ShaderToy中,可以通过以下方式加载贴图:

// 贴图
#iChannel0 "file://images/erha.jpg"

// Wrap方式:Clamp Repeat 
#iChannel0::WrapMode "Repeat"

// 采样方式:Nearest Linear NearestMipMapNearest
#iChannel0::MinFilter "NearestMipMapNearest"
#iChannel0::MagFilter "Nearest"

加载完贴图后,可以以此方式调用贴图:

texture(iChannel0, coord).rgba

接下来我们通过立方体和球体,说一下如何在三维物体上贴图。

2-在立方体上贴图

1.先准备一个立方体。

// 贴图
#iChannel0 "file://images/erha.jpg"

// Wrap方式:Clamp Repeat  Mirror
#iChannel0::WrapMode "Repeat"

// 采样方式:Nearest Linear NearestMipMapNearest
#iChannel0::MinFilter "NearestMipMapNearest"
#iChannel0::MagFilter "Nearest"


// 坐标系缩放
#define PROJECTION_SCALE  1.

// 长方体的中心位置
#define RECT_POS vec3(0.5)
// 长方体的尺寸
#define RECT_SIZE vec3(0.5)
// 长方体的漫反射系数
#define RECT_KD vec3(1)

// 相机视点位
#define CAMERA_POS mat3(cos(iTime),0,sin(iTime),0,1,0,-sin(iTime),0,cos(iTime))*(vec3(1, 1.5, 2.5)-RECT_POS)+RECT_POS

// 相机目标点
#define CAMERA_TARGET vec3(0.5)
// 上方向
#define CAMERA_UP vec3(0, 1, 0)

// 光线推进的起始距离 
#define RAYMARCH_NEAR 0.1
// 光线推进的最远距离
#define RAYMARCH_FAR 128.
// 光线推进次数
#define RAYMARCH_TIME 40
// 当推进后的点位距离物体表面小于RAYMARCH_PRECISION时,默认此点为物体表面的点
#define RAYMARCH_PRECISION 0.001

// 点光源位置
#define LIGHT_POS vec3(1, 4, 1)

// 相邻点的抗锯齿的行列数
#define AA 3

// 坐标系
vec2 Coord(in vec2 coord) {
  return PROJECTION_SCALE * 2. * (coord - 0.5 * iResolution.xy) / min(iResolution.x, iResolution.y);
}

// 长方体的的SDF模型
float SDFRect(vec3 coord) {
  vec3 d = abs(coord - RECT_POS) - RECT_SIZE;
  return length(max(d, 0.)) + min(max(d.x, max(d.y, d.z)), 0.);
}


// 视图旋转矩阵
mat3 RotateMatrix() {
  //基向量c,视线
  vec3 c = normalize(CAMERA_POS - CAMERA_TARGET);
  //基向量a,视线和上方向的垂线
  vec3 a = cross(CAMERA_UP, c);
  //基向量b,修正上方向
  vec3 b = cross(c, a);
  //正交旋转矩阵
  return mat3(a, b, c);
}

// 计算长方体的法线
vec3 SDFNormal(in vec3 p) {
  const float h = 0.0001;
  const vec2 k = vec2(1, -1);
  return normalize(k.xyy * SDFRect(p + k.xyy * h) +
    k.yyx * SDFRect(p + k.yyx * h) +
    k.yxy * SDFRect(p + k.yxy * h) +
    k.xxx * SDFRect(p + k.xxx * h));
}

// 获取纹理
vec3 getTexture(vec3 p,vec3 n){
  vec3 colorZ = texture(iChannel0, p.xy).rgb*n.z;
  vec3 colorY = texture(iChannel0, p.xz).rgb*n.y;
  vec3 colorX = texture(iChannel0, -p.zy).rgb*n.x;
  return colorZ+colorY+colorX;
}

// 光线推进
vec3 RayMarch(vec2 coord) {
  float d = RAYMARCH_NEAR;
  // 从相机视点到当前片元的射线
  vec3 rd = normalize(RotateMatrix() * vec3(coord, -1));
  // 片元颜色
  vec3 color = vec3(0);
  for(int i = 0; i < RAYMARCH_TIME && d < RAYMARCH_FAR; i++) {
    // 光线推进后的点位
    vec3 p = CAMERA_POS + d * rd;
    // 法线 
    vec3 n=SDFNormal(p);
    // 光线推进后的点位到长方体的有向距离
    float curD = SDFRect(p);
    // 若有向距离小于一定的精度,默认此点在长方体表面
    if(curD < RAYMARCH_PRECISION) {
      color=vec3(1);
      break;
    }
    // 距离累加
    d += curD;
  }
  return color;
}

// 抗锯齿 Anti-Aliasing
vec3 RayMarch_anti(vec2 fragCoord) {
  // 初始颜色
  vec3 color = vec3(0);
  // 行列的一半
  float aa2 = float(AA / 2);
  // 逐行列遍历
  for(int y = 0; y < AA; y++) {
    for(int x = 0; x < AA; x++) {
      // 基于像素的偏移距离
      vec2 offset = vec2(float(x), float(y)) / float(AA) - aa2;
      // 坐标位
      vec2 coord = Coord(fragCoord + offset);
      // 累加周围片元的颜色
      color += RayMarch(coord);
    }
  }
  // 返回周围颜色的均值
  return color / float(AA * AA);
}

/* 绘图函数,画布中的每个片元都会执行一次,执行方式是并行的。
fragColor 输出参数,用于定义当前片元的颜色。
fragCoord 输入参数,当前片元的位置,原点在画布左下角,右侧边界为画布的像素宽,顶部边界为画布的像素高
*/
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
  // 光线推进
  vec3 color = RayMarch_anti(fragCoord);
  // 最终颜色
  fragColor = vec4(color, 1);
}

效果如下:

image-20230924122230239

当前的立方体并没有光照,这只是为了简化代码。

这个立方体的位置和尺寸都是与uv坐标相匹配的,即其左后点在零点上,宽高深都是1.

image-20230924171344451

2.建立一个获取纹理数据的方法。

// 获取纹理
vec3 getTexture(vec3 p,vec3 n){
  return texture(iChannel0, p.xy).rgb;
}
// 光线推进
vec3 RayMarch(vec2 coord) {
  float d = RAYMARCH_NEAR;
  // 从相机视点到当前片元的射线
  vec3 rd = normalize(RotateMatrix() * vec3(coord, -1));
  // 片元颜色
  vec3 color = vec3(0);
  for(int i = 0; i < RAYMARCH_TIME && d < RAYMARCH_FAR; i++) {
    // 光线推进后的点位
    vec3 p = CAMERA_POS + d * rd;
    // 法线 
    vec3 n=SDFNormal(p);
    // 光线推进后的点位到长方体的有向距离
    float curD = SDFRect(p);
    // 若有向距离小于一定的精度,默认此点在长方体表面
    if(curD < RAYMARCH_PRECISION) {
      color=getTexture(p,n);
      break;
    }
    // 距离累加
    d += curD;
  }
  return color;
}

效果如下:

image-20230924124407886

当前的立方体只有前后两个面可以正常显示纹理。

3.我可以利用法线,只显示立方体的前后两个面看看。

将之前的Texture数据乘以当前着色点的法线绝对值的z值:

vec3 getTexture(vec3 p,vec3 n){
  vec3 absN=abs(n);
  return texture(iChannel0, p.xy).rgb*absN.z;
}

效果如下:

image-20230924125059680

出现这个效果是因为立方体上、下、左、右的4个面的法线的z值都是0,那一个颜色乘以0后的值还是0。

4.同理,绘制上、下、左、右的4个面的贴图。

vec3 getTexture(vec3 p,vec3 n){
  vec3 absN=abs(n);
  vec3 colorZ = texture(iChannel0, p.xy*2.).rgb*absN.z;
  vec3 colorY = texture(iChannel0, p.xz).rgb*absN.y;
  vec3 colorX = texture(iChannel0, -p.zy).rgb*absN.x;
  return colorZ+colorY+colorX;
}

效果如下:

image-20230924130424054

细心的同学会发现,右侧X轴向的小狗是竖着的。

你可以将计算colorX时的yz坐标转置一下:

vec3 colorX = texture(iChannel0, p.zy).rgb*absN.x;

这样,右侧的小狗就横过来了。

image-20230924164128362

以此原理,调整p点,你也可以让小狗倒过来,或者重复多个。

vec3 getTexture(vec3 p,vec3 n){
  vec3 colorZ = texture(iChannel0, p.xy*2.).rgb*absN.z;
  vec3 colorY = texture(iChannel0, p.xz).rgb*absN.y;
  vec3 colorX = texture(iChannel0, -p.zy).rgb*absN.x;
  return colorZ+colorY+colorX;
}

效果如下:

image-20230924171815298

关于立方体贴图的基本原理咱们就说到这,接下来我们可以以同样的原理在球体上贴图。

3-在球体上贴图

我们先参考立方体 的代码绘制一个球体。

// 贴图
#iChannel0 "file://images/erha.jpg"

// Wrap方式:Clamp Repeat  Mirror
#iChannel0::WrapMode "Repeat"

// 采样方式:Nearest Linear NearestMipMapNearest
#iChannel0::MinFilter "NearestMipMapNearest"
#iChannel0::MagFilter "Nearest"


// 坐标系缩放
#define PROJECTION_SCALE  1.

// 球体的球心位置
#define SPHERE_POS vec3(0.5)
// 球体的半径
#define SPHERE_R 0.5

// 相机视点位
#define CAMERA_POS mat3(cos(iTime),0,sin(iTime),0,1,0,-sin(iTime),0,cos(iTime))*(vec3(0, 1, 2)-SPHERE_POS)+SPHERE_POS

// 相机目标点
#define CAMERA_TARGET vec3(0.5)
// 上方向
#define CAMERA_UP vec3(0, 1, 0)

// 光线推进的起始距离 
#define RAYMARCH_NEAR 0.1
// 光线推进的最远距离
#define RAYMARCH_FAR 128.
// 光线推进次数
#define RAYMARCH_TIME 40
// 当推进后的点位距离物体表面小于RAYMARCH_PRECISION时,默认此点为物体表面的点
#define RAYMARCH_PRECISION 0.001

// 点光源位置
#define LIGHT_POS vec3(1, 4, 1)

// 相邻点的抗锯齿的行列数
#define AA 3


// 坐标系
vec2 Coord(in vec2 coord) {
  return PROJECTION_SCALE * 2. * (coord - 0.5 * iResolution.xy) / min(iResolution.x, iResolution.y);
}

//球体的SDF模型
float SDFSphere(vec3 coord) {
  return length(coord - SPHERE_POS) - SPHERE_R;
}


// 视图旋转矩阵
mat3 RotateMatrix() {
  //基向量c,视线
  vec3 c = normalize(CAMERA_POS - CAMERA_TARGET);
  //基向量a,视线和上方向的垂线
  vec3 a = cross(CAMERA_UP, c);
  //基向量b,修正上方向
  vec3 b = cross(c, a);
  //正交旋转矩阵
  return mat3(a, b, c);
}

// 计算长方体的法线
vec3 SDFNormal(in vec3 p) {
  const float h = 0.0001;
  const vec2 k = vec2(1, -1);
  return normalize(k.xyy * SDFSphere(p + k.xyy * h) +
    k.yyx * SDFSphere(p + k.yyx * h) +
    k.yxy * SDFSphere(p + k.yxy * h) +
    k.xxx * SDFSphere(p + k.xxx * h));
}

// 获取纹理
vec3 getTexture(vec3 p,vec3 n){
  vec3 absN=abs(n);
  vec3 colorZ = texture(iChannel0, p.xy*2.).rgb*absN.z;
  vec3 colorY = texture(iChannel0, p.xz).rgb*absN.y;
  vec3 colorX = texture(iChannel0, -p.zy).rgb*absN.x;
  return colorZ+colorY+colorX;
}

// 光线推进
vec3 RayMarch(vec2 coord) {
  float d = RAYMARCH_NEAR;
  // 从相机视点到当前片元的射线
  vec3 rd = normalize(RotateMatrix() * vec3(coord, -1));
  // 片元颜色
  vec3 color = vec3(0);
  for(int i = 0; i < RAYMARCH_TIME && d < RAYMARCH_FAR; i++) {
    // 光线推进后的点位
    vec3 p = CAMERA_POS + d * rd;
    // 法线 
    vec3 n=SDFNormal(p);
    // 光线推进后的点位到长方体的有向距离
    float curD = SDFSphere(p);
    // 若有向距离小于一定的精度,默认此点在长方体表面
    if(curD < RAYMARCH_PRECISION) {
      color=getTexture(p,n);
      break;
    }
    // 距离累加
    d += curD;
  }
  return color;
}

// 抗锯齿 Anti-Aliasing
vec3 RayMarch_anti(vec2 fragCoord) {
  // 初始颜色
  vec3 color = vec3(0);
  // 行列的一半
  float aa2 = float(AA / 2);
  // 逐行列遍历
  for(int y = 0; y < AA; y++) {
    for(int x = 0; x < AA; x++) {
      // 基于像素的偏移距离
      vec2 offset = vec2(float(x), float(y)) / float(AA) - aa2;
      // 坐标位
      vec2 coord = Coord(fragCoord + offset);
      // 累加周围片元的颜色
      color += RayMarch(coord);
    }
  }
  // 返回周围颜色的均值
  return color / float(AA * AA);
}

/* 绘图函数,画布中的每个片元都会执行一次,执行方式是并行的。
fragColor 输出参数,用于定义当前片元的颜色。
fragCoord 输入参数,当前片元的位置,原点在画布左下角,右侧边界为画布的像素宽,顶部边界为画布的像素高
*/
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
  // 光线推进
  vec3 color = RayMarch_anti(fragCoord);
  // 最终颜色
  fragColor = vec4(color, 1);
}

效果如下:

image-20230925160537351

现在的效果看起来有点曝,这是因为我们无法像之前的立方体那样,单纯的依靠着色点的法线的某个分量判断此着色点是否渲染。

我们可以参考立方体,将球体也分成相等的六个面。

3-1-将立方体投影在球体上

想象一个场景:

image-20230924185916617

球体中央有一个半透明的立方体,立方体中央有一个点光源,点光源可以把半透明的立方体投影到球体上。

那立方体的6个面在球体上的投影就可以把球体的表面均分成6份。

通过这种投影原理,我们就可以把一张矩形的纹理贴图映射到球体的每个面上。

例1

image-20230924194543561

已知:

  • 着色点P的法线为n
  • 球体半径为r

求:判断着色点P是否在上图中的红色圆弧上

思路:

这就是一个三角函数问题,判断法线n 的z值是否大于上图线段a 的长度即可。

解:

由三角函数可得:

a²+a²=r²

所以:

a=sqrt(r²/2)

所以:

当n.z>sqrt(r²/2)的时候,点P在红色圆弧上。

现在最核心的点已经通了,我们可以再将这个问题延伸到三维球体上去。

例2

image-20230925094132081

已知:

  • 例1的图是球体中间的水平截面
  • P点是球体上任意点

求:判断着色点P是否在上图中由红色的线连成的面上

思路:

红线连成的面是一种花瓣的形状。

image-20230925154826197

水平截面上的半径r 是动态变化的,水平截面越趋向上下两端r 越小。

解:

根据点P的x、z 位置算出P点所在的水平截面的半径r:

r=sqrt(x²+z²)

根据例1是思路,判断点P是否在当前截面圆的红线上:

a=sqrt(r²/2)

当n.z>sqrt(r²/2)的时候,点P在红色的线连成的面上。

以此原理,我们就可以判断出点P是在球体的上、下、左、右那个方向上。

但是,我们是要把圆等分出6个面来的,所以我们需要把属于上下两个面的部分过滤掉。

我们将刚才的那片花瓣绕z轴旋转90°后求交即可。交出来的中间那个面就是等分球体的6个面之一。

image-20230925154557163

至于这旋转90°的花瓣是怎么求的,大家参考第一片花瓣来画即可。

第一片花瓣的截面圆与y轴垂直,绕z轴旋转90°的花瓣的截面圆则是与x轴垂直。

其截面圆的r 值是sqrt(z²+y²),注意不要把z²写成x²,这样就会变成与z轴垂直的截面圆了。

基本原理咱们就说到这,接下来上代码。

3-2-代码实现

1.先把例1中前后方向的花瓣画出来。

vec3 getTexture(vec3 p,vec3 n){
  vec3 absN=abs(n);
  float a1=sqrt(pow(length(n.xz),2.)/2.);
  float z=absN.z>a1?1.:0.;
  return texture(iChannel0, p.xy).rgb*z;
}

效果如下:

image-20230925162221828

2.参考例2,把当前的花瓣过滤掉上下两个面的部分。

vec3 getTexture(vec3 p,vec3 n){
  vec3 absN=abs(n);
  float a1=sqrt(pow(length(n.xz),2.)/2.);
  float a2=sqrt(pow(length(n.yz),2.)/2.);
  float z=absN.z>a1&&absN.z>a2?1.:0.;
  return texture(iChannel0, p.xy).rgb*z;
}

效果如下:

image-20230925162555544

3.同理类推,把其它的几个面也画上。

vec3 getTexture(vec3 p,vec3 n){
  vec3 absN=abs(n);
    
  float a1=sqrt(pow(length(n.xz),2.)/2.);
  float a2=sqrt(pow(length(n.yz),2.)/2.);
  float a3=sqrt(pow(length(n.xy),2.)/2.);

  float z=absN.z>a1&&absN.z>a2?1.:0.;
  float y=absN.y>a3&&absN.y>a2?1.:0.;
  float x=absN.x>a1&&absN.x>a3?1.:0.;

  vec3 colorZ = texture(iChannel0, p.xy).rgb*z;
  vec3 colorY = texture(iChannel0, p.xz).rgb*y;
  vec3 colorX = texture(iChannel0, p.zy).rgb*x;

  return colorZ+colorX+colorY;
}

效果如下:

image-20230925164655962

这效果有点像地狱三头犬。接下来咱们思考一下当前的贴图和球体上每个面的映射关系。

3-3-贴图与球面的映射关系

当前的贴图实际上并没有完全的映射到球体的每一个面上。

image-20230925181848667

我换一张比较规则的贴看一下效果。

// #iChannel0 "file://images/erha.jpg"
#iChannel0 "file://https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/708fe531f61547e6882a913e38ae1e75~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=500&h=500&s=56593&e=jpg&b=ffffff"

贴图效果如下:

tile

渲染效果如下:

image-20230926082850128

由此可见,贴图里的橙色部分并没有显示出来。

若我想把一张贴图完全贴合到球体的每个面上应该怎么办呢?如下图所示:

image-20230926083842139

其实我们可以通过球体的法线与uv的映射关系来实现这种效果。

我们可以分别将每个球面上的点映射到uv坐标的(0,1)之间。

image-20230924194543561

代码如下:

// 线性插值
vec2 liner(vec2 vmin,vec2 vmax,vec2 v){
  return (v-vmin)/(vmax-vmin);
}

// 获取纹理
vec3 getTexture(vec3 n){
  vec3 absN=abs(n);

  //3个方向上的a值 
  float a1=sqrt(pow(length(n.xz),2.)/2.);
  float a2=sqrt(pow(length(n.yz),2.)/2.);
  float a3=sqrt(pow(length(n.xy),2.)/2.);

  float z=absN.z>a1&&absN.z>a2?1.:0.;
  float y=absN.y>a3&&absN.y>a2?1.:0.;
  float x=absN.x>a1&&absN.x>a3?1.:0.;

  // xy面(前后的面)、xz面(上下的面)、zy面(左右的面)上的采样点
  vec2 p_xy= liner(vec2(-a1,-a2),vec2(a1,a2),n.xy); 
  vec2 p_xz= liner(vec2(-a3,-a2),vec2(a3,a2),n.xz); 
  vec2 p_zy= liner(vec2(-a1,-a3),vec2(a1,a3),n.zy); 

  vec3 colorZ = texture(iChannel0, p_xy).rgb*z;
  vec3 colorY = texture(iChannel0, p_xz).rgb*y;
  vec3 colorX = texture(iChannel0, p_zy).rgb*x;

  return colorZ+colorY+colorX;
}

// 光线推进
vec3 RayMarch(vec2 coord) {
  float d = RAYMARCH_NEAR;
  // 从相机视点到当前片元的射线
  vec3 rd = normalize(RotateMatrix() * vec3(coord, -1));
  // 片元颜色
  vec3 color = vec3(0);
  for(int i = 0; i < RAYMARCH_TIME && d < RAYMARCH_FAR; i++) {
    // 光线推进后的点位
    vec3 p = CAMERA_POS + d * rd;
    // 法线绝对值
    vec3 n=SDFNormal(p);
    // 光线推进后的点位到长方体的有向距离
    float curD = SDFSphere(p);
    // 若有向距离小于一定的精度,默认此点在长方体表面
    if(curD < RAYMARCH_PRECISION) {
      color=getTexture(n);
      break;
    }
    // 距离累加
    d += curD;
  }
  return color;
}

liner() 是一个线性插值方法,用来计算一个点在两点之间的百分比。

球体法线与uv 的映射关系如下:

  • 前后球面

    • n.x 对应xz截面上的a1
    • n.y 对应zy截面上的a2
  • 左右球面

    • n.z对应xz截面上的a1
    • n.y 对应xy截面上的a3
  • 上下球面

    • n.x对应xy截面上的a3
    • n.z 对应zy截面上的a2

整体有点绕,若不理解可以微信我。

对于球体的贴图,还有一个比较快捷的方式,咱们看一下。

3-4-球体的另一种贴图方式

若对球体的贴图没啥太苛刻的要求,我们可以这样贴图。

vec3 getTexture(vec3 p,vec3 n){
  vec3 absN=abs(n);
  absN=pow(absN,vec3(10.));
  absN/=absN.x+absN.y+absN.z;

  vec3 colorZ = texture(iChannel0, p.xy).rgb*absN.z;
  vec3 colorY = texture(iChannel0, p.xz).rgb*absN.y;
  vec3 colorX = texture(iChannel0, p.zy).rgb*absN.x;

  return colorZ+colorY+colorX;
}

效果如下:

image-20230926121240654

这种方式适合砂石之类的不规则肌理效果。

image-20230926121729111

4-环境光

环境光为场景的渲染提供丰富的光能,其贴图可以是立方体的6个面,也可以是等距圆柱投影贴图,其模型可以是立方体,也可以是球体。

接下来咱们就说一下如何把立方体的6个面贴图贴到立方体和球体上,从而制作环境光。

image-20230926123810038

4-1-立方体环境光

4-1-1-立方体外部贴图

我们可以把提前制作好的立方体贴图依次贴到立方体的6个面上。效果如下:

image-20230928080250381

代码如下:

// 获取纹理
vec3 getTexture(vec3 p,vec3 n){
  vec3 absN=abs(n);
  vec4 textureZ=n.z>0.? texture(iChannel5, p.xy): texture(iChannel4, vec2(-p.x,p.y));
  vec4 textureY=n.y>0.? texture(iChannel2, vec2(-p.x,p.z)): texture(iChannel3,-p.xz);
  vec4 textureX=n.x>0.? texture(iChannel1,  vec2(-p.z,p.y)): texture(iChannel0, p.zy);
  vec3 colorZ = textureZ.rgb*absN.z;
  vec3 colorY = textureY.rgb*absN.y;
  vec3 colorX = textureX.rgb*absN.x;
  return colorZ+colorY+colorX;
}

// 光线推进
vec3 RayMarch(vec2 coord) {
  float d = RAYMARCH_NEAR;
  // 从相机视点到当前片元的射线
  vec3 rd = normalize(RotateMatrix() * vec3(coord, -1));
  // 片元颜色
  vec3 color = vec3(0);
  for(int i = 0; i < RAYMARCH_TIME && d < RAYMARCH_FAR; i++) {
    // 光线推进后的点位
    vec3 p = CAMERA_POS + d * rd;
    // 法线绝对值
    vec3 n=SDFNormal(p);
    // 光线推进后的点位到长方体的有向距离
    float curD = SDFRect(p);
    // 若有向距离小于一定的精度,默认此点在长方体表面
    if(curD < RAYMARCH_PRECISION) {
      color=getTexture(p,n);
      break;
    }
    // 距离累加
    d += curD;
  }
  return color;
}

其原理很简单,就是根据法线方向,识别出立方体的6个面,然后为每个面贴上相应的贴图。

以此原理,我们把相机打到立方体内部,那就是环境光了。

4-1-1-立方体内部贴图

立方体内部贴图效果如下:

1

整体代码如下:

// 贴图 [ 'posx', 'negx', 'posy', 'negy', 'posz', 'negz' ].
#iChannel0 "file://images/bridge/posx.jpg"
#iChannel1 "file://images/bridge/negx.jpg"
#iChannel2 "file://images/bridge/posy.jpg"
#iChannel3 "file://images/bridge/negy.jpg"
#iChannel4 "file://images/bridge/posz.jpg"
#iChannel5 "file://images/bridge/negz.jpg"

// Wrap方式:Clamp Repeat  Mirror
#iChannel0::WrapMode "Repeat"

// 采样方式:Nearest Linear NearestMipMapNearest
#iChannel0::MinFilter "NearestMipMapNearest"
#iChannel0::MagFilter "Nearest"


// 坐标系缩放
#define PROJECTION_SCALE  1.

// 长方体的中心位置
#define RECT_POS vec3(0)
// 长方体的尺寸,此数值为宽高深的一半
#define RECT_SIZE vec3(3)
// 长方体的漫反射系数
#define RECT_KD vec3(1)

// 相机视点位
#define CAMERA_POS mat3(cos(iTime),0,sin(iTime),0,1,0,-sin(iTime),0,cos(iTime))*(vec3(0, 0, 2)-RECT_POS)+RECT_POS
// #define CAMERA_POS vec3(0, 0, 1)

// 相机目标点
#define CAMERA_TARGET vec3(0)
// 上方向
#define CAMERA_UP vec3(0, 1, 0)

// 光线推进的起始距离 
#define RAYMARCH_NEAR 0.1
// 光线推进的最远距离
#define RAYMARCH_FAR 128.
// 光线推进次数
#define RAYMARCH_TIME 40
// 当推进后的点位距离物体表面小于RAYMARCH_PRECISION时,默认此点为物体表面的点
#define RAYMARCH_PRECISION 0.001

// 相邻点的抗锯齿的行列数
#define AA 3

// 坐标系
vec2 Coord(in vec2 coord) {
  return PROJECTION_SCALE * 2. * (coord - 0.5 * iResolution.xy) / min(iResolution.x, iResolution.y);
}

// 长方体的的SDF模型
/* float SDFRect(vec3 coord) {
  vec3 d = abs(coord - RECT_POS) - RECT_SIZE;
  return length(max(d, 0.)) + min(max(d.x, max(d.y, d.z)), 0.);
} */

// 长方体的的SDF模型-法线翻转
float SDFRect(vec3 coord) {
  vec3 d = RECT_SIZE-abs(coord - RECT_POS) ;
  return min(min(d.x,d.y),d.z);
}


// 视图旋转矩阵
mat3 RotateMatrix() {
  //基向量c,视线
  vec3 c = normalize(CAMERA_POS - CAMERA_TARGET);
  //基向量a,视线和上方向的垂线
  vec3 a = cross(CAMERA_UP, c);
  //基向量b,修正上方向
  vec3 b = cross(c, a);
  //正交旋转矩阵
  return mat3(a, b, c);
}

// 计算长方体的法线
vec3 SDFNormal(in vec3 p) {
  const float h = 0.0001;
  const vec2 k = vec2(1, -1);
  return normalize(k.xyy * SDFRect(p + k.xyy * h) +
    k.yyx * SDFRect(p + k.yyx * h) +
    k.yxy * SDFRect(p + k.yxy * h) +
    k.xxx * SDFRect(p + k.xxx * h));
}

// 获取纹理
vec3 getTexture(vec3 p,vec3 n){
  vec3 absN=abs(n);
  p=(p/RECT_SIZE)/2.+0.5;
  vec4 textureZ=n.z>0.? texture(iChannel5, p.xy): texture(iChannel4, vec2(-p.x,p.y));
  vec4 textureY=n.y>0.? texture(iChannel3, vec2(-p.x,p.z)): texture(iChannel2,-p.xz);
  vec4 textureX=n.x>0.? texture(iChannel0,  vec2(-p.z,p.y)): texture(iChannel1, vec2(p.z,p.y));
  vec3 colorZ = textureZ.rgb*absN.z;
  vec3 colorY = textureY.rgb*absN.y;
  vec3 colorX = textureX.rgb*absN.x;
  return colorZ+colorY+colorX;
}

// 光线推进
vec3 RayMarch(vec2 coord) {
  float d = RAYMARCH_NEAR;
  // 从相机视点到当前片元的射线
  vec3 rd = normalize(RotateMatrix() * vec3(coord, -1));
  // 片元颜色
  vec3 color = vec3(0);
  for(int i = 0; i < RAYMARCH_TIME && d < RAYMARCH_FAR; i++) {
    // 光线推进后的点位
    vec3 p = CAMERA_POS + d * rd;
    // 法线绝对值
    vec3 n=SDFNormal(p);
    // 光线推进后的点位到长方体的有向距离
    float curD = SDFRect(p);
    // 若有向距离小于一定的精度,默认此点在长方体表面
    if(curD < RAYMARCH_PRECISION) {
      color=getTexture(p,n);
      break;
    }
    // 距离累加
    d += curD;
  }
  return color;
}

// 抗锯齿 Anti-Aliasing
vec3 RayMarch_anti(vec2 fragCoord) {
  // 初始颜色
  vec3 color = vec3(0);
  // 行列的一半
  float aa2 = float(AA / 2);
  // 逐行列遍历
  for(int y = 0; y < AA; y++) {
    for(int x = 0; x < AA; x++) {
      // 基于像素的偏移距离
      vec2 offset = vec2(float(x), float(y)) / float(AA) - aa2;
      // 坐标位
      vec2 coord = Coord(fragCoord + offset);
      // 累加周围片元的颜色
      color += RayMarch(coord);
    }
  }
  // 返回周围颜色的均值
  return color / float(AA * AA);
}

/* 绘图函数,画布中的每个片元都会执行一次,执行方式是并行的。
fragColor 输出参数,用于定义当前片元的颜色。
fragCoord 输入参数,当前片元的位置,原点在画布左下角,右侧边界为画布的像素宽,顶部边界为画布的像素高
*/
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
  // 光线推进
  vec3 color = RayMarch_anti(fragCoord);
  // 最终颜色
  fragColor = vec4(color, 1);
}

解释一下其中的几个关键点。

当前立方的面是朝内的,相机是在立方之中的,所以我们需要从内部判断RayMarching的推进点到立方体边界的距离,其SDF模型如下:

float SDFRect(vec3 coord) {
  vec3 d = RECT_SIZE-abs(coord - RECT_POS) ;
  return min(min(d.x,d.y),d.z);
}

其原理就是让立方体边界减去推进点到立方体的距离,然后取其最小的分量。

接下来,在getTexture() 中需要做好着色点与vu坐标的映射关系。

p=(p/RECT_SIZE)/2.+0.5;

这里就是在将采样点映射到uv坐标的[0,1]之间。

其实当前的立方体环境光还有个问题,立方体贴图在边界处的转折太明显:

image-20230928104021803

那我接下来把它贴在球体上试试。

4-2-球体环境光

4-2-1-球体外部贴图

下面是法线朝外的球体贴图效果。

1

看其效果会发现其面的边界处的转折不再那么明显。

整体代码如下:

// 贴图
#iChannel0 "file://images/bridge/posx.jpg"
#iChannel1 "file://images/bridge/negx.jpg"
#iChannel2 "file://images/bridge/posy.jpg"
#iChannel3 "file://images/bridge/negy.jpg"
#iChannel4 "file://images/bridge/posz.jpg"
#iChannel5 "file://images/bridge/negz.jpg"

// Wrap方式:Clamp Repeat  Mirror
#iChannel0::WrapMode "Repeat"

// 采样方式:Nearest Linear NearestMipMapNearest
#iChannel0::MinFilter "NearestMipMapNearest"
#iChannel0::MagFilter "Nearest"


// 坐标系缩放
#define PROJECTION_SCALE  1.

// 球体的球心位置
#define SPHERE_POS vec3(0)
// 球体的半径
#define SPHERE_R 1.

// 相机视点位
#define CAMERA_POS mat3(cos(iTime),0,sin(iTime),0,1,0,-sin(iTime),0,cos(iTime))*(vec3(0, 2, 4)-SPHERE_POS)+SPHERE_POS

// 相机目标点
#define CAMERA_TARGET vec3(0)
// 上方向
#define CAMERA_UP vec3(0, 1, 0)

// 光线推进的起始距离 
#define RAYMARCH_NEAR 0.1
// 光线推进的最远距离
#define RAYMARCH_FAR 128.
// 光线推进次数
#define RAYMARCH_TIME 40
// 当推进后的点位距离物体表面小于RAYMARCH_PRECISION时,默认此点为物体表面的点
#define RAYMARCH_PRECISION 0.001


// 相邻点的抗锯齿的行列数
#define AA 3


// 坐标系
vec2 Coord(in vec2 coord) {
  return PROJECTION_SCALE * 2. * (coord - 0.5 * iResolution.xy) / min(iResolution.x, iResolution.y);
}

//球体的SDF模型
float SDFSphere(vec3 coord) {
  return length(coord - SPHERE_POS) - SPHERE_R;
}


// 视图旋转矩阵
mat3 RotateMatrix() {
  //基向量c,视线
  vec3 c = normalize(CAMERA_POS - CAMERA_TARGET);
  //基向量a,视线和上方向的垂线
  vec3 a = cross(CAMERA_UP, c);
  //基向量b,修正上方向
  vec3 b = cross(c, a);
  //正交旋转矩阵
  return mat3(a, b, c);
}

// 计算长方体的法线
vec3 SDFNormal(in vec3 p) {
  const float h = 0.0001;
  const vec2 k = vec2(1, -1);
  return normalize(k.xyy * SDFSphere(p + k.xyy * h) +
    k.yyx * SDFSphere(p + k.yyx * h) +
    k.yxy * SDFSphere(p + k.yxy * h) +
    k.xxx * SDFSphere(p + k.xxx * h));
}

// 线性插值
vec2 liner(vec2 vmin,vec2 vmax,vec2 v){
  return (v-vmin)/(vmax-vmin);
}

// 获取纹理
vec3 getTexture(vec3 n){
  vec3 absN=abs(n);

  //3个方向上的a值 
  float a1=sqrt(pow(length(n.xz),2.)/2.);
  float a2=sqrt(pow(length(n.yz),2.)/2.);
  float a3=sqrt(pow(length(n.xy),2.)/2.);

  /* float z=absN.z>a1&&absN.z>a2?1.:0.;
  float y=absN.y>a3&&absN.y>a2?1.:0.;
  float x=absN.x>a1&&absN.x>a3?1.:0.; */
  
  float z=absN.z>=a1&&absN.z>a2?1.:0.;
  float y=absN.y>=a3&&absN.y>=a2?1.:0.;
  float x=absN.x>a1&&absN.x>a3?1.:0.;

  // xy面(前后的面)、xz面(上下的面)、zy面(左右的面)上的采样点
  vec2 p_xy= liner(vec2(-a1,-a2),vec2(a1,a2),n.xy); 
  vec2 p_xz= liner(vec2(-a3,-a2),vec2(a3,a2),n.xz); 
  vec2 p_zy= liner(vec2(-a1,-a3),vec2(a1,a3),n.zy); 

  vec4 textureZ=n.z>0.? texture(iChannel5, p_xy): texture(iChannel4, vec2(-p_xy.x,p_xy.y));
  vec4 textureY=n.y>0.? texture(iChannel2, vec2(-p_xz[0],p_xz[1])): texture(iChannel3,-p_xz);
  vec4 textureX=n.x>0.? texture(iChannel1,  vec2(-p_zy[0],p_zy[1])): texture(iChannel0, p_zy);

  vec3 colorZ = textureZ.rgb*z;
  vec3 colorY = textureY.rgb*y;
  vec3 colorX = textureX.rgb*x;

  return colorZ+colorY+colorX;
}

// 光线推进
vec3 RayMarch(vec2 coord) {
  float d = RAYMARCH_NEAR;
  // 从相机视点到当前片元的射线
  vec3 rd = normalize(RotateMatrix() * vec3(coord, -1));
  // 片元颜色
  vec3 color = vec3(0);
  for(int i = 0; i < RAYMARCH_TIME && d < RAYMARCH_FAR; i++) {
    // 光线推进后的点位
    vec3 p = CAMERA_POS + d * rd;
    // 法线绝对值
    vec3 n=SDFNormal(p);
    // 光线推进后的点位到长方体的有向距离
    float curD = SDFSphere(p);
    // 若有向距离小于一定的精度,默认此点在长方体表面
    if(curD < RAYMARCH_PRECISION) {
      color=getTexture(n);
      break;
    }
    // 距离累加
    d += curD;
  }
  return color;
}

// 抗锯齿 Anti-Aliasing
vec3 RayMarch_anti(vec2 fragCoord) {
  // 初始颜色
  vec3 color = vec3(0);
  // 行列的一半
  float aa2 = float(AA / 2);
  // 逐行列遍历
  for(int y = 0; y < AA; y++) {
    for(int x = 0; x < AA; x++) {
      // 基于像素的偏移距离
      vec2 offset = vec2(float(x), float(y)) / float(AA) - aa2;
      // 坐标位
      vec2 coord = Coord(fragCoord + offset);
      // 累加周围片元的颜色
      color += RayMarch(coord);
    }
  }
  // 返回周围颜色的均值
  return color / float(AA * AA);
}

/* 绘图函数,画布中的每个片元都会执行一次,执行方式是并行的。
fragColor 输出参数,用于定义当前片元的颜色。
fragCoord 输入参数,当前片元的位置,原点在画布左下角,右侧边界为画布的像素宽,顶部边界为画布的像素高
*/
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
  // 光线推进
  vec3 color = RayMarch_anti(fragCoord);
  // 最终颜色
  fragColor = vec4(color, 1);
}

其6个面的区分原理和立方体是一样的,不过我又做了一点细节调整。

我在判断球面在x,y,z哪个方向的时候,有些地方用了>=,而不是之前的>。

/* 
float z=absN.z>a1&&absN.z>a2?1.:0.;
float y=absN.y>a3&&absN.y>a2?1.:0.;
float x=absN.x>a1&&absN.x>a3?1.:0.; 
*/

float z=absN.z>=a1&&absN.z>a2?1.:0.;
float y=absN.y>=a3&&absN.y>=a2?1.:0.;
float x=absN.x>a1&&absN.x>a3?1.:0.;

这是因为之前的>会让球面边界采不到点,从而出现缝隙,如下图所示:

image-20230929225430466

不过我当前的解决方式并不是最好,接缝依旧存在,只是不太明显。

这个问题跟贴图没关系,应该是我划分球面时的边界数据的精度问题。

若大家有好的解决方式可以微信我(1051904257)。

接下来咱们把相机打到球体内部。

4-2-1-球体内部贴图

球体内部贴图效果如下:

1

整体代码如下:

// 贴图
#iChannel0 "file://images/bridge/posx.jpg"
#iChannel1 "file://images/bridge/negx.jpg"
#iChannel2 "file://images/bridge/posy.jpg"
#iChannel3 "file://images/bridge/negy.jpg"
#iChannel4 "file://images/bridge/posz.jpg"
#iChannel5 "file://images/bridge/negz.jpg"

// Wrap方式:Clamp Repeat  Mirror
#iChannel0::WrapMode "Repeat"

// 采样方式:Nearest Linear NearestMipMapNearest
#iChannel0::MinFilter "NearestMipMapNearest"
#iChannel0::MagFilter "Nearest"


// 坐标系缩放
#define PROJECTION_SCALE  1.

// 球体的球心位置
#define SPHERE_POS vec3(0)
// 球体的半径
#define SPHERE_R 3.

// 相机视点位
#define CAMERA_POS mat3(cos(iTime),0,sin(iTime),0,1,0,-sin(iTime),0,cos(iTime))*(vec3(0, 0, 0.4)-SPHERE_POS)+SPHERE_POS
// #define CAMERA_POS vec3(0, 0, 0.5)

// 相机目标点
#define CAMERA_TARGET vec3(0)
// 上方向
#define CAMERA_UP vec3(0, 1, 0)

// 光线推进的起始距离 
#define RAYMARCH_NEAR 0.1
// 光线推进的最远距离
#define RAYMARCH_FAR 128.
// 光线推进次数
#define RAYMARCH_TIME 40
// 当推进后的点位距离物体表面小于RAYMARCH_PRECISION时,默认此点为物体表面的点
#define RAYMARCH_PRECISION 0.001


// 相邻点的抗锯齿的行列数
#define AA 3


// 坐标系
vec2 Coord(in vec2 coord) {
  return PROJECTION_SCALE * 2. * (coord - 0.5 * iResolution.xy) / min(iResolution.x, iResolution.y);
}

//球体的SDF模型-翻转法线
float SDFSphere(vec3 coord) {
  return SPHERE_R-length(coord - SPHERE_POS);
}


// 视图旋转矩阵
mat3 RotateMatrix() {
  //基向量c,视线
  vec3 c = normalize(CAMERA_POS - CAMERA_TARGET);
  //基向量a,视线和上方向的垂线
  vec3 a = cross(CAMERA_UP, c);
  //基向量b,修正上方向
  vec3 b = cross(c, a);
  //正交旋转矩阵
  return mat3(a, b, c);
}

// 计算长方体的法线
vec3 SDFNormal(in vec3 p) {
  const float h = 0.0001;
  const vec2 k = vec2(1, -1);
  return normalize(k.xyy * SDFSphere(p + k.xyy * h) +
    k.yyx * SDFSphere(p + k.yyx * h) +
    k.yxy * SDFSphere(p + k.yxy * h) +
    k.xxx * SDFSphere(p + k.xxx * h));
}

// 线性插值
vec2 liner(vec2 vmin,vec2 vmax,vec2 v){
  return (v-vmin)/(vmax-vmin);
}

// 获取纹理
vec3 getTexture(vec3 n){
  vec3 absN=abs(n);

  //3个方向上的a值 
  float a1=sqrt(pow(length(n.xz),2.)/2.);
  float a2=sqrt(pow(length(n.yz),2.)/2.);
  float a3=sqrt(pow(length(n.xy),2.)/2.);

  float z=absN.z>=a1&&absN.z>a2?1.:0.;
  float y=absN.y>=a3&&absN.y>=a2?1.:0.;
  float x=absN.x>a1&&absN.x>a3?1.:0.;

  // xy面(前后的面)、xz面(上下的面)、zy面(左右的面)上的采样点
  vec2 p_xy= liner(vec2(-a1,-a2),vec2(a1,a2),n.xy); 
  vec2 p_xz= liner(vec2(-a3,-a2),vec2(a3,a2),n.xz); 
  vec2 p_zy= liner(vec2(-a1,-a3),vec2(a1,a3),n.zy); 

  vec4 textureZ=n.z>0.? texture(iChannel4, -p_xy): texture(iChannel5, vec2(p_xy.x,-p_xy.y));
  vec4 textureY=n.y>0.? texture(iChannel3, vec2(-p_xz[0],p_xz[1])): texture(iChannel2,-p_xz);
  vec4 textureX=n.x>0.? texture(iChannel1,  vec2(p_zy[0],-p_zy[1])): texture(iChannel0, vec2(-p_zy[0],-p_zy[1]));

  vec3 colorZ = textureZ.rgb*z;
  vec3 colorY = textureY.rgb*y;
  vec3 colorX = textureX.rgb*x;

  return colorZ+colorY+colorX;
}

// 光线推进
vec3 RayMarch(vec2 coord) {
  float d = RAYMARCH_NEAR;
  // 从相机视点到当前片元的射线
  vec3 rd = normalize(RotateMatrix() * vec3(coord, -1));
  // 片元颜色
  vec3 color = vec3(0);
  for(int i = 0; i < RAYMARCH_TIME && d < RAYMARCH_FAR; i++) {
    // 光线推进后的点位
    vec3 p = CAMERA_POS + d * rd;
    // 法线绝对值
    vec3 n=SDFNormal(p);
    // 光线推进后的点位到长方体的有向距离
    float curD = SDFSphere(p);
    // 若有向距离小于一定的精度,默认此点在长方体表面
    if(curD < RAYMARCH_PRECISION) {
      color=getTexture(n);
      break;
    }
    // 距离累加
    d += curD;
  }
  return color;
}

// 抗锯齿 Anti-Aliasing
vec3 RayMarch_anti(vec2 fragCoord) {
  // 初始颜色
  vec3 color = vec3(0);
  // 行列的一半
  float aa2 = float(AA / 2);
  // 逐行列遍历
  for(int y = 0; y < AA; y++) {
    for(int x = 0; x < AA; x++) {
      // 基于像素的偏移距离
      vec2 offset = vec2(float(x), float(y)) / float(AA) - aa2;
      // 坐标位
      vec2 coord = Coord(fragCoord + offset);
      // 累加周围片元的颜色
      color += RayMarch(coord);
    }
  }
  // 返回周围颜色的均值
  return color / float(AA * AA);
}

/* 绘图函数,画布中的每个片元都会执行一次,执行方式是并行的。
fragColor 输出参数,用于定义当前片元的颜色。
fragCoord 输入参数,当前片元的位置,原点在画布左下角,右侧边界为画布的像素宽,顶部边界为画布的像素高
*/
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
  // 光线推进
  vec3 color = RayMarch_anti(fragCoord);
  // 最终颜色
  fragColor = vec4(color, 1);
}

当前的球体和之前的立方体一样,都做了法线的翻转,其翻转原理都是让模型的尺寸减去推进点位。

//球体的SDF模型-翻转法线
float SDFSphere(vec3 coord) {
  return SPHERE_R-length(coord - SPHERE_POS);
}

贴图和球面的映射原理都是一样的,只是细节需要做一下微调,比如纹理的镜像操作。

4-3-内部贴图的简单化

其实对于在模型内部贴图制作环境光的过程,是可以很简单的。

以球天为例,我只需要知道相机的视线方向就够了。

想象我们在仰望蓝天的时候,蓝天只会随我们视线的旋转而改变,不会随视点的移动而改变。

球天是不需要考虑其位置和半径大小的,因此也就不需要再做光线推进。

最终其代码就可以写成这样:

// 贴图
#iChannel0 "file://images/bridge/posx.jpg"
#iChannel1 "file://images/bridge/negx.jpg"
#iChannel2 "file://images/bridge/posy.jpg"
#iChannel3 "file://images/bridge/negy.jpg"
#iChannel4 "file://images/bridge/posz.jpg"
#iChannel5 "file://images/bridge/negz.jpg"

// Wrap方式:Clamp Repeat  Mirror
#iChannel0::WrapMode "Repeat"

// 采样方式:Nearest Linear NearestMipMapNearest
#iChannel0::MinFilter "NearestMipMapNearest"
#iChannel0::MagFilter "Nearest"

// 坐标系缩放
#define PROJECTION_SCALE  1.

// 相机视点位
#define CAMERA_POS mat3(cos(iTime),0,sin(iTime),0,1,0,-sin(iTime),0,cos(iTime))*(vec3(0, 0, 0.4))

// 相机目标点
#define CAMERA_TARGET vec3(0)
// 上方向
#define CAMERA_UP vec3(0, 1, 0)

// 坐标系
vec2 Coord(in vec2 coord) {
  return PROJECTION_SCALE * 2. * (coord - 0.5 * iResolution.xy) / min(iResolution.x, iResolution.y);
}

// 视图旋转矩阵
mat3 RotateMatrix() {
  //基向量c,视线
  vec3 c = normalize(CAMERA_POS - CAMERA_TARGET);
  //基向量a,视线和上方向的垂线
  vec3 a = cross(CAMERA_UP, c);
  //基向量b,修正上方向
  vec3 b = cross(c, a);
  //正交旋转矩阵
  return mat3(a, b, c);
}

// 线性插值
vec2 liner(vec2 vmin,vec2 vmax,vec2 v){
  return (v-vmin)/(vmax-vmin);
}

// 获取纹理
vec3 getTexture(vec3 n){
  vec3 absN=abs(n);

  //3个方向上的a值 
  float a1=sqrt(pow(length(n.xz),2.)/2.);
  float a2=sqrt(pow(length(n.yz),2.)/2.);
  float a3=sqrt(pow(length(n.xy),2.)/2.);

  float z=absN.z>=a1&&absN.z>a2?1.:0.;
  float y=absN.y>=a3&&absN.y>=a2?1.:0.;
  float x=absN.x>a1&&absN.x>a3?1.:0.;

  // xy面(前后的面)、xz面(上下的面)、zy面(左右的面)上的采样点
  vec2 p_xy= liner(vec2(-a1,-a2),vec2(a1,a2),n.xy); 
  vec2 p_xz= liner(vec2(-a3,-a2),vec2(a3,a2),n.xz); 
  vec2 p_zy= liner(vec2(-a1,-a3),vec2(a1,a3),n.zy); 

  vec4 textureZ=n.z>0.? texture(iChannel5, p_xy): texture(iChannel4, vec2(-p_xy.x,p_xy.y));
  vec4 textureY=n.y>0.? texture(iChannel2, vec2(-p_xz[0],p_xz[1])): texture(iChannel3,-p_xz);
  vec4 textureX=n.x>0.? texture(iChannel1,  vec2(-p_zy[0],p_zy[1])): texture(iChannel0, p_zy);

  vec3 colorZ = textureZ.rgb*z;
  vec3 colorY = textureY.rgb*y;
  vec3 colorX = textureX.rgb*x;

  return colorZ+colorY+colorX;
}

/* 绘图函数,画布中的每个片元都会执行一次,执行方式是并行的。
fragColor 输出参数,用于定义当前片元的颜色。
fragCoord 输入参数,当前片元的位置,原点在画布左下角,右侧边界为画布的像素宽,顶部边界为画布的像素高
*/
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
  vec3 rd = normalize(RotateMatrix() * vec3(Coord(fragCoord), -1));
  vec3 color =getTexture(rd);
  fragColor = vec4(color, 1);
}

其效果和之前是一样的,所以我就不再多说。

总结

其实,我们也可以把圆柱投影贴图映射到球体上,这个映射原理我在WebGL里说过,所以我就先不写了,有时间了再做补充。

各种SDF模型与贴图的映射方式是多种多样的,我就先说这2种了。

后面我会说一下如何以球天为环境光,渲染模型。

参考链接:

www.bilibili.com/video/BV1X7…