上一篇文章学习了RayMarching,但是做出的场景的效果是黑白,这一篇在上一篇内容的基础上,进行改进,使用Blinn-Phong光照模型渲染出不同色彩的物体,下面是demo的最终效果,不同的物体有了属于自己的色彩
shader-blinn-phong
场景中照射在物体表面的光线要分为三种
反应到物体表面颜色的计算方式就是 表面颜色 = 环境光 + 漫反射光 + 镜面反射光
替换为公式如下
之前学了一下二维场景下SDF图形的函数,下面的函数就是demo中需要用到的几个简单的三维SDF函数
//球体
float sdSphere( vec3 p, float s )
{
return length(p)-s;
}
//立方体
float sdBox( vec3 p, vec3 b,float rad )
{
vec3 d = abs(p) - b;
return min(max(d.x,max(d.y,d.z)),0.0) + length(max(d,0.0)) - rad;
}
//甜圈圈
float sdTorus( vec3 p, vec2 t )
{
return length( vec2(length(p.xz)-t.x,p.y) )-t.y;
}
main函数中原来rayMarch返回一个float型数据表示光线从视点出发与物体相交的距离,这一次除了要返回这个距离还要返回一个材质ID,用来给不同模型渲染不同的颜色,所以返回的是二维向量
vec2 res = rayMarch(ro,rd);//反向光线追踪求交点距离与材质ID
float d = res.x;//物体与视点的距离
float m = res.y;//材质ID
光线步进函数也需要调整,返回值变为vec2,x分量表示距离,y分量表示材质ID,具体如下
vec2 rayMarch(vec3 rayStart, vec3 rayDirection) {
float depth=0.;
float material=0.;
for(int i=0; i<MAX_STEPS; i++) {
vec3 p = rayStart + rayDirection*depth;//上一次步进结束后的坐标也就是这一次步进出发点
vec2 dm = getDistandMaterial(p);
float dist = dm.x;//获取当前步进出发点与物体相交时距离
material = dm.y;
depth += dist; //步进长度累加
if(depth>MAX_DIST || dist<SURF_DIST) break;//步进距离大于最大步进距离或与物体表面距离小于最小表面距离(光线进入物体)停止前进
}
return vec2(depth,material);
}
获取光线与物体相交距离函数也是同样的处理方式,具体如下
vec2 opU( vec2 d1, vec2 d2 )
{
return (d1.x<d2.x) ? d1 : d2;
}
vec2 getDistandMaterial(vec3 p){
float plane = p.y;//地面
vec2 res = vec2(plane,0.0);
vec2 sphere = vec2(sdSphere(p-vec3(0,1,5),0.8),1.0);//球体,材质ID为1.0
vec2 box = vec2(sdBox(p-vec3(2,1,5),vec3(0.8,0.3,0.6),0.06),2.0);//方块,材质ID为2.0
vec2 torus = vec2(sdTorus(p-vec3(-2,1,5),vec2(0.6,0.2)),3.0);//甜圈圈,材质ID为3.0
res = opU(res,sphere);
res = opU(res,box);
res = opU(res,torus);
return res;
}
获取法向量的函数调整,之前是直接使用计算距离函数的返回值相减,现在调整为使用返回值的x分量相减
vec3 getNormal(vec3 p){
return normalize(vec3(
getDistandMaterial(vec3(p.x + SURF_DIST, p.y, p.z)).x - getDistandMaterial(vec3(p.x - SURF_DIST, p.y, p.z)).x,
getDistandMaterial(vec3(p.x, p.y + SURF_DIST, p.z)).x - getDistandMaterial(vec3(p.x, p.y - SURF_DIST, p.z)).x,
getDistandMaterial(vec3(p.x, p.y, p.z + SURF_DIST)).x - getDistandMaterial(vec3(p.x, p.y, p.z - SURF_DIST)).x
));
}
这一部分与上一篇文章中的过程完全不同,是使用全新的计算方法,首先在main函数中,依据光线步进函数返回的材质ID为不同物体设置不同的表面颜色,具体如下
void main( void ) {
//窗口坐标调整为[-1,1],坐标原点在屏幕中心
vec2 st = (gl_FragCoord.xy * 2. - u_resolution) / u_resolution.y;
vec3 ro = vec3(0.0,2.0,0.0);//视点
vec3 rd = normalize(vec3(st.x,st.y,1.0));//视线方向
vec2 res = rayMarch(ro,rd);//反向光线追踪求交点距离与材质ID
float d = res.x;//物体与视点的距离
float m = res.y;//材质ID
vec3 p = ro + rd * d;
vec3 materialColor = vec3(1.0, 1.0, 1.0);
//为不同物体设置不同的材质颜色
if(m==0.0){
materialColor = vec3(.2, 0.0, 0.0);
}
if(m==1.0){
materialColor = vec3(.2, 0.0, 1.0);
}
if(m==2.0){
materialColor = vec3(.7, 0.2, 0.0);
}
if(m==3.0){
materialColor = vec3(.8, .9, 0.0);
}
vec3 color = vec3(1.0,1.0,1.0);
//使用Blinn-Phong模型计算光照
color *= calcBlinnPhongLight( materialColor, p, ro);
gl_FragColor = vec4(color, 1.0);
}
这一步是最重要的部分,依据Blinn-Phong模型计算光照,分别计算出物体表面的环境光、漫反射光 和 镜面反射光(高光),首先贴一下漫反射模型的示意图
Blinn-Phong模型计算高光示意图
对上面两个示意图整体说明一下,
这个是用来计算光照的衰减,点光源从光源发射点开始在三维空间中像一个不断变大的球体一样扩散能量,距离球心的距离越远,到达物体表面的能量越小,而这个关系正好和这个距离的平方成反比
公式中的max函数是为了剔除大于90°的光线,我们实现是使用clamp函数剔除的
公式中计算高光的部分,使用了指数函数,指数为p,在视点位置能看到高光,是反射光与视线的夹角很接近,通常随着这个夹角慢慢变大,就慢慢看不到高光了,这个指数p就是加速衰减的,也就是这个p值越大,看到的高光范围越小
具体实现过程如下
//Blinn-Phong模型光照计算
vec3 calcBlinnPhongLight( vec3 materialColor, vec3 p, vec3 ro) {
vec3 lightPos = vec3(4.0 * sin(u_time), 20.0, 4.0*cos(u_time));//光源坐标
//计算环境光
float k_a = 0.2;//环境光反射系数
vec3 ambientLight = 0.6 * vec3(1.0, 1.0, 1.0);
vec3 ambient = k_a*ambientLight;
vec3 N = getNormal(p); //法线
vec3 L = normalize(lightPos - p); //光照方向
vec3 V = normalize(ro - p); //视线
vec3 H = normalize(V+L); //半程向量
float r = length(lightPos - p);
//计算漫反射光
float k_d = 0.6;//漫反射系数
float dotLN = clamp(dot(L, N),0.0,1.0);//点乘,并将结果限定在0~1
vec3 diffuse = k_d * (materialColor/r*r) * dotLN;
//计算高光反射光
float k_s = 0.8;//镜面反射系数
float shininess = 160.0;
vec3 specularColor = vec3(1.0, 1.0, 1.0);
vec3 specular = k_s * (specularColor/r*r)* pow(clamp(dot(N, H), 0.0, 1.0), shininess);//计算高光
//计算阴影
vec2 res = rayMarch(p + N*SURF_DIST*2.0,L);
if(res.x<length(lightPos-p)-0.001){
diffuse*=0.1;
}
//颜色 = 环境光 + 漫反射光 + 镜面反射光
return ambient +diffuse + specular;
}
老规矩,贴上所有代码
<body>
<div id="container"></div>
<script src="http://www.yanhuangxueyuan.com/versions/threejsR92/build/three.js"></script>
<script>
var container;
var camera, scene, renderer;
var uniforms;
var vertexShader = `
void main() {
gl_Position = vec4( position, 1.0 );
}
`
var fragmentShader = `
#ifdef GL_ES
precision mediump float;
#endif
uniform float u_time;
uniform vec2 u_mouse;
uniform vec2 u_resolution;
const int MAX_STEPS = 100;//最大步进步数
const float MAX_DIST = 100.0;//最大步进距离
const float SURF_DIST = 0.01;//相交检测临近表面距离
vec2 opU( vec2 d1, vec2 d2 )
{
return (d1.xMAX_DIST || dist`
init();
animate();
function init() {
container = document.getElementById('container');
camera = new THREE.Camera();
camera.position.z = 1;
scene = new THREE.Scene();
var geometry = new THREE.PlaneBufferGeometry(2, 2);
uniforms = {
u_time: {
type: "f",
value: 1.0
},
u_resolution: {
type: "v2",
value: new THREE.Vector2()
},
u_mouse: {
type: "v2",
value: new THREE.Vector2()
}
};
var material = new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: vertexShader,
fragmentShader: fragmentShader
});
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
renderer = new THREE.WebGLRenderer();
//renderer.setPixelRatio(window.devicePixelRatio);
container.appendChild(renderer.domElement);
onWindowResize();
window.addEventListener('resize', onWindowResize, false);
document.onmousemove = function (e) {
uniforms.u_mouse.value.x = e.pageX
uniforms.u_mouse.value.y = e.pageY
}
}
function onWindowResize(event) {
renderer.setSize(800, 800);
uniforms.u_resolution.value.x = renderer.domElement.width;
uniforms.u_resolution.value.y = renderer.domElement.height;
}
function animate() {
requestAnimationFrame(animate);
render();
}
function render() {
uniforms.u_time.value += 0.02;
renderer.render(scene, camera);
}
</script>
</body>