跳转至

24 如何模拟光照让3D场景更逼真?(下)

你好,我是月影。今天,我们接着来讲,怎么模拟光照。

上节课,我们讲了四种光照的漫反射模型。实际上,因为物体的表面材质不同,反射光不仅有漫反射,还有镜面反射。

什么是镜面反射呢?如果若干平行光照射在表面光滑的物体上,反射出来的光依然平行,这种反射就是镜面反射。镜面反射的性质是,入射光与法线的夹角等于反射光与法线的夹角。

越光滑的材质,它的镜面反射效果也就越强。最直接的表现就是物体表面会有闪耀的光斑,也叫镜面高光。但并不是所有光都能产生镜面反射,我们上节课讲的四种光源中,环境光因为没有方向,所以不参与镜面反射。剩下的平行光、点光源、聚光灯这三种光源,都是能够产生镜面反射的有向光。

那么今天,我们就来讨论一下如何实现镜面反射,然后将它和上节课的漫反射结合起来,就可以实现标准的光照模型,也就是Phong反射模型了,从而能让我们实现的可视化场景更加接近于自然界的效果。

如何实现有向光的镜面反射?

首先,镜面反射需要同时考虑光的入射方向以及相机也就是观察者所在的方向。

接着,我们再来说说怎么实现镜面反射效果,一般来说需要4个步骤。

第一步,求出反射光线的方向向量。这里我们以点光源为例,要求出反射光的方向,我们可以直接使用GLSL的内置函数reflect,这个函数能够返回一个向量相对于某个法向量的反射向量,正好就是我们要的镜面反射结果。

// 求光源与点坐标的方向向量
vec3 dir = (viewMatrix * vec4(pointLightPosition, 1.0)).xyz - vPos;

// 归一化
dir = normalize(dir);

// 求反射向量
vec3 reflectionLight = reflect(-dir, vNormal);

第二步,我们要根据相机位置计算视线与反射光线夹角的余弦,用到原理是向量的点乘。

vec3 eyeDirection = vCameraPos - vPos;
eyeDirection = normalize(eyeDirection);
// 与视线夹角余弦
float eyeCos = max(dot(eyeDirection, reflectionLight), 0.0);

第三步,我们使用系数和指数函数设置镜面反射强度。指数越大,镜面越聚焦,高光的光斑范围就越小。这里,我们指数取50.0,系数取2.0。系数能改变反射亮度,系数越大,反射的亮度就越高。

float specular = 2.0 *  pow(eyeCos, 50.0);

最后,我们将漫反射和镜面反射结合起来,就会让距离光源近的物体上,形成光斑。

// 合成颜色
gl_FragColor.rgb = specular + (ambientLight + diffuse) * materialReflection;
gl_FragColor.a = 1.0;

上面的代码是以点光源为例来实现的光斑,其实只要是有向光,都可以用同样的方法求出镜面反射,只不过对应的入射光方向计算有所不同,也就是着色器代码中的dir变量计算方式不一样。 你可以利用我上节课讲的内容,自己动手试试。

如何实现完整的Phong反射模型?

那在自然界中,除了环境光以外,其他每种光源在空间中都可以存在不止一个,而且因为几何体材质不同,物体表面也可能既出现漫反射,又出现镜面反射。

可能出现的情况这么多,分析和计算起来也会非常复杂。为了方便处理,我们可以把多种光源和不同材质结合起来,形成标准的反射模型,这一模型被称为Phong反射模型

Phong反射模型的完整公式如下:

$$
I_{\mathrm{p}}=k_{\mathrm{a}} i_{\mathrm{a}}+\sum_{m \in \text { lights }}\left(k_{\mathrm{d}}\left(\hat{L}_{m} \cdot \hat{N}\right) i_{m, \mathrm{d}}+k_{\mathrm{s}}\left(\hat{R}_{m} \cdot \hat{V}\right)^{\alpha} i_{m, \mathrm{s}}\right)
$$

