Lambertian 光照模型

在这篇文章中,我们继续使用自己开发的软件渲染流程,来实现 Lambertian 光照模型,并验证效果。

1. Lambertian 光照模型

Lambertian 光照模型,主要用于模拟那些可以完全漫反射光线的表面。Lambertian 模型理解起来比较直观,表面的明暗是由表面法线与光线方向之间的角度决定的。

直观的点在于:光线垂直照射表面,最亮;平行照不到光,最暗。即,和角度的余弦相关。

如图 1 所示,在此次实验环境中,我们定义光线的方向就是照向表面的方向。所以,设法线为 N,光线方向为 L,那么光线作用的量为:

  • dot(N,-L)

光线可能从背面,即 N 和 L 之间的角度大于 90 度,余弦值会为负。因为都是不贡献光照效果,所以不需要负数:

  • max(dot(N,-L),0)
图1 Lambertian 反射

已经获取了光的“贡献量”,接着进行简单的反射颜色模拟。此处颜色采用 RGB 方式表示,光源颜色和物体表面颜色进行元素积操作(逐分量进行乘法操作),可以模拟反射光的颜色。

同样比较符合日常直觉:白光反射物体固有色;黑色物体不反射光;有颜色的光会影响物体的固有色。

我们设光线的颜色为 light_color,物体表面的颜色为 surface_color,可以得到反射光的颜色为:

  • light_color × surface_color

再加上光的“贡献量”,可以得到最终看到的表面颜色:

  • final_color = light_color × surface_color × max(dot(N,-L),0)

我们还能继续完善一下,我们不希望物体完全照不到光。可以增加一个比较暗的环境光,设为 ambient_color。那么,最终表面反射的颜色为:

  • final_color = light_color × surface_color × max(dot(N,-L),0)
  •               + ambient_color × surface_color

2. 代码实现

在了解了 Lambertian 光照模型的理论之后,我们进行代码实现。如代码清单 1 所示,我们在顶点着色器中获取顶点位置、法线和 uv 坐标,并传递给后续片元着色器。

Lambertian 的具体实现逻辑在片段着色器中。我们首先获取到法线和 uv 坐标。根据 uv 坐标,可以得到表面的颜色值。光源的方向和颜色,以及环境光的颜色,在本次实现中都是常量。所以,公式中涉及的所有参数都“集齐”了,按照先前所讲的公式进行计算即可。

注意,最终的颜色值可能不在 0 到 1 范围内,我们使用 clamp 进行截断。

代码清单 1 Lambertian 实现
  1. void TLambertianShader::VertexShader(const TShaderContext& context, TVertexShaderOutput& output)
  2. {
  3.     tmath::Vec3f position;
  4.     context.GetAttribute(0, position);
  5.  
  6.     output.builtin_position = projectionMatrix * viewMatrix * modelMatrix * tmath::Vec4f(position, 1.0f);
  7.  
  8.     tmath::Vec3f normal;
  9.     context.GetAttribute(1, normal);
  10.     output.variables["normal"] = normal;
  11.  
  12.     tmath::Vec2f uv;
  13.     context.GetAttribute(2, uv);
  14.     output.variables["uv"] = uv;
  15. }
  16.  
  17. void TLambertianShader::FragmentShader(
  18.     const TShaderContext& context,
  19.     const TVertexShaderOutput& input,
  20.     TFragmentShaderOutput& output)
  21. {
  22.     tmath::Vec3f normal = std::get<tmath::Vec3f>(input.variables.at("normal"));
  23.     tmath::Vec2f uv = std::get<tmath::Vec2f>(input.variables.at("uv"));
  24.  
  25.     normal.Normalize();
  26.     tmath::Vec4f surfaceColor = context.texture(uv);
  27.     float NDotL = std::max(tmath::dot(normal, -lightDirection), 0.0f);
  28.     tmath::Vec4f final_color = lightColor * surfaceColor * NDotL;
  29.  
  30.     final_color += ambientColor * surfaceColor;
  31.  
  32.     final_color =
  33.     {
  34.         std::clamp(final_color.x(), 0.0f, 1.0f),
  35.         std::clamp(final_color.y(), 0.0f, 1.0f),
  36.         std::clamp(final_color.z(), 0.0f, 1.0f),
  37.         std::clamp(final_color.w(), 0.0f, 1.0f)
  38.     };
  39.  
  40.     output.color = final_color;
  41. }

3. 测试

最后,我们编写测试用例,对实现的 Lambertian 效果进行验证。

采用的场景还是贴图立方体,但是和之前的数据定义方式不同。之前立方体的三角形顶点是共用的,但是现在不行了。因为相同的顶点可能属于不同的面,所以它们的法线可能不同,需要单独指定。

如代码清单 2 所示,我们单独定义 12 个面,即 36 个点。vertices 有 36 行,太长,所以此处进行了省略。同时,vertices 采用交错格式,一个顶点数据包括位置、法线和 uv 坐标。

