threejs复刻原神渲染(二)

1,304 阅读6分钟

书接上文,上一次我们已经添加了漫反射,这一次我们添加高光,脸部阴影

1.高光

我们上一节在准备上下文的时候计算了一个变量NDotH,他是法向量和半程向量的点积,半程向量是眼睛向量和光照相加后归一化得到的值

    vec3 hDirWS = normalize(vDirWs + dirL);
    float NDotH = dot(worldNormal, hDirWS); //Blinn-Phong

和半兰伯特结合起来看看效果,可以自行调整光泽系数

  float blinPhong = step(0., NDotL) * pow(max(NDotH, 0.), 10.);

image.png

高光分为金属高光和非金属高光,我们先做非金属的高光,对blinn-phong取反,利用魔法图的b通道抠出高光的形状,最后乘以魔法图的r通道乘以自己定义非金属的高光系数即可

 vec3 noMetalicSpec = vec3(step(1.0 - blinPhong, lightMapTex.b) * lightMapTex.r) * uNoMetallic;

注意当lightMapTex.b的值为1时,如果blinPhong的值为0,这时候step会返回1导致出现漏光,所以我们需要稍微增加一点值 image.png

 vec3 noMetalicSpec = vec3(step(1.04 - blinPhong, lightMapTex.b) * lightMapTex.r) * uNoMetallic;

image.png

金属的高光直接用biln-phong乘以魔法图的b通道再乘以基础色即可,因为光滑的非金属表面是不吸收颜色的,但是金属会吸收,所以要乘基础色

vec3 metalicSpec = vec3(blinPhong * lightMapTex.b * baseColor.rgb) * uMetallic;

最后根据魔法图的r通道提取金属区域,混合金属高光和非金属高光

     float isMetal = step(.95, lightMapTex.r);
     vec3 finalSpec = mix(noMetalicSpec, metalicSpec, isMetal);

image.png

金属材质还有一种镜面的反光细节,这里用matcap贴图来补充,采样matcap图需要用到matcapUV,matcapUV的计算是由摄像机空间下的法向量取xy平面并且映射到[0,1]之后用isMetal取混合即可

    vec2 matcapUV = (normalize(vViewNormal.xy) + 1.) * .5;
    vec3 metallic = mix(vec3(0.), texture2D(uMetalMap, matcapUV).r * baseColor.rgb, isMetal);

最后把高光和漫反射叠加起来作为最终的颜色输出即可

  vec3 albedo = diffuse + finalSpec + metallic;

已经可以明显看到各种高光的细节了image.png

2.脸部阴影

脸部的阴影需要用到sdf图,这是张阈值图,存了不同角度下阴影覆盖脸上哪些部分,关于sdf图的相关的资料感兴趣的话可以求看下这篇文章,注意sdf图的generateMipmaps需要设置成false不然会有很严重的锯齿

  const faceLightMap = useTexture("/Face/faceLightmap.png");
  faceLightMap.wrapS = faceLightMap.wrapT = RepeatWrapping;
  faceLightMap.generateMipmaps = false;
  faceLightMap.flipY = false;

image.png

采样伦勃朗光

image.png 老样子提前准备上下文,我们需要从外部传入着色器角色的朝向,以及左方向,光源方向

因为webgl是右手坐标系,可以通过前向叉乘左方向得到上方向,我们要获取角色面部的朝向可以计算光源方向的向量在上方向上的投影,计算出投影后就可以用光源方向的向量减去在向上方向上的投影就可以获得角色面部的朝向了

  vec3 forwardVec = normalize(uForwardVec);
  
  vec3 leftVec = normalize(uLeftVec);
  
  vec3 lightVec = normalize(uLightPosition);
  
  vec3 upVector = cross(forwardVec, leftVec);
  
  vec3 LpU = dot(lightVec, upVector) / pow(length(upVector), 2.) * upVector;
  
  vec3 LpHeadHorizon = normalize(lightVec - LpU);

我们需要计算光是照在左脸还是右脸,在这之前我们角色脸部朝向的向量点成左向量后用反余弦可以获得角度值,除以π归一化到[0,1],下图为值的1的点经过反余弦除以π后的值是,0,为0的是0.5,为-1的的时候值是1,所以可以用step函数区分左右脸,小于0.5为左脸,大于0.5为右脸,我们需要将左右脸分别都映射到[0-1]之间,最后在混合左右脸即可

image.png

  float value = acos(dot(LpHeadHorizon, leftVec)) / PI;

  // 0-0.5 expose left 0.5-1 expose right
  float exposeLeft = step(value, 0.5);

  // left: map 0-0.5 to 1-0, right: map 0.5-1 to 0-1
  float valueL = 1. - value * 2.;
  float valueR = value * 2. - 1.;
  float mixValue = mix(valueR, valueL, exposeLeft);