公式里的$k_{\mathrm{a}}$、$k_{\mathrm{d}}$和$k_{\mathrm{s}}$分别对应环境反射系数、漫反射系数和镜面反射系数。$\hat{L}_{m}$是入射光,$N$是法向量,$\hat{R}_{m}$是反射光,$V$是视线向量。$i$是强度,漫反射和镜面反射的强度可考虑因为距离的衰减。$⍺$是和物体材质有关的常量,决定了镜面高光的范围。

根据上面的公式,我们把多个光照的计算结果相加,就能得到光照下几何体的最终颜色了。不过,这里的Phong反射模型实际上是真实物理世界光照的简化模型,因为它只考虑光源的光作用于物体,没有考虑各个物体之间的反射光。所以我们最终实现出的效果也只是自然界效果的一种近似,不过这种近似也高度符合真实情况了。

在一般的图形库或者图形框架中,会提供符合Phong反射模型的物体材质,比如ThreeJS中,就支持各种光源和反射材质。

下面,我们来实现一下完整的Phong反射模型。它可以帮助你对这个模型有更深入的理解,让你以后使用ThreeJS等其他图形库,也能够更加得心应手。整个过程分为三步:定义光源模型、定义几何体材质和实现着色器。

1. 定义光源模型

我们先来定义光源模型对象。环境光比较特殊,我们将它单独抽象出来,放在一个ambientLight的属性中,而其他的光源一共有5个属性与材质无关,我列了一张表放在了下面。

这样,我们就可以定义一个Phong类。这个类由一个环境光属性和其他三种光源的集合组合而成,表示一个可以添加和删除光源的对象。它的主要作用是添加和删除光源,并把光源的属性通过uniforms访问器属性转换成对应的uniform变量,主要的代码如下:

class Phong {
  constructor(ambientLight = [0.5, 0.5, 0.5]) {
    this.ambientLight = ambientLight;
    this.directionalLights = new Set();
    this.pointLights = new Set();
    this.spotLights = new Set();
  }

  addLight(light) {
    const {position, direction, color, decay, angle} = light;
    if(!position && !direction) throw new TypeError('invalid light');
    light.color = color || [1, 1, 1];
    if(!position) this.directionalLights.add(light);
    else {
      light.decay = decay || [0, 0, 1];
      if(!angle) {
        this.pointLights.add(light);
      } else {
        this.spotLights.add(light);
      }
    }
  }

  removeLight(light) {
    if(this.directionalLights.has(light)) this.directionalLights.delete(light);
    else if(this.pointLights.has(light)) this.pointLights.delete(light);
    else if(this.spotLights.has(light)) this.spotLights.delete(light);
  }

  get uniforms() {
    const MAX_LIGHT_COUNT = 16; // 最多每种光源设置16个
    this._lightData = this._lightData || {};
    const lightData = this._lightData;

    lightData.directionalLightDirection = lightData.directionalLightDirection || {value: new Float32Array(MAX_LIGHT_COUNT * 3)};
    lightData.directionalLightColor = lightData.directionalLightColor || {value: new Float32Array(MAX_LIGHT_COUNT * 3)};

    lightData.pointLightPosition = lightData.pointLightPosition || {value: new Float32Array(MAX_LIGHT_COUNT * 3)};
    lightData.pointLightColor = lightData.pointLightColor || {value: new Float32Array(MAX_LIGHT_COUNT * 3)};
    lightData.pointLightDecay = lightData.pointLightDecay || {value: new Float32Array(MAX_LIGHT_COUNT * 3)};

    lightData.spotLightDirection = lightData.spotLightDirection || {value: new Float32Array(MAX_LIGHT_COUNT * 3)};
    lightData.spotLightPosition = lightData.spotLightPosition || {value: new Float32Array(MAX_LIGHT_COUNT * 3)};
    lightData.spotLightColor = lightData.spotLightColor || {value: new Float32Array(MAX_LIGHT_COUNT * 3)};
    lightData.spotLightDecay = lightData.spotLightDecay || {value: new Float32Array(MAX_LIGHT_COUNT * 3)};
    lightData.spotLightAngle = lightData.spotLightAngle || {value: new Float32Array(MAX_LIGHT_COUNT)};

    [...this.directionalLights].forEach((light, idx) => {
      lightData.directionalLightDirection.value.set(light.direction, idx * 3);
      lightData.directionalLightColor.value.set(light.color, idx * 3);
    });

    [...this.pointLights].forEach((light, idx) => {
      lightData.pointLightPosition.value.set(light.position, idx * 3);
      lightData.pointLightColor.value.set(light.color, idx * 3);
      lightData.pointLightDecay.value.set(light.decay, idx * 3);
    });

    [...this.spotLights].forEach((light, idx) => {
      lightData.spotLightPosition.value.set(light.position, idx * 3);
      lightData.spotLightColor.value.set(light.color, idx * 3);
      lightData.spotLightDecay.value.set(light.decay, idx * 3);
      lightData.spotLightDirection.value.set(light.direction, idx * 3);
      lightData.spotLightAngle.value[idx] = light.angle;
    });

    return {
      ambientLight: {value: this.ambientLight},
      ...lightData,
    };
  }
}