其余的操作都是之前接触过的接口。创建 vao 和 vbo。现在只需要创建一个 vbo,并指定三个 layout。指定纹理贴图。设置着色器 “uniform”。开启深度测试。引入摄像机模块。

代码清单 2 测试样例
  1. TLambertianRenderTask::TLambertianRenderTask(TBasicWindow& win)
  2.     : m_camera(tmath::Vec3f(0.0f, 0.0f, -4.0f), tmath::Vec3f(0.0f, 0.0f, 0.0f))
  3. {
  4.     float vertices[] = {
  5.         // positions          // normals           // texture coords
  6.         -0.5f, -0.5f, -0.5f,  0.0f,  0.0f, -1.0f,  0.0f,  0.0f,
  7.          0.5f, -0.5f, -0.5f,  0.0f,  0.0f, -1.0f,  1.0f,  0.0f,
  8.         …… …… ……
  9.         -0.5f,  0.5f, -0.5f,  0.0f,  1.0f,  0.0f,  0.0f,  1.0f
  10.     };
  11.  
  12.     uint32_t indices[] =
  13.     {
  14.         0, 1, 2, 3, 4, 5,
  15.         6, 7, 8, 9, 10, 11,
  16.         12,13,14,15,16,17,
  17.         18,19,20,21,22,23,
  18.         24,25,26,27,28,29,
  19.         30,31,32,33,34,35
  20.     };
  21.  
  22.     TSoftRenderer& sr = win.GetRenderer();
  23.  
  24.     uint32_t vao, vbo, ebo;
  25.     sr.GenVertexArrays(1, &vao);
  26.     sr.BindVertexArray(vao);
  27.  
  28.     sr.GenBuffers(1, &vbo);
  29.     sr.BindBuffer(TBufferType::ArrayBuffer, vbo);
  30.     sr.BufferData(TBufferType::ArrayBuffer, sizeof(vertices), vertices);
  31.    
  32.     sr.VertexAttribPointer(0, 3, 8 * sizeof(float), 0);
  33.     sr.VertexAttribPointer(1, 3, 8 * sizeof(float), 3 * sizeof(float));
  34.     sr.VertexAttribPointer(2, 2, 8 * sizeof(float), 6 * sizeof(float));
  35.  
  36.     sr.GenBuffers(1, &ebo);
  37.     sr.BindBuffer(TBufferType::ElementArrayBuffer, ebo);
  38.     sr.BufferData(TBufferType::ElementArrayBuffer, sizeof(indices), indices);
  39.  
  40.     sr.PrintVAO(vao);
  41.     ////
  42.     uint32_t textureId;
  43.     sr.GenTextures(1, &textureId);
  44.     sr.BindTexture(textureId);
  45.  
  46.     TImage img("image/dog.jpg", TImage::ColorFormat::RGBA);
  47.     sr.TexImage2D(img.GetWidth(), img.GetHeight(), img.GetData());
  48.  
  49.     sr.TexParameter(TTextureParam::WrapS, (int)TTextureWrapMode::Repeat);
  50.     sr.TexParameter(TTextureParam::WrapT, (int)TTextureWrapMode::Repeat);
  51.     sr.TexParameter(TTextureParam::Filter, (int)TTextureFilterMode::Nearest);
  52.     ////
  53.     int width = win.GetWindowWidth();
  54.     int height = win.GetWindowHeight();
  55.  
  56.     float aspect = (float)width / height;
  57.  
  58.     m_shader.projectionMatrix = tmath::PerspectiveMatrix(tmath::degToRad(60.0f), aspect, 0.1f, 100.0f);
  59.     m_shader.viewMatrix = tmath::TranslationMatrix(0.0f, 0.0f, 4.0f);
  60.     m_shader.modelMatrix.ToIdentity();
  61.  
  62.     m_shader.lightDirection = { -1.0f, -0.3f, 0.7f };
  63.     m_shader.lightDirection.Normalize();
  64.     m_shader.lightColor = { 1.0f, 1.0f, 1.0f, 1.0f };
  65.     m_shader.ambientColor = { 0.1f, 0.1f, 0.1f, 1.0f };
  66.  
  67.     sr.UseProgram(&m_shader);
  68.  
  69.     ////
  70.     sr.Enable(TEnableCap::DepthTest);
  71.     sr.DepthFunc(TDepthFunc::Less);
  72.  
  73.     ////
  74.     win.AddInputHandler(&m_camera);
  75. }
  76.  
  77. void TLambertianRenderTask::Render(TSoftRenderer& sr)
  78. {
  79.     m_shader.viewMatrix = m_camera.GetViewMatrix();
  80.     sr.ClearColor({ 0,0,0 });
  81.     sr.ClearDepth(1.0f);
  82.  
  83.     sr.DrawElements(TDrawMode::Triangles, 36, 0);
  84. }

图 2 是运行的效果,光线设置从右侧打过来,可以看到效果正确。

本章的完整代码见 tag/lambertian

图2 运行结果