接下来就可以去采样左右脸的伦勃朗光了,当伦勃朗光出现在左脸时说明光照在右脸,我们拿到的sdf图只有半张脸,所以需要再u轴取反,当伦勃朗光出现在右脸时,说明光照在左脸,正常采样就行,最后根据exposeLeft混合左右脸的伦勃朗光即可

  float sdfRembrandLeft = texture2D(uFaceLightMap, vec2(1. - vUv.x, vUv.y)).r;
  float sdfRembrandRight = texture2D(uFaceLightMap, vUv).r;
  // 混合左右脸的sdf
  float mixSdf = mix(sdfRembrandLeft, sdfRembrandRight, exposeLeft);

比较mixValuemixSdf的灰度值,当value小于sdf时说明是亮面,当value大于sdf时是暗面,因为我们的范围只有0-180度,所以是不知道光线照在后脑勺的情况的,所以我们需要用点乘来判断一下,最后混合光照在正面和光照在后脑勺的sdf

  // 当value小于sdf的就是亮面 
  float sdf = step(mixValue, mixSdf);
  // 判断光照是否在后面
  sdf = mix(0., sdf, step(0., dot(LpHeadHorizon, forwardVec)));

最后我们输出sdf看一下

QQ2024626-18530.gif 可以看到脸部sdf变化的有点过快了,利用pow函数让趋近于0的才为0,趋近于1的才无限接近1

  float valueL = pow(1. - value * 2., 3.);
  float valueR = pow(value * 2. - 1., 3.);

QQ2024626-181839.gif

下一步是采样ramp图作为阴影颜色,在ramp图上皮肤的颜色分别是5和10行,计算中心点,混合白天和夜晚的阴影颜色即可,同样也是通过外部传入来控制白天还是夜晚

  float rampV = RampRow / 10. - .05;
  float rampClampMin = .003;

  vec2 rampDayUV = vec2(rampClampMin, 1. - rampV);
  vec2 rampNightUV = vec2(rampClampMin, 1. - (rampV + .5));

  float isDay = (uIsDay + 1.) * .5;

  vec3 rampColor = mix(texture2D(uRampMap, rampNightUV), texture2D(uRampMap, rampDayUV), isDay).rgb;

  vec3 col = csm_DiffuseColor.rgb;
  vec3 darkCol = col * rampColor;

最后根据sdf的阈值来混合基础色和阴影颜色

   csm_Emissive = mix(darkCol, col, sdf);

最终效果 image.png

QQ2024626-18271.gif

完整代码

varying vec2 vUv;
uniform vec3 uLightPosition;
uniform sampler2D uFaceLightMap;
uniform sampler2D uLightMap;
uniform float uIsDay;
uniform sampler2D uRampMap;
uniform vec3 uForwardVec;
uniform vec3 uLeftVec;

float RampRow = 5.;
void main() {
  /* 处理数据 */
  vec3 forwardVec = normalize(uForwardVec);
  vec3 leftVec = normalize(uLeftVec);
  vec3 lightVec = normalize(uLightPosition);
  vec3 upVector = cross(forwardVec, leftVec);

  vec3 LpU = dot(lightVec, upVector) / pow(length(upVector), 2.) * upVector;
  vec3 LpHeadHorizon = normalize(lightVec - LpU);

  float value = acos(dot(LpHeadHorizon, leftVec)) / PI;

  // 0-0.5 expose left 0.5-1 expose right
  float exposeLeft = step(value, 0.5);

  // left: map 0-0.5 to 1-0, right: map 0.5-1 to 0-1
  float valueL = pow(1. - value * 2., 3.);
  float valueR = pow(value * 2. - 1., 3.);

  float mixValue = mix(valueR, valueL, exposeLeft);

  float sdfRembrandLeft = texture2D(uFaceLightMap, vec2(1. - vUv.x, vUv.y)).r;
  float sdfRembrandRight = texture2D(uFaceLightMap, vUv).r;
  // 混合左右脸的sdf
  float mixSdf = mix(sdfRembrandLeft, sdfRembrandRight, exposeLeft);
  // 当value小于sdf的就是亮面 
  float sdf = step(mixValue, mixSdf);
  // 判断光照是否在后面
  sdf = mix(0., sdf, step(0., dot(LpHeadHorizon, forwardVec)));

  /* 计算rampV */
  float rampV = RampRow / 10. - .05;
  float rampClampMin = .003;

  vec2 rampDayUV = vec2(rampClampMin, 1. - rampV);
  vec2 rampNightUV = vec2(rampClampMin, 1. - (rampV + .5));

  float isDay = (uIsDay + 1.) * .5;

  vec3 rampColor = mix(texture2D(uRampMap, rampNightUV), texture2D(uRampMap, rampDayUV), isDay).rgb;

  vec3 col = csm_DiffuseColor.rgb;
  vec3 darkCol = col * rampColor;

  csm_Emissive = mix(darkCol, col, sdf);
  csm_Roughness = 1.;
  csm_Metalness = 0.;
}

项目地址:github.com/KallkaGo/To…

Ending

本文到这里就结束了,感谢知乎和b站等平台的各位大佬的文章和视频,也算是对自己三渲二过程的一个记录吧,谢谢