有了这个类之后,我们就可以创建并添加各种光源了。我在下面的代码中,添加了一个平行光和两个点光源,你可以看看。

const phong = new Phong();
// 添加一个平行光
phong.addLight({
  direction: [-1, 0, 0],
});
// 添加两个点光源
phong.addLight({
  position: [-3, 3, 0],
  color: [1, 0, 0],
});

phong.addLight({
  position: [3, 3, 0],
  color: [0, 0, 1],
});

2. 定义几何体材质

定义完光源之后,我们还需要定义几何体的材质(material),因为几何体材质决定了光反射的性质。

在前面的课程里,我们已经了解了一种与几何体材质有关的变量,即物体的反射率(MaterialReflection)。那在前面计算镜面反射的公式,float specular = 2.0 * pow(eyeCos, 50.0);中也有两个常量2.0和50.0,把它们也提取出来,我们就能得到两个新的变量。其中,2.0对应specularFactor,表示镜面反射强度,50.0指的是shininess,表示镜面反射的光洁度。

这样,我们就有了3个与材质有关的变量,分别是matrialReflection (材质反射率)、specularFactor (镜面反射强度)、以及shininess (镜面反射光洁度)。

然后,我们可以创建一个Matrial类,来定义物体的材质。与光源类相比,这个类非常简单,只是设置这三个参数,并通过uniforms访问器属性,获得它的uniform数据结构形式。

class Material {
  constructor(reflection, specularFactor = 0, shininess = 50) {
    this.reflection = reflection;
    this.specularFactor = specularFactor;
    this.shininess = shininess;
  }

  get uniforms() {
    return {
      materialReflection: {value: this.reflection},
      specularFactor: {value: this.specularFactor},
      shininess: {value: this.shininess},
    };
  }
}

那么,我们就可以创建matrial对象了。这里,我一共创建4个matrial对象,分别对应要显示的四个几何体的材质。

const matrial1 = new Material(new Color('#0000ff'), 2.0);
const matrial2 = new Material(new Color('#ff00ff'), 2.0);
const matrial3 = new Material(new Color('#008000'), 2.0);
const matrial4 = new Material(new Color('#ff0000'), 2.0);

有了phong对象和matrial对象,我们就可以给几何体创建WebGL程序了。那我们就使用上面四个WebGL程序,来创建真正的几何体网格,并将它们渲染出来吧。具体代码如下:

const program1 = new Program(gl, {
  vertex,
  fragment,
  uniforms: {
    ...matrial1.uniforms,
    ...phong.uniforms,
  },
});
const program2 = new Program(gl, {
  vertex,
  fragment,
  uniforms: {
    ...matrial2.uniforms,
    ...phong.uniforms,
  },
});
const program3 = new Program(gl, {
  vertex,
  fragment,
  uniforms: {
    ...matrial3.uniforms,
    ...phong.uniforms,
  },
});
const program4 = new Program(gl, {
  vertex,
  fragment,
  uniforms: {
    ...matrial4.uniforms,
    ...phong.uniforms,
  },
});

