mirror of
https://github.com/mouse0w0/lwjglbook-CN-Translation.git
synced 2025-08-23 04:35:29 +08:00
550 lines
37 KiB
HTML
Executable File
550 lines
37 KiB
HTML
Executable File
<!DOCTYPE html>
|
||
<html class="writer-html5" lang="en" >
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="author" content="Mouse0w0" />
|
||
<link rel="shortcut icon" href="../img/favicon.ico" />
|
||
<title>要有光 - Lwjglbook中文翻译</title>
|
||
<link rel="stylesheet" href="../css/theme.css" />
|
||
<link rel="stylesheet" href="../css/theme_extra.css" />
|
||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github.min.css" />
|
||
|
||
<script>
|
||
// Current page data
|
||
var mkdocs_page_name = "\u8981\u6709\u5149";
|
||
var mkdocs_page_input_path = "10-let-there-be-light.md";
|
||
var mkdocs_page_url = null;
|
||
</script>
|
||
|
||
<!--[if lt IE 9]>
|
||
<script src="../js/html5shiv.min.js"></script>
|
||
<![endif]-->
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
|
||
<script>hljs.highlightAll();</script>
|
||
</head>
|
||
|
||
<body class="wy-body-for-nav" role="document">
|
||
|
||
<div class="wy-grid-for-nav">
|
||
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
|
||
<div class="wy-side-scroll">
|
||
<div class="wy-side-nav-search">
|
||
<a href=".." class="icon icon-home"> Lwjglbook中文翻译
|
||
</a><div role="search">
|
||
<form id ="rtd-search-form" class="wy-form" action="../search.html" method="get">
|
||
<input type="text" name="q" placeholder="Search docs" aria-label="Search docs" title="Type search term here" />
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="Navigation menu">
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="../01-first-steps/">事前准备</a>
|
||
</li>
|
||
</ul>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="../02-the-game-loop/">游戏循环</a>
|
||
</li>
|
||
</ul>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="../03-a-brief-about-coordinates/">坐标简介</a>
|
||
</li>
|
||
</ul>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="../04-rendering/">渲染</a>
|
||
</li>
|
||
</ul>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="../05-more-on-rendering/">渲染补充</a>
|
||
</li>
|
||
</ul>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="../06-transformations/">变换</a>
|
||
</li>
|
||
</ul>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="../07-textures/">纹理</a>
|
||
</li>
|
||
</ul>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="../08-camera/">摄像机</a>
|
||
</li>
|
||
</ul>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="../09-loading-more-complex-models/">加载更复杂的模型</a>
|
||
</li>
|
||
</ul>
|
||
<ul class="current">
|
||
<li class="toctree-l1 current"><a class="reference internal current" href="#">要有光</a>
|
||
<ul class="current">
|
||
<li class="toctree-l2"><a class="reference internal" href="#_1">环境光分量</a>
|
||
</li>
|
||
<li class="toctree-l2"><a class="reference internal" href="#_2">漫反射</a>
|
||
</li>
|
||
<li class="toctree-l2"><a class="reference internal" href="#_3">镜面反射</a>
|
||
</li>
|
||
<li class="toctree-l2"><a class="reference internal" href="#_4">衰减</a>
|
||
</li>
|
||
<li class="toctree-l2"><a class="reference internal" href="#_5">实现</a>
|
||
</li>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="../11-let-there-be-even-more-light/">要有更多的光</a>
|
||
</li>
|
||
</ul>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="../12-game-hud/">游戏HUD</a>
|
||
</li>
|
||
</ul>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="../13-sky-box-and-some-optimizations/">天空盒与一些优化</a>
|
||
</li>
|
||
</ul>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="../14-height-maps/">高度图</a>
|
||
</li>
|
||
</ul>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="../15-terrain-collisions/">地形碰撞</a>
|
||
</li>
|
||
</ul>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="../16-fog/">雾</a>
|
||
</li>
|
||
</ul>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="../17-normal-mapping/">法线贴图</a>
|
||
</li>
|
||
</ul>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="../18-shadows/">阴影</a>
|
||
</li>
|
||
</ul>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="../19-animations/">动画</a>
|
||
</li>
|
||
</ul>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="../20-particles/">粒子</a>
|
||
</li>
|
||
</ul>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="../21-instanced-rendering/">实例化渲染</a>
|
||
</li>
|
||
</ul>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="../22-audio/">音效</a>
|
||
</li>
|
||
</ul>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="../23-3d-object-picking/">三维物体选取</a>
|
||
</li>
|
||
</ul>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="../24-hud-revisited/">回顾HUD - NanoVG</a>
|
||
</li>
|
||
</ul>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="../25-optimizations-frustum-culling/">优化 - 截锥剔除</a>
|
||
</li>
|
||
</ul>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="../26-cascaded-shadow-maps/">级联阴影映射</a>
|
||
</li>
|
||
</ul>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="../27-assimp/">Assimp库</a>
|
||
</li>
|
||
</ul>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="../28-deferred-shading/">延迟着色法</a>
|
||
</li>
|
||
</ul>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="../a01-opengl-debugging/">附录 A - OpenGL调试</a>
|
||
</li>
|
||
</ul>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="../glossary/">术语表</a>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
|
||
<nav class="wy-nav-top" role="navigation" aria-label="Mobile navigation menu">
|
||
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
|
||
<a href="..">Lwjglbook中文翻译</a>
|
||
|
||
</nav>
|
||
<div class="wy-nav-content">
|
||
<div class="rst-content"><div role="navigation" aria-label="breadcrumbs navigation">
|
||
<ul class="wy-breadcrumbs">
|
||
<li><a href=".." class="icon icon-home" aria-label="Docs"></a></li>
|
||
<li class="breadcrumb-item active">要有光</li>
|
||
<li class="wy-breadcrumbs-aside">
|
||
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/edit/master/docs/10-let-there-be-light.md" class="icon icon-github"> Edit on GitHub</a>
|
||
</li>
|
||
</ul>
|
||
<hr/>
|
||
</div>
|
||
<div role="main" class="document" itemscope="itemscope" itemtype="http://schema.org/Article">
|
||
<div class="section" itemprop="articleBody">
|
||
|
||
<h1 id="let-there-be-light">要有光(Let there be light)</h1>
|
||
<p>在本章中,我们将学习如何为3D游戏引擎添加光照。我们不会去实现一个完美的光照模型,因为抛开复杂性不谈,它还需要消耗大量的计算机资源,相反我们只需要一个近似的、像样的光照效果:我们将使用名为 <strong>Phong</strong> 的着色算法(由Bui Tuong Phong开发)。另一个需要注意的是,我们将只模拟光照,但不会模拟这些光照所产生的阴影(这将在后续章节中实现)。</p>
|
||
<p>在开始之前,先定义几个光照类型:</p>
|
||
<ul>
|
||
<li><strong>点光源(Point Light)</strong>:这种光模拟的是一个由点向空间各个方向均匀发射的光源。</li>
|
||
<li><strong>聚光源(Spot Light)</strong>:这种光模拟从空间中的点发射的光源,但不是向所有方向上发射,而是限定在了一个锥形方向上。</li>
|
||
<li><strong>平行光(Directional Light)</strong>:这种光模拟了太阳光,3D场景中的所有物体都会受到来自特定方向的平行光线的照射。无论物体是近抑或是远,光线总是以相同角度照射在物体上。</li>
|
||
<li><strong>环境光(Ambient Light)</strong>:这种光来自空间的任何方向,并以相同的强度照亮所有物体。</li>
|
||
</ul>
|
||
<p><img alt="光照类型" src="../_static/10/light_types.png" /></p>
|
||
<p>因此,为了模拟光照,我们需要考虑光照的类型、位置和其他一些参数,如颜色。当然,我们还必须考虑物体如何受光照影响吸收和反射光。</p>
|
||
<p>Phong着色算法将模拟光线对我们模型中每个点的影响,即对每个顶点的影响。这就是为什么它被称为局部光照模型的原因,这也是该算法不能计算阴影的原因:它只会计算应用到每个顶点的光照,而不考虑顶点是否位于遮光物体之后。我们将在此后的章节中解决这个问题。但正因如此,它是一种非常简单快速的算法,并且可以提供非常好的效果。我们将在此实现一个没有深入考虑材质影响的简化版本。</p>
|
||
<p>Phong算法提供了三种光照分量:</p>
|
||
<ul>
|
||
<li><strong>环境光(Ambient Light)</strong>:模拟来自任何方向的光照,它将照亮(需要对应强度值)未受任何光线照射的区域,就像背景光。</li>
|
||
<li><strong>漫反射(Diffuse Reflectance)</strong>:考虑到面向光源的表面更亮。</li>
|
||
<li><strong>镜面反射(Specular Reflectance)</strong>:模拟光线在抛光或金属表面上的反射。</li>
|
||
</ul>
|
||
<p>最后,我们要得到一个因数,将它与指定片元的颜色相乘,根据它所受的光照将该颜色变得更亮或更暗。令<script type="math/tex">A</script>为环境光、<script type="math/tex">D</script>为漫反射光、<script type="math/tex">S</script>为镜面反射光,目标因数将是上述分量的总和:</p>
|
||
<p>
|
||
<script type="math/tex; mode=display">L = A + D + S</script>
|
||
</p>
|
||
<p>这些分量其实就是颜色,也就是每个光照分量所贡献的颜色分量。这是因为光照分量不仅会提供一定程度的亮度,还会改变模型的颜色。在我们的片元着色器中,只需将该光照的颜色与原始片元颜色(从纹理或基色获得的)相乘即可。</p>
|
||
<p>我们还可以为相同的材质指定不同的颜色,这些颜色将用于环境光、漫反射和镜面反射分量。因此,这些分量将由材质关联的颜色调整。如果材质有纹理,我们只需为每个分量使用一个纹理。</p>
|
||
<p>所以对于无纹理的材质的最终颜色将是:</p>
|
||
<p>
|
||
<script type="math/tex; mode=display">L = A * 环境光的颜色 + D * 漫反射的颜色 + S * 镜面反射的颜色</script>
|
||
</p>
|
||
<p>对于有纹理的材质的最终颜色将是:</p>
|
||
<p>
|
||
<script type="math/tex; mode=display">L = A * 纹理颜色 + D * 纹理颜色 + S * 纹理颜色</script>
|
||
</p>
|
||
<h2 id="_1">环境光分量</h2>
|
||
<p>让我们来看看第一个分量,即环境光分量,它只是一个常量,会使我们的所有物体变得更亮或更暗。我们可以使用它来模拟一天中特定时间段内的光照(黎明、黄昏等),也可以用它来添加一些不直接被光线照射到,但可以以简单的方式被间接光线照射(比如反射)到的点的光照。</p>
|
||
<p>环境光是最容易计算的分量,我们只需要传递一种颜色,它将与基色相乘,以调整该基色。假如我们已经确定片元的颜色是<script type="math/tex">(1.0, 0.0, 0.0)</script>,即红色。如果没有环境光时,它将显示为完全红色的片元。如果我们将环境光设置为<script type="math/tex">(0.5, 0.5, 0.5)</script>,则最终颜色将为<script type="math/tex">(0.5, 0.0, 0.0)</script>,其实就是变暗的红色。这种光照会以相同的方式使所有片元变暗(称其为使物体变暗的光似乎有点奇怪,事实上这就是我们得到的效果)。此外,如果光色的RGB分量不相同,它还可以为片元添加一些颜色,所以我们只需要一个矢量来调节环境光强度和颜色。</p>
|
||
<h2 id="_2">漫反射</h2>
|
||
<p>现在我们来谈谈漫反射,它模拟了这样一个现象:与光线垂直的面看起来比以更接近与光线平行的角度接收光线的面更亮。一个物体接收到的光越多,其光密度(在此这样称呼)就越高。</p>
|
||
<p><img alt="漫反射光" src="../_static/10/diffuse_light.png" /></p>
|
||
<p>但是,我们该如何计算它?你还记得上一章中我们介绍过的法线的概念吗?法线是垂直于平面并且长度为1的向量。因此,让我们为上图中的三个点的绘制法线。如你所见,每个点的法线将是垂直于每个点的切平面的向量。我们不用绘制来自光源的光线,而是绘制从每个点到光源(即相反的方向)的向量。</p>
|
||
<p><img alt="法线与光线的方向" src="../_static/10/diffuse_light_normals.png" /></p>
|
||
<p>如你所见,<script type="math/tex">P1</script>点的法线<script type="math/tex">N1</script>与指向光源的向量平行,该法线的方向与光线的方向相反(<script type="math/tex">N1</script>已被移动,以便你可以看到它,但这在数学上是等价的)。<script type="math/tex">P1</script>与指向光源的向量所成的夹角等于<script type="math/tex">0</script>。因为它的切平面垂直于光源,所以<script type="math/tex">P1</script>将是最亮的点。</p>
|
||
<p>
|
||
<script type="math/tex">P2</script>点的法线<script type="math/tex">N2</script>与指向光源的向量所成的夹角约为30度,所以它应该比<script type="math/tex">P1</script>更暗。最后,<script type="math/tex">P3</script>的法线<script type="math/tex">N3</script>也与指向光源的向量平行,但两个向量的方向相反。<script type="math/tex">P3</script>与指向光源的向量的角度为180度,所以根本不应该接收到任何光线。</p>
|
||
<p>因此,我们似乎得到了一个计算某点的光照强度的好方法,光强与该点的法线和该点指向光源的向量之间的夹角大小有关。但我们要怎么计算它呢?</p>
|
||
<p>有一个我们可以使用的数学运算————数量积(又称为点积)。该运算需要两个向量并得到一个数字(标量),如果它们之间的角度较小,则得到一个正数;如果它们之间的角度很大,则得到一个负数。如果两个向量都被归一化,即两者的长度都等于1,那么数量积的结果将介于<script type="math/tex">-1</script>和<script type="math/tex">1</script>之间。如果两个向量的方向相同(即夹角为<script type="math/tex">0</script>),则数量积为1;如果两个向量夹角为直角,则它的值为<script type="math/tex">0</script>;如果两个向量的方向相反,则为<script type="math/tex">-1</script>。</p>
|
||
<p>我们定义两个向量,<script type="math/tex">v1</script>和<script type="math/tex">v2</script>,并以<script type="math/tex">α</script>作为它们之间的夹角。数量积的定义如下:</p>
|
||
<p><img alt="数量积" src="../_static/10/dot_product.png" /></p>
|
||
<p>如果两个向量都归一化,即它们的长度,或它们的模长等于1,它们的数量积即为夹角的余弦值。我们将使用该运算来计算漫反射分量。</p>
|
||
<p>所以我们需要计算指向光源的向量,该怎么做呢?假如我们有每个点的位置(即顶点位置)和光源的位置。首先,这两个坐标必须位于同一个坐标空间中。为了简化,我们假设它们都处于世界坐标系中,那么这些位置是指向顶点位置(<script type="math/tex">VP</script>)和光源(<script type="math/tex">VS</script>)的向量的坐标,如下图所示:</p>
|
||
<p><img alt="漫反射光照运算I" src="../_static/10/diffuse_calc_i.png" /></p>
|
||
<p>如果我们从<script type="math/tex">VP</script>中减去<script type="math/tex">VS</script>,就会得到我们所需的向量,称为<script type="math/tex">L</script>。</p>
|
||
<p>现在,我们可以在指向光源的矢量和法线之间做数量积,因为Johann Lambert是第一个提出这种关系来模拟平面亮度的,所以该乘积被称为兰伯特项。</p>
|
||
<p>让我们总结一下,定义以下变量:</p>
|
||
<ul>
|
||
<li>
|
||
<script type="math/tex">vPos</script> :我们的顶点在模型观察空间坐标系中的位置。</li>
|
||
<li>
|
||
<script type="math/tex">lPos</script>:观察空间坐标系中的光源位置。</li>
|
||
<li>
|
||
<script type="math/tex">intensity</script>:光的强度(从0到1)。</li>
|
||
<li>
|
||
<script type="math/tex">lCourour</script>:光的颜色。</li>
|
||
<li>
|
||
<script type="math/tex">normal</script>:顶点法线。</li>
|
||
</ul>
|
||
<p>首先我们需要计算从当前位置指向光源的向量:<script type="math/tex">toLightDirection = lPos - vPos</script>,该运算的结果需要归一化。</p>
|
||
<p>然后我们需要计算漫反射因数(标量):<script type="math/tex">diffuseFactor = normal \cdot toLightDirection</script>。计算两向量之间的数量积,我们希望值介于<script type="math/tex">-1</script>和<script type="math/tex">1</script>之间,所以两个向量都需要归一化。颜色需要介于<script type="math/tex">0</script>到<script type="math/tex">1</script>之间,所以如果值低于<script type="math/tex">0</script>,我们将其设为<script type="math/tex">0</script>。</p>
|
||
<p>最后,我们只需要通过漫反射因数和光的强度来调节光的颜色:</p>
|
||
<p>
|
||
<script type="math/tex; mode=display"> color = diffuseColour * lColour * diffuseFactor * intensity</script>
|
||
</p>
|
||
<h2 id="_3">镜面反射</h2>
|
||
<p>现在看到镜面反射,但首先我们需要知道光线是如何反射的。当光照射到一个表面时,它的一部分被吸收,另一部分被反射,如果你还记得你的物理课知识,反射就是光从物体反弹回来。</p>
|
||
<p><img alt="光反射" src="../_static/10/light_reflection.png" /></p>
|
||
<p>当然,物体表面不是完全抛光的,如果你仔细观察,你会看到很多不平整的地方。此外还有许多光线(实际上是光子),会撞击这个表面,并且会以不同的角度进行反射。因此,我们看到的就像是一束光照射表面并散射回来。也就是说,光在撞击表面时会发散,这就是我们之前讨论过的漫反射分量。</p>
|
||
<p><img alt="表面" src="../_static/10/surface.png" /></p>
|
||
<p>但是,当光线照射抛光表面时,例如金属,光线会受到较低的扩散影响,并且在它撞到表面时,大部分会向反方向反射。</p>
|
||
<p><img alt="抛光平面" src="../_static/10/polished_surface.png" /></p>
|
||
<p>这就是镜面反射分量的模型,它取决于材质属性。对于镜面反射,重要的是只有当摄像机处于恰当的位置时,即反射光的反射区域内,反射光才可见。</p>
|
||
<p><img alt="镜面光照" src="../_static/10/specular_lightining.png" /></p>
|
||
<p>既然已经解释了镜面反射之中的机理,我们接下来准备计算这个分量。首先我们需要一个从光源指向顶点的向量。当我们计算漫反射分量时,我们使用的是方向与之相反的向量,它指向的是光源,即<script type="math/tex">toLightDirection</script>。所以让我们将其计算为<script type="math/tex">fromLightDirection = -(toLightDirection)</script>。</p>
|
||
<p>然后我们需要考虑到表面的法线,来计算由<script type="math/tex">fromLightDirection</script>射出的光线撞击表面所产生的反射光。有一个名为<code>reflect</code>的GLSL函数实现了该功能。所以,<script type="math/tex">reflectLight = reflect(fromLightSource, normal)</script>。</p>
|
||
<p>我们还需要一个指向摄像机的向量,并将其命名为<script type="math/tex">cameraDirection</script>,然后计算出摄像机位置和顶点位置之间的差值:<script type="math/tex">cameraDirection = cameraPos - vPos</script>。摄像机位置向量和顶点位置需要处于相同的坐标系中,并且得到的向量需要归一化。下图概述了我们目前计算的主要分量:</p>
|
||
<p><img alt="镜面光照计算" src="../_static/10/specular_lightining_calc.png" /></p>
|
||
<p>现在我们需要计算光强,即<script type="math/tex">specularFactor</script>。如果<script type="math/tex">cameraDirection</script>和<script type="math/tex">reflectLight</script>向量指向相同的方向,该值就越高,如果它们方向相反其值则越低。为了计算该值我们将再次运用数量积。<script type="math/tex">specularFactor = cameraDirection \cdot reflectLight</script>。我们只希望该值位于<script type="math/tex">0</script>和<script type="math/tex">1</script>之间,所以如果它低于<script type="math/tex">0</script>,就将它设为0。</p>
|
||
<p>我们还需要考虑到,如果摄像机指向反射光锥,则该光更强烈。这可以通过计算<script type="math/tex">specularFactor</script>的<script type="math/tex">specularPower</script>次幂来实现,其中<script type="math/tex">specularPower</script>为给定的参数:</p>
|
||
<p>
|
||
<script type="math/tex; mode=display">specularFactor = specularFactor^{specularPower}</script>
|
||
</p>
|
||
<p>最后,我们需要对材质的反射率进行建模,如果光线反射,反射率将调节反射光的强度,这将用到一个名为<code>reflectance</code>的参数。所以镜面反射分量的颜色为:</p>
|
||
<p>
|
||
<script type="math/tex; mode=display">specularColour * lColour * reflectance * specularFactor * intensity</script>
|
||
</p>
|
||
<h2 id="_4">衰减</h2>
|
||
<p>我们现在知道如何计算这三个分量了,这些分量可以帮助我们用环境光模拟点光源。但是我们的光照模型还不完整,因为物体反射的光与光源的距离无关,也就是说,我们需要模拟光线衰减。</p>
|
||
<p>衰减是一个与距离和光有关的函数。光的强度与距离的平方成反比。这很容易理解,随着光线的传播,其能量沿着球体表面分布,其半径等于光线行进的距离,而球的表面积与其半径的平方成正比。我们可以用下式来计算衰减因子:<script type="math/tex">1.0 /(atConstant + atLineardist + atExponentdist ^ {2})</script>。</p>
|
||
<p>为了模拟衰减,我们只需要将衰减因数乘以最终颜色即可。</p>
|
||
<h2 id="_5">实现</h2>
|
||
<p>现在我们可以开始编程实现上述的所有概念,我们将从着色器开始。大部分工作将在片元着色器中完成,但我们还需要将顶点着色器中的一些数据传递给它。在上一章中,片元着色器仅接收纹理坐标,现在我们还将传递两个参数:</p>
|
||
<ul>
|
||
<li>已转换为模型观察空间坐标系并已归一化的顶点法线。</li>
|
||
<li>已转换为模型观察空间坐标系的顶点位置。</li>
|
||
</ul>
|
||
<p>顶点着色器的代码如下所示:</p>
|
||
<pre><code class="language-glsl">#version 330
|
||
|
||
layout (location=0) in vec3 position;
|
||
layout (location=1) in vec2 texCoord;
|
||
layout (location=2) in vec3 vertexNormal;
|
||
|
||
out vec2 outTexCoord;
|
||
out vec3 mvVertexNormal;
|
||
out vec3 mvVertexPos;
|
||
|
||
uniform mat4 modelViewMatrix;
|
||
uniform mat4 projectionMatrix;
|
||
|
||
void main()
|
||
{
|
||
vec4 mvPos = modelViewMatrix * vec4(position, 1.0);
|
||
gl_Position = projectionMatrix * mvPos;
|
||
outTexCoord = texCoord;
|
||
mvVertexNormal = normalize(modelViewMatrix * vec4(vertexNormal, 0.0)).xyz;
|
||
mvVertexPos = mvPos.xyz;
|
||
}
|
||
</code></pre>
|
||
<p>在我们继续讲解片元着色器之前,必须强调一个非常重要的概念。从上述代码可以看到,<code>mvVertexNormal</code>,该变量包含已转换为模型观察空间坐标的顶点法线。这是通过将<code>vertexNormal</code>乘上<code>modelViewMatrix</code>来实现的,就像顶点位置一样。但有一个细微的区别,该顶点法线的w分量在乘以矩阵之前被设置为0:<code>vec4(vertexNormal, 0.0)</code>。我们为什么要这样做呢?因为我们希望法线被旋转和缩放,但我们不希望它被平移,所以我们只对它的方向感兴趣,而不是它的位置。而这是通过将w分量设置为0来实现的,这也是是使用齐次坐标的优点之一,通过设置w分量,我们可以控制应用的变换。你可以在纸上做矩阵乘法,看看为什么是这样。</p>
|
||
<p>现在我们可以开始在片元着色器中开工了,除了将来自顶点着色器的值声明为输入参数之外,我们将定义一些有用的结构体来建模光照和材质属性。首先我们定义用于建模光的结构体。</p>
|
||
<pre><code class="language-glsl">struct Attenuation
|
||
{
|
||
float constant;
|
||
float linear;
|
||
float exponent;
|
||
};
|
||
|
||
struct PointLight
|
||
{
|
||
vec3 colour;
|
||
// 光源位置是在观察坐标系中的
|
||
vec3 position;
|
||
float intensity;
|
||
Attenuation att;
|
||
};
|
||
</code></pre>
|
||
<p>点光源由一个颜色,一个位置,以及一个介于<script type="math/tex">0</script>和<script type="math/tex">1</script>之间的数字(用于模拟光照强度)和一组用于模拟衰减方程的参数定义。</p>
|
||
<p>建模材质属性的结构体如下:</p>
|
||
<pre><code class="language-glsl">struct Material
|
||
{
|
||
vec4 ambient;
|
||
vec4 diffuse;
|
||
vec4 specular;
|
||
int hasTexture;
|
||
float reflectance;
|
||
};
|
||
</code></pre>
|
||
<p>材质由一组颜色定义(假如我们不使用纹理为片元着色):</p>
|
||
<ul>
|
||
<li>用于环境光分量的颜色。</li>
|
||
<li>用于漫反射分量的颜色。</li>
|
||
<li>用于镜面反射分量的颜色。</li>
|
||
</ul>
|
||
<p>材质还由一个标志控制它是否拥有关联的纹理和反射率指数定义。我们将在片元着色器中使用以下Uniform。</p>
|
||
<pre><code class="language-glsl">uniform sampler2D texture_sampler;
|
||
uniform vec3 ambientLight;
|
||
uniform float specularPower;
|
||
uniform Material material;
|
||
uniform PointLight pointLight;
|
||
uniform vec3 camera_pos;
|
||
</code></pre>
|
||
<p>我们用新建的Uniform设置以下变量:</p>
|
||
<ul>
|
||
<li>环境光:储存颜色,以同样方式影响每个片元。</li>
|
||
<li>镜面反射强度(在讨论镜面反射光时给出的方程式中使用的指数)。</li>
|
||
<li>一个点光源。</li>
|
||
<li>材质属性。</li>
|
||
<li>摄像机在观察空间坐标系中的位置。</li>
|
||
</ul>
|
||
<p>我们还将定义一些全局变量,它们将储存要在环境、漫反射和镜面反射中使用的材质颜色分量。我们使用这些变量是因为如果分量具有纹理,我们将对所有分量使用相同的颜色,并且我们不希望进行冗余的纹理查找。这些变量的定义如下:</p>
|
||
<pre><code class="language-glsl">vec4 ambientC;
|
||
vec4 diffuseC;
|
||
vec4 speculrC;
|
||
</code></pre>
|
||
<p>我们现在可以定义一个函数,来根据材质属性设置这些变量:</p>
|
||
<pre><code class="language-glsl">void setupColours(Material material, vec2 textCoord)
|
||
{
|
||
if (material.hasTexture == 1)
|
||
{
|
||
ambientC = texture(texture_sampler, textCoord);
|
||
diffuseC = ambientC;
|
||
speculrC = ambientC;
|
||
}
|
||
else
|
||
{
|
||
ambientC = material.ambient;
|
||
diffuseC = material.diffuse;
|
||
speculrC = material.specular;
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>现在我们要定义一个函数,它以点光源、顶点位置及其法线为输入并返回此前描述的漫反射和镜面反射分量计算的颜色。</p>
|
||
<pre><code class="language-glsl">vec4 calcPointLight(PointLight light, vec3 position, vec3 normal)
|
||
{
|
||
vec4 diffuseColour = vec4(0, 0, 0, 0);
|
||
vec4 specColour = vec4(0, 0, 0, 0);
|
||
|
||
// 漫反射
|
||
vec3 light_direction = light.position - position;
|
||
vec3 to_light_source = normalize(light_direction);
|
||
float diffuseFactor = max(dot(normal, to_light_source ), 0.0);
|
||
diffuseColour = diffuseC * vec4(light.colour, 1.0) * light.intensity * diffuseFactor;
|
||
|
||
// 镜面反射
|
||
vec3 camera_direction = normalize(-position);
|
||
vec3 from_light_source = -to_light_source;
|
||
vec3 reflected_light = normalize(reflect(from_light_source, normal));
|
||
float specularFactor = max( dot(camera_direction, reflected_light), 0.0);
|
||
specularFactor = pow(specularFactor, specularPower);
|
||
specColour = speculrC * specularFactor * material.reflectance * vec4(light.colour, 1.0);
|
||
|
||
// 衰减
|
||
float distance = length(light_direction);
|
||
float attenuationInv = light.att.constant + light.att.linear * distance +
|
||
light.att.exponent * distance * distance;
|
||
return (diffuseColour + specColour) / attenuationInv;
|
||
}
|
||
</code></pre>
|
||
<p>上述代码相对比较直白简单,它只是计算了漫反射分量的颜色,另一个是计算镜面反射的颜色,并通过光线在到达我们正在处理的顶点时受到的衰减来调整它们。</p>
|
||
<p>请注意,顶点坐标是位于观察空间中的。在计算镜面反射时,我们得出到观察点(即摄像机位置)的方向,代码如下:</p>
|
||
<pre><code class="language-glsl"> vec3 camera_direction = normalize(camera_pos - position);
|
||
</code></pre>
|
||
<p>但是,由于<code>position</code>位于观察空间中,摄像机位置始终位于原点,即<script type="math/tex">(0, 0, 0)</script>,所以我们按如下代码计算它:</p>
|
||
<pre><code class="language-glsl"> vec3 camera_direction = normalize(vec3(0, 0, 0) - position);
|
||
</code></pre>
|
||
<p>可以简化为:</p>
|
||
<pre><code class="language-glsl"> vec3 camera_direction = normalize(-position);
|
||
</code></pre>
|
||
<p>有了上述函数,顶点着色器的主函数就变得非常简单了。</p>
|
||
<pre><code class="language-glsl">void main()
|
||
{
|
||
setupColours(material, outTexCoord);
|
||
|
||
vec4 diffuseSpecularComp = calcPointLight(pointLight, mvVertexPos, mvVertexNormal);
|
||
|
||
fragColor = ambientC * vec4(ambientLight, 1) + diffuseSpecularComp;
|
||
}
|
||
</code></pre>
|
||
<p>调用<code>setupColours</code>函数将使用适当的颜色来设置变量<code>ambientC</code>、<code>diffuseC</code>和<code>speculrC</code>。然后,我们计算漫反射和镜面反射分量,并考虑到衰减。为了方便起见,我们使用单个函数调用来实现此操作,如上所述。最终的颜色是通过添加环境光分量来计算的(将<code>ambientC</code>乘以环境光)。如你所见,环境光不受衰减的影响。</p>
|
||
<p>在着色器中我们引入了一些需要进一步解释的新概念,我们正在定义结构体并将它们用作Uniform。但我们要怎么传递这些结构体?首先,我们将定义两个新类,它们建模点光源和材质属性,名为<code>PointLight</code>和<code>Material</code>。它们只是普通的Java对象,所以你可以在本书附带的源代码中查看它们。然后,我们需要在<code>ShaderProgram</code>类中创建新方法,首先要能够为点光源和材质结构体创建Uniform。</p>
|
||
<pre><code class="language-java">public void createPointLightUniform(String uniformName) throws Exception {
|
||
createUniform(uniformName + ".colour");
|
||
createUniform(uniformName + ".position");
|
||
createUniform(uniformName + ".intensity");
|
||
createUniform(uniformName + ".att.constant");
|
||
createUniform(uniformName + ".att.linear");
|
||
createUniform(uniformName + ".att.exponent");
|
||
}
|
||
|
||
public void createMaterialUniform(String uniformName) throws Exception {
|
||
createUniform(uniformName + ".ambient");
|
||
createUniform(uniformName + ".diffuse");
|
||
createUniform(uniformName + ".specular");
|
||
createUniform(uniformName + ".hasTexture");
|
||
createUniform(uniformName + ".reflectance");
|
||
}
|
||
</code></pre>
|
||
<p>如你所见,它非常简单,我们只为构成结构体的所有属性创建一个单独的Uniform。现在我们需要创建另外两个方法来设置这些Uniform的值,并将<code>PointLight</code>和<code>Material</code>的实例作为参数。</p>
|
||
<pre><code class="language-java">public void setUniform(String uniformName, PointLight pointLight) {
|
||
setUniform(uniformName + ".colour", pointLight.getColor() );
|
||
setUniform(uniformName + ".position", pointLight.getPosition());
|
||
setUniform(uniformName + ".intensity", pointLight.getIntensity());
|
||
PointLight.Attenuation att = pointLight.getAttenuation();
|
||
setUniform(uniformName + ".att.constant", att.getConstant());
|
||
setUniform(uniformName + ".att.linear", att.getLinear());
|
||
setUniform(uniformName + ".att.exponent", att.getExponent());
|
||
}
|
||
|
||
public void setUniform(String uniformName, Material material) {
|
||
setUniform(uniformName + ".ambient", material.getAmbientColour());
|
||
setUniform(uniformName + ".diffuse", material.getDiffuseColour());
|
||
setUniform(uniformName + ".specular", material.getSpecularColour());
|
||
setUniform(uniformName + ".hasTexture", material.isTextured() ? 1 : 0);
|
||
setUniform(uniformName + ".reflectance", material.getReflectance());
|
||
}
|
||
</code></pre>
|
||
<p>在本章源代码中,你还将看到我们还修改了<code>Mesh</code>类来储存材质实例,并且创建了一个简单的示例,在其中创建了一个可用“N”和“M”键控制移动的点光源,以显示点光源聚焦在反射率值高于0的网格上时是怎样的。</p>
|
||
<p>让我们回到片元着色器,如上所述,我们需要另一个储存摄像机位置<code>camera_pos</code>的Uniform。这些坐标必须位于观察空间中。通常我们将在世界空间坐标系中设置光源坐标,因此我们需要将它们乘以观察矩阵,以便能够在着色器中使用它们,所以需要在<code>Transformation</code>类中创建一个新方法,该方法返回观察矩阵以便变换光源坐标。</p>
|
||
<pre><code class="language-java">// 获得光源对象的副本并将它的坐标变换到观察空间坐标系
|
||
PointLight currPointLight = new PointLight(pointLight);
|
||
Vector3f lightPos = currPointLight.getPosition();
|
||
Vector4f aux = new Vector4f(lightPos, 1);
|
||
aux.mul(viewMatrix);
|
||
lightPos.x = aux.x;
|
||
lightPos.y = aux.y;
|
||
lightPos.z = aux.z;
|
||
shaderProgram.setUniform("pointLight", currPointLight);
|
||
</code></pre>
|
||
<p>我们不会写上完整的源代码,因为如果这样这一章就太长了,且对于解释清楚概念没有太多的帮助,你可以在本书附带的源代码中查阅源代码。</p>
|
||
<p><img alt="光照效果" src="../_static/10/lightning_result.png" /></p>
|
||
|
||
</div>
|
||
</div><footer>
|
||
<div class="rst-footer-buttons" role="navigation" aria-label="Footer Navigation">
|
||
<a href="../09-loading-more-complex-models/" class="btn btn-neutral float-left" title="加载更复杂的模型"><span class="icon icon-circle-arrow-left"></span> Previous</a>
|
||
<a href="../11-let-there-be-even-more-light/" class="btn btn-neutral float-right" title="要有更多的光">Next <span class="icon icon-circle-arrow-right"></span></a>
|
||
</div>
|
||
|
||
<hr/>
|
||
|
||
<div role="contentinfo">
|
||
<!-- Copyright etc -->
|
||
<p>2019, Mouse0w0</p>
|
||
</div>
|
||
|
||
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/readthedocs/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
|
||
</footer>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
</section>
|
||
|
||
</div>
|
||
|
||
<div class="rst-versions" role="note" aria-label="Versions">
|
||
<span class="rst-current-version" data-toggle="rst-current-version">
|
||
|
||
<span>
|
||
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
|
||
</span>
|
||
|
||
|
||
<span><a href="../09-loading-more-complex-models/" style="color: #fcfcfc">« Previous</a></span>
|
||
|
||
|
||
<span><a href="../11-let-there-be-even-more-light/" style="color: #fcfcfc">Next »</a></span>
|
||
|
||
</span>
|
||
</div>
|
||
<script src="../js/jquery-3.6.0.min.js"></script>
|
||
<script>var base_url = "..";</script>
|
||
<script src="../js/theme_extra.js"></script>
|
||
<script src="../js/theme.js"></script>
|
||
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML"></script>
|
||
<script src="../search/main.js"></script>
|
||
<script>
|
||
jQuery(function () {
|
||
SphinxRtdTheme.Navigation.enable(true);
|
||
});
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|