地址
上海市闵行区申虹路666弄
正荣中心2号楼
手机/微信
180-1627-5139
文章发表于:2013年9月
阅读时间:5分钟
灯光是电脑生成图像最重要的部分之一。为了得到最终真实可信的展示效果,需要进行大量计算、设置以及微调工作。
球面反射/环境贴图技术(SEM)是一种模拟(译注:此处表达的是伪造、仿造的意思,表示这种技术并没有真正的使用着色器中的光照算法来实现,只是用一张静态的位图来模拟反射光。如果你还是理解不了,那么就理解为这就是一张最基础的颜色贴图,然后在颜色贴图上画上了反射光或反射环境。)光照算法中高光反射的快捷方法,在特定的使用场景中,甚至可以模拟完整光照实现效果。这种技术已经在三维软件中广泛应用,如: Pixologic ZBrush 和 Luxology Modo。
SEM会使用特制的纹理贴图,这种贴图被叫做“lit spheres”或“matcaps”。它们就是基本的球面反射贴图,但是显示效果上有更多漫反射颜色,而不是像下图中最左侧的金属反光球一样将场景清晰的反射出来。
这种球面贴图包含了在相机前方展示的所有元素,也就说此贴图包含了入射光线照射到的、面朝相机的球体表面(译注:模型背朝相机部分的反射因为相机没有拍摄到,所以贴图中也没有办法表达)。这也是它不能作为完美环境贴图(译注:原文为perfect environment map,应该指的是Cube map、Equirectangular这种捕获到360°的环境贴图方式)的原因:因为缺失了背朝相机部分的图像信息,所以这种反射不能跟随相机视角旋转而旋转。
那么,我们所能模拟的就是相机和灯光固定不动、模型自身转动的显示效果(译注:类似淘宝商家拍摄商品展示照片,相机是放在三脚架上固定的,灯光也是固定的(或使用摄影专用灯箱),将产品放在一个转盘上,每拍一张照片底部的转盘就旋转一定角度。见图示)。
SEM的基本思路是使用从片元上法向量获取的UV坐标(此坐标用来查找matCap纹理)替代模型对象的原始纹理坐标。即可以在顶点着色器中完成(GPU会处理插值),也可以在片元着色器中完成。我们会先实现逐顶点的版本。
为了全面的理解SEM,我们假定一个完全正对相机镜头的平面(即平面的法向量与相机的向量平行)被映射到正正好好位于matCap纹理中心的纹素上;随着法向量从相机向量偏移(即两个向量打破平行状态,夹角从0°趋向于90°),被映射到的纹素就会越靠近边缘。指向上方的法线(我们说的是屏幕空间,所以这里的“上方”指的是物理屏幕的顶部)被映射到matCap纹理的顶部,指向下方的法线被映射到matCap纹理的底部。法线指向左和右也是同理。
所以,第一步我们需要设置两个向量,e(Eye)和n(Normal)。Eye是从相机(空间中在原点上的一个点。此处原文为“a point in space at the origin”,不确定是否翻译准确)射到片元位置的向量。Normal是在空间屏幕中的法线。我们需要将三维位置转换成四维向量,使它能够和矩阵相乘。
一旦我们有了这两个向量,我们就能计算反射向量了。
这个教程是基于GLSL语言编写着色器的。
如果你使用了其他着色语言,并且该语言没有
reflect()
函数,你可以使用相对应的表达式替代:r = e - 2. * dot( n, e ) * n
;
我们获取向量,然后应用到公式中得到UV元组。
这是顶点着色器的代码:
//SEM shader, per-vertex
//GLSL - Vertex shader source
varying vec2 vN;
void main() {
vec4 p = vec4( position, 1. );
vec3 e = normalize( vec3( modelViewMatrix * p ) );
vec3 n = normalize( normalMatrix * normal );
vec3 r = reflect( e, n );
float m = 2. * sqrt(
pow( r.x, 2. ) +
pow( r.y, 2. ) +
pow( r.z + 1., 2. )
);
vN = r.xy / m + .5;
gl_Position = projectionMatrix * modelViewMatrix * p;
}
片元着色器获取了UV元组经过GPU插值后的值,然后使用它查找matCap纹理上对应的颜色值。这是代码:
//SEM shader, per-vertex
//GLSL - Fragment shader source
uniform sampler2D tMatCap;
varying vec2 vN;
void main() {
vec3 base = texture2D( tMatCap, vN ).rgb;
gl_FragColor = vec4( base, 1. );
}
在JavaScript代码,使用了thrss.js创建着色器材质。实例化了一个新的THREE.ShaderMaterial
,引用了上文定义的顶点着色器和片元着色器脚本,在一个材质uniform
对象中使用了matCap贴图。以防万一,将水平和垂直的纹理包裹模式设置为THREE.ClampToEdgeWrapping
,纹理边缘不会被卷曲(译注:猜测这边作者的意思是纹理不会被重复铺贴或者镜像铺贴,其实根据three.js开发文档中的描述,.wrapS
和.wrapT
的默认值就是THREE.ClampToEdgeWrapping
)。这是代码:
//Creating THREE.js material
//JavaScript - index.html
var material = new THREE.ShaderMaterial( {
uniforms: {
tMatCap: {
type: 't',
value: THREE.ImageUtils.loadTexture( 'matcap.jpg' )
},
},
vertexShader: document.getElementById( 'sem-vs' ).textContent,
fragmentShader: document.getElementById( 'sem-fs' ).textContent,
shading: THREE.SmoothShading
} );
material.uniforms.tMatCap.value.wrapS =
material.uniforms.tMatCap.value.wrapT =
THREE.ClampToEdgeWrapping;
材质准备完毕,已经可以指定给模型对象了。
使用SEM材质很适合表达多边形上的不同类型高光,比如:褶皱、凹凸、甚至缓慢的波动。但在立方体上的表现效果不理想。在球体上效果是完全没有变化的,SEM在一个球体上的效果和matCap纹理的平面投影的效果完全相同(译注:也就说球体上的效果就是matCap纹理贴图的效果,不论你旋转摄像机镜头到任何角度,都是这个显示效果,不会有任何变化)。使用环面纽结几何体作为这个着色器测试模型是一个非常好的选择。
我并不认为逐片元着色是真正必要的,但可能也取决于被渲染的模型,特别是被渲染的模型是低多边形面片的(译注:模型顶点数很少,精度很低)。在这种情况下,你可能需要逐片元的去计算,而不是依靠GPU插值。下面是使用逐片元着色修改后顶点着色器:
// SEM shader, per-fragment
// GLSL - vertex shader source
varying vec3 e;
varying vec3 n;
void main() {
e = normalize( vec3( modelViewMatrix * vec4( position, 1.0 ) ) );
n = normalize( normalMatrix * normal );
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1. );
}
注意我们没有计算vN
元组,并使用varying
传递到片元着色器中,而是使用e
(Eye)和n
(Normal)值替代,并传递到片元着色器中。下面是片元着色器的代码:
//SEM shader, per-fragment
//GLSL - fragment shader source
uniform sampler2D tMatCap;
varying vec3 e;
varying vec3 n;
void main() {
vec3 r = reflect( e, n );
float m = 2. * sqrt( pow( r.x, 2. ) + pow( r.y, 2. ) + pow( r.z + 1., 2. ) );
vec2 vN = r.xy / m + .5;
vec3 base = texture2D( tMatCap, vN ).rgb;
gl_FragColor = vec4( base, 1. );
}
所以,之前在顶点着色器中被计算的reflection
和vN
值,在被插值后传递到片元着色器中。而在现在,每个片元都会计算reflection
和vN
值。
这个示例展示三种不同的材质(Matte red,Shiny black 和 Skin),被用于三个不同的模型(Torus Knot, Bolb 和 Suzanne)。他们的材质着色器是完全相同的,唯一的不同是matCap纹理贴图。这展现这种技术惊人的强大能力,它使模型对象的效果完全不同。
勾选“Enable Phong(per-pixel) shading”复选框后,可以观察逐顶点和逐片元之前的差异。最值得关注的差异在模型对象的边缘处。
Torus Knot是three.js的自带的几何体对象THREE.TorusKnotGeometry
。Bolb是在THREE.IcosahedronGeometry
几何体对象上通过Perlin噪音函数(混合了标准噪音和crinkly噪音)进行了顶点扰乱。Suzanne是Blender的吉祥物,并使用THREE.SubdivisionModifier
进行细分后得到的一个更光滑模型。
这个示例可以在OSX、Windows和Linux平台的Chrome、Firefox、Safari浏览器上正常工作。也能工作在Android平台的Chrome和Firefox。在移动端试一下,这个示例支持触摸事件。
就这么多,SEM也没什么神秘之处。它的关键之处在于使用优秀的模型以及优秀的matCap纹理贴图。谷歌图片搜索、加上图像编辑软件、最好再有一个专业的美术,这就够了!
可以基于这种SEM的技术进行更多尝试探索:
原文链接:Creating a Spherical Reflection/Environment Mapping shader by Jaume Sanchez.