3. 实现着色器

接下来,我们重点看一下,支持phong反射模型的片元着色器代码是怎么实现的。这个着色器代码比较复杂,我们一段一段来看。

首先,我们来看光照相关的uniform变量的声明。这里,我们声明了vec3和float数组,数组的大小为16。这样,对于每一种光源,我们都可以支持16个。

#define MAX_LIGHT_COUNT 16
uniform mat4 viewMatrix;

uniform vec3 ambientLight;
uniform vec3 directionalLightDirection[MAX_LIGHT_COUNT];
uniform vec3 directionalLightColor[MAX_LIGHT_COUNT];
uniform vec3 pointLightColor[MAX_LIGHT_COUNT];
uniform vec3 pointLightPosition[MAX_LIGHT_COUNT];
uniform vec3 pointLightDecay[MAX_LIGHT_COUNT];
uniform vec3 spotLightColor[MAX_LIGHT_COUNT];
uniform vec3 spotLightDirection[MAX_LIGHT_COUNT];
uniform vec3 spotLightPosition[MAX_LIGHT_COUNT];
uniform vec3 spotLightDecay[MAX_LIGHT_COUNT];
uniform float spotLightAngle[MAX_LIGHT_COUNT];

uniform vec3 materialReflection;
uniform float shininess;
uniform float specularFactor;

接下来,我们实现计算phong反射模型的主题逻辑。事实上,处理平行光、点光源、聚光灯的主体逻辑类似,都是循环处理每个光源,再计算入射光方向,然后计算漫反射以及镜面反射,最终将结果返回。

float getSpecular(vec3 dir, vec3 normal, vec3 eye) {
  vec3 reflectionLight = reflect(-dir, normal);
  float eyeCos = max(dot(eye, reflectionLight), 0.0);
  return specularFactor *  pow(eyeCos, shininess);
}

vec4 phongReflection(vec3 pos, vec3 normal, vec3 eye) {
  float specular = 0.0;
  vec3 diffuse = vec3(0);

  // 处理平行光
  for(int i = 0; i < MAX_LIGHT_COUNT; i++) {
    vec3 dir = directionalLightDirection[i];
    if(dir.x == 0.0 && dir.y == 0.0 && dir.z == 0.0) continue;
    vec4 d = viewMatrix * vec4(dir, 0.0);
    dir = normalize(-d.xyz);
    float cos = max(dot(dir, normal), 0.0);
    diffuse += cos * directionalLightColor[i];
    specular += getSpecular(dir, normal, eye);
  }

  // 处理点光源
  for(int i = 0; i < MAX_LIGHT_COUNT; i++) {
    vec3 decay = pointLightDecay[i];
    if(decay.x == 0.0 && decay.y == 0.0 && decay.z == 0.0) continue;
    vec3 dir = (viewMatrix * vec4(pointLightPosition[i], 1.0)).xyz - pos;
    float dis = length(dir);
    dir = normalize(dir);
    float cos = max(dot(dir, normal), 0.0);
    float d = min(1.0, 1.0 / (decay.x * pow(dis, 2.0) + decay.y * dis + decay.z));
    diffuse += d * cos * pointLightColor[i];
    specular += getSpecular(dir, normal, eye);
  }

  // 处理聚光灯
  for(int i = 0; i < MAX_LIGHT_COUNT; i++) {
    vec3 decay = spotLightDecay[i];
    if(decay.x == 0.0 && decay.y == 0.0 && decay.z == 0.0) continue;

    vec3 dir = (viewMatrix * vec4(spotLightPosition[i], 1.0)).xyz - pos;
    float dis = length(dir);
    dir = normalize(dir);

    // 聚光灯的朝向
    vec3 spotDir = (viewMatrix * vec4(spotLightDirection[i], 0.0)).xyz;
    // 通过余弦值判断夹角范围
    float ang = cos(spotLightAngle[i]);
    float r = step(ang, dot(dir, normalize(-spotDir)));

    float cos = max(dot(dir, normal), 0.0);
    float d = min(1.0, 1.0 / (decay.x * pow(dis, 2.0) + decay.y * dis + decay.z));
    diffuse += r * d * cos * spotLightColor[i];
    specular += r * getSpecular(dir, normal, eye);
  }

  return vec4(diffuse, specular);
}

