1. demo overview and effects
The last article studied RayMarching, but the effect of the scene was black and white. Based on the previous article, this article improves it by using the Blinn-Phong lighting model to render objects of different colors. Below is the final effect of demo. Different objects have their own colors
shader-blinn-phong
2. Introduction of Blinn-Phong lighting model
There are three types of light that shine on the surface of an object in a scene
- Ambient light
- Diffuse reflectance
- Mirror Reflection (High Light Reflection)
Surface color = ambient light + diffuse light + specular light
Replace with the following formula
3. demo implementation
3.1 Scene object preparation
Previously, I learned the functions of SDF graphics in two-dimensional scenes. The following functions are simple three-dimensional SDF functions needed in demo
//sphere float sdSphere( vec3 p, float s ) { return length(p)-s; } //Cube 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; } //Doughnut float sdTorus( vec3 p, vec2 t ) { return length( vec2(length(p.xz)-t.x,p.y) )-t.y; }
3.2 rayMarching Adjustment
In the main function, the original rayMarch returns a float data representing the distance from the viewpoint at which light intersects the object. This time, in addition to returning this distance, it also returns a material ID, so it returns a two-dimensional vector
vec2 res = rayMarch(ro,rd);//Inverse ray tracing for intersection distance and material ID float d = res.x;//Distance between object and viewpoint float m = res.y;//Material ID
The light step function also needs to be adjusted, the return value becomes vec2, the x component represents distance, and the y component represents material ID, as follows
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;//The coordinates after the last step are the starting point of this step vec2 dm = getDistandMaterial(p); float dist = dm.x;//Gets the distance when the current starting point of the step intersects the object material = dm.y; depth += dist; //Step Length Accumulation if(depth>MAX_DIST || dist<SURF_DIST) break;//Step distance greater than maximum step distance or less than minimum surface distance from object surface (light entering object) stops moving } return vec2(depth,material); }
Getting the distance function of the intersection of a light and an object is done the same way, as follows
vec2 opU( vec2 d1, vec2 d2 ) { return (d1.x<d2.x) ? d1 : d2; } vec2 getDistandMaterial(vec3 p){ float plane = p.y;//ground vec2 res = vec2(plane,0.0); vec2 sphere = vec2(sdSphere(p-vec3(0,1,5),0.8),1.0);//Sphere, material ID 1.0 vec2 box = vec2(sdBox(p-vec3(2,1,5),vec3(0.8,0.3,0.6),0.06),2.0);//Square, Material ID 2.0 vec2 torus = vec2(sdTorus(p-vec3(-2,1,5),vec2(0.6,0.2)),3.0);//Doughnut, material ID 3.0 res = opU(res,sphere); res = opU(res,box); res = opU(res,torus); return res; }
The function adjustment to get the normal vector, which used to subtract directly from the return value of the computed distance function, is now adjusted to subtract using the x component of the return value
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 )); }
3.3 Blinn-Phong Lighting Calculation
This part is completely different from the process in the previous article. Using a new calculation method, first set different surface colors for different objects in the main function based on the material ID returned by the light step function, as follows:
void main( void ) { //Window coordinates adjusted to [-1,1], coordinate origin at the center of the screen vec2 st = (gl_FragCoord.xy * 2. - u_resolution) / u_resolution.y; vec3 ro = vec3(0.0,2.0,0.0);//viewpoint vec3 rd = normalize(vec3(st.x,st.y,1.0));//Line of sight direction vec2 res = rayMarch(ro,rd);//Inverse ray tracing for intersection distance and material ID float d = res.x;//Distance between object and viewpoint float m = res.y;//Material ID vec3 p = ro + rd * d; vec3 materialColor = vec3(1.0, 1.0, 1.0); //Set different material colors for different objects 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); //Calculating illumination using Blinn-Phong model color *= calcBlinnPhongLight( materialColor, p, ro); gl_FragColor = vec4(color, 1.0); }
This step is the most important part. First, paste a diagram of the diffuse reflection model.
Blinn-Phong model calculates highlight diagrams
To illustrate the above two diagrams as a whole,
This is used to calculate the decay of light. A point light source diffuses energy like a growing sphere in three-dimensional space starting from the point where the light source emits. The farther it is from the center of the sphere, the less energy it reaches the surface of the object. This relationship is inversely proportional to the square of the distance.
max function In order to eliminate light greater than 90 degrees, we use clamp function to eliminate
When calculating highlights, there is also an exponential p. When you see highlights at the viewpoint, the angle between the reflected light and the line of sight is very close. Usually, as the angle increases slowly, you will no longer see highlights. This exponential p accelerates the attenuation, that is, the larger the p value, the smaller the range of highlights you see.
The implementation process is as follows
//Illumination calculation of Blinn-Phong model vec3 calcBlinnPhongLight( vec3 materialColor, vec3 p, vec3 ro) { vec3 lightPos = vec3(4.0 * sin(u_time), 20.0, 4.0*cos(u_time));//Light source coordinates //Computing ambient light float k_a = 0.2;//Ambient light reflection coefficient vec3 ambientLight = 0.6 * vec3(1.0, 1.0, 1.0); vec3 ambient = k_a*ambientLight; vec3 N = getNormal(p); //normal vec3 L = normalize(lightPos - p); //Illumination direction vec3 V = normalize(ro - p); //sight vec3 H = normalize(V+L); //Half-range vector float r = length(lightPos - p); //Calculating diffuse reflectance float k_d = 0.6;//Diffuse reflectance float dotLN = clamp(dot(L, N),0.0,1.0);//Point multiplies and limits results to 0~1 vec3 diffuse = k_d * (materialColor/r*r) * dotLN; //Calculating high-light reflection float k_s = 0.8;//Mirror Reflectance 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);//Calculate highlights //Calculate Shadow vec2 res = rayMarch(p + N*SURF_DIST*2.0,L); if(res.x<length(lightPos-p)-0.001){ diffuse*=0.1; } //Color = ambient light + diffuse light + specular light return ambient +diffuse + specular; }
4. demo code
Old rules, paste all code
<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;//Maximum Step Number const float MAX_DIST = 100.0;//Maximum Step Distance const float SURF_DIST = 0.01;//Intersection Detection Near Surface Distance vec2 opU( vec2 d1, vec2 d2 ) { return (d1.x<d2.x) ? d1 : d2; } //sphere float sdSphere( vec3 p, float s ) { return length(p)-s; } //Cube 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; } //Doughnut float sdTorus( vec3 p, vec2 t ) { return length( vec2(length(p.xz)-t.x,p.y) )-t.y; } vec2 getDistandMaterial(vec3 p){ float plane = p.y;//ground vec2 res = vec2(plane,0.0); vec2 sphere = vec2(sdSphere(p-vec3(0,1,5),0.8),1.0);//Sphere, material ID 1.0 vec2 box = vec2(sdBox(p-vec3(2,1,5),vec3(0.8,0.3,0.6),0.06),2.0);//Square, Material ID 2.0 vec2 torus = vec2(sdTorus(p-vec3(-2,1,5),vec2(0.6,0.2)),3.0);//Doughnut, material ID 3.0 res = opU(res,sphere); res = opU(res,box); res = opU(res,torus); return res; } 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;//The coordinates after the last step are the starting point of this step vec2 dm = getDistandMaterial(p); float dist = dm.x;//Gets the distance when the current starting point of the step intersects the object material = dm.y; depth += dist; //Step Length Accumulation if(depth>MAX_DIST || dist<SURF_DIST) break;//Step distance greater than maximum step distance or less than minimum surface distance from object surface (light entering object) stops moving } return vec2(depth,material); } 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 )); } //Illumination calculation of Blinn-Phong model vec3 calcBlinnPhongLight( vec3 materialColor, vec3 p, vec3 ro) { vec3 lightPos = vec3(4.0 * sin(u_time), 20.0, 4.0*cos(u_time));//Light source coordinates //Computing ambient light float k_a = 0.2;//Ambient light reflection coefficient vec3 ambientLight = 0.6 * vec3(1.0, 1.0, 1.0); vec3 ambient = k_a*ambientLight; vec3 N = getNormal(p); //normal vec3 L = normalize(lightPos - p); //Illumination direction vec3 V = normalize(ro - p); //sight vec3 H = normalize(V+L); //Half-range vector float r = length(lightPos - p); //Calculating diffuse reflectance float k_d = 0.6;//Diffuse reflectance float dotLN = clamp(dot(L, N),0.0,1.0);//Point multiplies and limits results to 0~1 vec3 diffuse = k_d * (materialColor/r*r) * dotLN; //Calculating high-light reflection float k_s = 0.8;//Mirror Reflectance 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);//Calculate highlights //Calculate Shadow vec2 res = rayMarch(p + N*SURF_DIST*2.0,L); if(res.x<length(lightPos-p)-0.001){ diffuse*=0.1; } //Color = ambient light + diffuse light + specular light return ambient +diffuse + specular; } void main( void ) { //Window coordinates adjusted to [-1,1], coordinate origin at the center of the screen vec2 st = (gl_FragCoord.xy * 2. - u_resolution) / u_resolution.y; vec3 ro = vec3(0.0,2.0,0.0);//viewpoint vec3 rd = normalize(vec3(st.x,st.y,1.0));//Line of sight direction vec2 res = rayMarch(ro,rd);//Inverse ray tracing for intersection distance and material ID float d = res.x;//Distance between object and viewpoint float m = res.y;//Material ID vec3 p = ro + rd * d; vec3 materialColor = vec3(1.0, 1.0, 1.0); //Set different material colors for different objects 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); //Calculating illumination using Blinn-Phong model color *= calcBlinnPhongLight( materialColor, p, ro); gl_FragColor = vec4(color, 1.0); } ` 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>