最后,我们在main函数中,调用phongReflection函数来合成颜色。代码如下:

void main() {
  vec3 eyeDirection = normalize(vCameraPos - vPos);
  vec4 phong = phongReflection(vPos, vNormal, eyeDirection);

  // 合成颜色
  gl_FragColor.rgb = phong.w + (phong.xyz + ambientLight) * materialReflection;
  gl_FragColor.a = 1.0;
}

最终呈现的视觉效果如下图所示:

你注意一下上图右侧的球体。因为我们一共设置了3个光源,一个平行光、两个点光源,它们都能够产生镜面反射。所以,这些光源叠加在一起后,这个球体就呈现出3个镜面高光。

Phong反射模型的局限性

虽然,phong反射模型已经比较接近于真实的物理模型,不过它仍然是真实模型的一种近似。因为它没有考虑物体反射光对其他物体的影响,也没有考虑物体对光线遮挡产生的阴影。

当然,我们可以完善这个模型。比如,将物体本身反射光(主要是镜面反射光)对其他物体的影响纳入到模型中。另外,我们也要考虑物体的阴影。当我们把这些因素更多地考虑进去的时候,我们的模型就会更加接近真实世界的物理模型。

当我们渲染3D图形的时候,要呈现越接近真实的效果,往往要考虑更多的参数,因此所需的计算量也越大,那我们就需要有更强的渲染能力,比如,更好的显卡,更快的CPU和GPU,并且也需要我们尽可能地优化计算的性能。

但是,有很多时候,我们需要在细节和性能上做出平衡和取舍。那性能优化的部分,也是我们课程的重点,我会在性能篇详细来讲。这节课,我们就重点关注反射模型,总结出完整的Phong反射模型就可以了。

要点总结

今天,我们把环境光、平行光、点光源、聚光灯这四种光源整合,并且在上节课讲的漫反射的基础上,添加了镜面反射,形成了完整的Phong反射模型。在这里,我们实现的着色器代码能够结合四种光源的效果,除了环境光外,每种光源还可以设置多个。

在Phong反射模型中,光照在物体上的最终效果,由各个光源的性质(参数)和物体的表面材质共同决定。

Phong反射模型也只是真实世界的一种近似,因为我们并没有考虑物体之间反射光的相互影响,也没有考虑光线的遮挡。如果把这些因素考虑进去,那我们的模型可以更接近真实世界了。

小试牛刀

我们知道,平行光、点光源和聚光灯是三种常见的方向光,但真实世界还有其他的方向光,比如探照灯,它是一种有范围的平行光,类似于聚光灯,但又不完全一样。你能给物体实现探照灯效果吗?

这里,我先把需要用到的参数告诉你,包括光源方向searchLightDirection、光源半径searchLightRadius、光源位置searchLightPosition、光照颜色searchLightColor。你可以用OGL实现探照灯效果,然后把对应的着色器代码写在留言区。

而且,探照灯的光照截面不一定是圆形,也可以是其他图形,比如三角形、菱形、正方形,你也可以试着让它支持不同的光照截面。

欢迎在留言区和我讨论,分享你的答案和思考,也欢迎你把这节课分享给你的朋友,我们下节课再见!


源码

课程中完整代码详见GitHub仓库

推荐阅读

Phong反射模型简介

精选留言(7)
  • 哈珀朋友 👍(2) 💬(0)

    基本等同于把计算机图形学复习了一遍哦

    2021-09-23

  • 量子蔷薇 👍(1) 💬(0)

    小试牛刀,这是圆形探照灯的实现: // 探照灯 for (int i = 0; i < MAX_LIGHT_COUNT; i++) { vec3 decay = searchLightDecay[i]; if (decay.x == 0.0 && decay.y == 0.0 && decay.z == 0.0) { continue; } vec3 dir = (viewMatrix * vec4(searchLightDirection[i], 0.0)).xyz; dir = normalize(-dir); float c = max(dot(dir, normal), 0.0); vec3 v = (viewMatrix * vec4(searchLightPosition[i], 1.0)).xyz - pos; float l = dot(v, dir); float d = min(1.0, 1.0 / (decay.x * pow(l, 2.0) + decay.y * l + decay.z)); float r = step(length(v - dir * l), searchLightRadius[i]) * step(0.0, dot(v, dir)); vec3 color = searchLightColor[i]; diffuse += c * d * r * color; specular += getSpecular(dir, normal, eye) * d * r * color; } 注意我的 specular 是 vec3 和老师的不一样。 入射光的方向 dir 的计算类似平行光,不受光源位置的影响。 v 是当前坐标到探照灯圆心的向量,与 dir 点乘后就可以得到 v 平行于 dir 方向的分量的长度 l(因为 dir 是单位向量,所以省去了 除以 length(dir) 的操作)。 d 是和老师一样的算法得到的衰减系数。 r 是实现圆形光照范围的关键,前面已经得到了 v 平行于 dir 的分量的长度 l,dir * l 就可以得到真正的分量 v∥,再用 v - v∥ 得到 v⊥,也就是 v 垂直于 dir 的分量,比较 v⊥ 的长度与探照灯半径的大小就能知道当前坐标是否在探照灯的范围内(以上其实就是点乘在向量分解上的应用)。 至此,还没有结束,探照灯是有 position 属性的,因此位于探照灯照射方向背面180°范围内的物体不应该被照亮,这是探照灯与平行光的另一个区别。所以需要判断探照灯圆心到当前坐标的向量也就是 -v 与照射方向也就是 -dir(dir 在 normalize 时取反了,所以这里再取反表示照射的方向)是否同向,或者说夹角是否小于 90°,也就是判断 dot(-v, -dir) 是否大于零,dot(v, dir) 与 dot(-v, -dir) 的结果是相同的,所以省去了取反的操作。

    2022-11-02

  • 👍(0) 💬(0)

    课后练习,三角形、菱形、正方形等多边形光照截面探照灯思路: 1.光照截面作三角剖分 2.坐标点与每个剖分出来的三角形组成四面体,把剖分出来的三角形三角形当做四面体的底面 3.判断每个四面体底面与其他三个面的夹角,如果三个夹角都小于等于90度,说明该坐标点能被探照到 4.计算两个面的夹角可以用两个面的法向量计算

    2024-06-22

  • 👍(0) 💬(0)

    字数限制,接上一个评论,补上顶点着色器代码和示例灯光输入: const vertex = /* glsl */ ` precision highp float; attribute vec3 position; attribute vec3 normal; uniform mat4 modelViewMatrix; uniform mat4 projectionMatrix; uniform mat3 normalMatrix; varying vec3 vNormal; varying vec3 vPos; void main() { vec4 pos = modelViewMatrix * vec4(position, 1.0); vPos = pos.xyz; vNormal = normalize(normalMatrix * normal); gl_Position = projectionMatrix * pos; } `; const ambientLight = {value: [0.2, 0.2, 0.1]}; const directional = { searchLightPosition: {value: [3, 3, 0]}, searchLightDirection: {value: [-1, -1, 0]}, searchLightRadius: {value: 1}, searchLightColor: {value: [1, 1, 1]}, };

    2024-06-22

  • 👍(0) 💬(0)

    圆形探照灯思路,不过用了二次方和开方,应该有更高效的方法 const fragment = /* glsl */ ` precision highp float; uniform mat4 viewMatrix; uniform vec3 ambientLight; uniform vec3 materialReflection; uniform vec3 searchLightDirection; uniform float searchLightRadius; uniform vec3 searchLightPosition; uniform vec3 searchLightColor; varying vec3 vNormal; varying vec3 vPos; void main() { // 圆心上方向向量 dir vec3 dir = (viewMatrix * vec4(searchLightDirection, 0.0)).xyz; // 圆心到当前点向量反向 invLight vec3 invLight = (viewMatrix * vec4(searchLightPosition, 1.0)).xyz - vPos; vec3 invNormal = normalize(invLight); // dir,invLight夹角在大于-90度到90度时,才能照到 // 当前点到圆心所在方向线的距离,小于半径,说明能照到 float cosTheta = dot(invNormal, normalize(-dir)); float r1 = step(0.0, cosTheta); float r2 = step(length(invLight) * sqrt( 1.0 - cosTheta * cosTheta),searchLightRadius); // 用了二次方和开方,感觉有更好的方法 // if (cosTheta >= 0.0 && length(invLight) * sqrt( 1.0 - cosTheta * cosTheta) <= searchLightRadius) { // 方向是dir vec3 dirNormal = normalize(dir); // 光线到点坐标的距离,用来计算衰减 // float dis = length(invLight) * cosTheta; // 与法线夹角余弦 float cos = max(dot(invNormal, vNormal), 0.0); vec3 diffuse = r1 * r2 * cos * searchLightColor; gl_FragColor.rgb = (ambientLight + diffuse) * materialReflection; // } else { // gl_FragColor.rgb = (ambientLight) * materialReflection; // } gl_FragColor.a = 1.0; } `;

    2024-06-22

  • Geek_00734e 👍(0) 💬(0)

    void main() { // 光线到点坐标的方向 vec3 invLight = (viewMatrix * vec4(searchLightPosition, 1.0)).xyz - vPos; // vec3 invLight = searchLightPosition - vPos; vec3 invNormal = normalize(invLight); // 光线到点坐标的距离,用来计算衰减 float dis = length(invLight); // 求光线中心与法线夹角的余弦 float cosmid = max(dot(normalize(vDir), vNormal), 0.0); // 求光线与法线夹角的余弦 float cosa = max(dot(invNormal, vNormal), 0.0); // 照射范围半径 float radius = searchLightRadius / cosmid; // 光线中心射线与射线夹角 float cosb = max(dot(normalize(vDir), invNormal), 0.0); // 根据正弦定理求对应边长度 float sinb = sqrt(1.0 - cosb * cosb); float sinc = sin(PI/2.0 - acos(cosa) - acos(cosb)); float lenb = dis * sinb / sinc; // 边长度小于半径的为1.0 float r = step(lenb, radius); // 计算衰减 float decay = min(1.0, 1.0 / (searchLightDecayFactor.x * pow(dis, 2.0) + searchLightDecayFactor.y * dis + searchLightDecayFactor.z)); // 计算漫反射 vec3 diffuse = r * decay * cosmid * searchLightColor; // 合成颜色 gl_FragColor.rgb = (ambientLight + diffuse) * materialReflection; gl_FragColor.a = 1.0; } 探照灯 但是感觉算法 不够完美,实在找不到合适的思路了

    2022-01-19

  • Geek_00734e 👍(0) 💬(0)

    探照灯那个课后作业有答案吗?思来想去做不出来,我的思路是求光源中心射线跟界面的交点,光线照射到的范围为 r/cos(θ) (θ为光线与界面法线夹角), 可是这个思路要求射线与平面交点,这个不知道怎么求,是不是我思路有问题,感觉不大对。 我的理解光源方向 searchLightDirection这个参数可以参照平行光的方式、光源位置 searchLightPosition用于计算距离衰减、光源半径 searchLightRadius 用于确定照射到的范围 这个逻辑怎么算

    2022-01-18