preface
Hello, this is CSS magic – Alphardex.
In this article we will use three.js to achieve a cool optical effect — dew drops. We know that when dewdrops fall off an object, they produce a sticky effect. In 2D, this adhesive effect can be easily achieved using CSS filters. But in the 3D world, it’s not that easy, and we rely on lighting, which involves a key algorithm called Ray Marching. Here’s a look at the final implementation
Ah, Hajima route!
The preparatory work
The author’s three.js template can be copied by clicking on fork in the lower right corner
positive
Full screen camera
First change the camera to an orthogonal one, and then adjust the length of the plane to 2 so that it fills the screen
class RayMarching extends Base {
constructor(sel: string, debug: boolean) {
super(sel, debug);
this.clock = new THREE.Clock();
this.cameraPosition = new THREE.Vector3(0.0.0);
this.orthographicCameraParams = {
left: -1.right: 1.top: 1.bottom: -1.near: 0.far: 1.zoom: 1
};
}
/ / initialization
init() {
this.createScene();
this.createOrthographicCamera();
this.createRenderer();
this.createRayMarchingMaterial();
this.createPlane();
this.createLight();
this.trackMousePos();
this.addListeners();
this.setLoop();
}
// Create plane
createPlane() {
const geometry = new THREE.PlaneBufferGeometry(2.2.100.100);
const material = this.rayMarchingMaterial;
this.createMesh({ geometry, material }); }}Copy the code
Create the material
Create a shader material that defines all the parameters to be passed to the shader
const matcapTextureUrl = "https://i.loli.net/2021/02/27/7zhBySIYxEqUFW3.png";
class RayMarching extends Base {
// Create a raytrace material
createRayMarchingMaterial() {
const loader = new THREE.TextureLoader();
const texture = loader.load(matcapTextureUrl);
const rayMarchingMaterial = new THREE.ShaderMaterial({
vertexShader: rayMarchingVertexShader,
fragmentShader: rayMarchingFragmentShader,
side: THREE.DoubleSide,
uniforms: {
uTime: {
value: 0
},
uMouse: {
value: new THREE.Vector2(0.0)},uResolution: {
value: new THREE.Vector2(window.innerWidth, window.innerHeight)
},
uTexture: {
value: texture
},
uProgress: {
value: 1
},
uVelocityBox: {
value: 0.25
},
uVelocitySphere: {
value: 0.5
},
uAngle: {
value: 1.5
},
uDistance: {
value: 1.2}}});this.rayMarchingMaterial = rayMarchingMaterial; }}Copy the code
Vertex shader rayMarchingVertexShader, which is a ready-made template
The focus is on fragment shader rayMarchingFragmentShader
Chip shader
background
As a warm-up exercise, create a radial background
varying vec2 vUv;
vec3 background(vec2 uv){
float dist=length(uv-vec2(. 5));
vec3 bg=mix(vec3(3.),vec3(. 0),dist);
return bg;
}
void main(){
vec3 bg=background(vUv);
vec3 color=bg;
gl_FragColor=vec4(color,1.);
}
Copy the code
sdf
How do you create objects in the lighting model? We need SDF.
SDF means symbolic distance function: if passed to a coordinate in function space, the shortest distance between that point and some plane is returned. The symbol of the return value indicates whether the point is inside or outside the plane, so it is called symbolic distance function.
If we want to create a ball, we need to create it with the SDF of the ball. The sphere equation can be expressed in GLSL code as follows
float sdSphere(vec3 p,float r)
{
return length(p)-r;
}
Copy the code
The code for the cube is as follows
float sdBox(vec3 p,vec3 b)
{
vec3 q=abs(p)-b;
return length(max(q,0.)) +min(max(q.x,max(q.y,q.z)),0.);
}
Copy the code
Do not understand how to do? It doesn’t matter, foreign countries have been the common SDF formula sorted out
Create a square in SDF
float sdf(vec3 p){
float box=sdBox(p,vec3(3.));
return box;
}
Copy the code
The picture is still blank, because our guest – light has not entered.
Step into the light
Now comes the first person in this article – light walking. Before we introduce her, let’s take a look at her good friend Ray tracing.
First, we need to know how ray tracing works: give the camera a position eye, put a grid in front of it, shoot a ray ray from the camera’s position, hit the object through the grid, and each pixel of the image corresponds to each point on the grid.
In a light step, the scene is defined by a series of SDF angles. To find the boundary between the scene and the line of sight, we move each point bit by bit, starting with the camera’s position, along the ray, and each step determines whether the point is inside a surface of the scene. If it is, it is done, indicating that the light has hit something. If it is not, the light continues to step.
In the image above, P0 is the camera position and the blue line represents the rays. You can see that the first step of the light, P0P1, is very large, and it happens to be the shortest distance of the light to the surface. Although the point on the surface is the shortest distance, it does not follow the direction of the line of sight, so the point P4 should continue to be detected
An interactive example is available on Shadertoy
The GLSL code implementation of ray stepping is as follows
const float EPSILON=0001.;
float rayMarch(vec3 eye,vec3 ray,float end,int maxIter){
float depth=0.;
for(int i=0; i<maxIter; i++){vec3 pos=eye+depth*ray;
float dist=sdf(pos);
depth+=dist;
if(dist<EPSILON||dist>=end){
break; }}return depth;
}
Copy the code
Create a ray in the main function and feed it to the ray stepping algorithm to get the shortest distance from the ray to the surface
void main(){
...
vec3 eye=vec3(0..0..2.5);
vec3 ray=normalize(vec3(vUv,-eye.z));
float end=5.;
int maxIter=256;
float depth=rayMarch(eye,ray,end,maxIter);
if(depth<end){
vec3pos=eye+depth*ray; color=pos; }... }Copy the code
Lured by the light step, the wild squares appear!
The middle material
The current block has 2 problems: 1. It is not centered 2. It is stretched in the x direction
Center + stretch quality 2 go up
vec2 centerUv(vec2 uv){
uv=2.*uv1.;
float aspect=uResolution.x/uResolution.y;
uv.x*=aspect;
return uv;
}
void main(){
...
vec2 cUv=centerUv(vUv);
vec3 ray=normalize(vec3(cUv,-eye.z)); . }Copy the code
The cube floats to the center of the picture, but she has no color yet
Compute surface normals
In the lighting model, we need to calculate surface normals to give color to materials
vec3 calcNormal(in vec3 p)
{
const float eps=0001.;
const vec2 h=vec2(eps,0);
return normalize(vec3(sdf(p+h.xyy)-sdf(p-h.xyy),
sdf(p+h.yxy)-sdf(p-h.yxy),
sdf(p+h.yyx)-sdf(p-h.yyx)));
}
void main(){
...
if(depth<end){
vec3 pos=eye+depth*ray;
vec3normal=calcNormal(pos); color=normal; }... }Copy the code
At this point the cube is given a blue color, but we don’t see it as a solid yet
move
Let’s rotate the cube 360 degrees, and you can find the 3D rotation function directly in GIST
uniform float uVelocityBox;
mat4 rotationMatrix(vec3 axis,float angle){
axis=normalize(axis);
float s=sin(angle);
float c=cos(angle);
float oc=1.-c;
return mat4(oc*axis.x*axis.x+c,oc*axis.x*axis.y-axis.z*s,oc*axis.z*axis.x+axis.y*s,0.,
oc*axis.x*axis.y+axis.z*s,oc*axis.y*axis.y+c,oc*axis.y*axis.z-axis.x*s,0.,
oc*axis.z*axis.x-axis.y*s,oc*axis.y*axis.z+axis.x*s,oc*axis.z*axis.z+c,0..0..0..0..1.);
}
vec3 rotate(vec3 v,vec3 axis,float angle){
mat4 m=rotationMatrix(axis,angle);
return(m*vec4(v,1.)).xyz;
}
float sdf(vec3 p){
vec3 p1=rotate(p,vec3(1.),uTime*uVelocityBox);
float box=sdBox(p1,vec3(3.));
return box;
}
Copy the code
The fusion results
A square is too lonely. Create a ball to keep her company
How do you get the ball and the cube to stick together, you need the smin function
uniform float uProgress;
float smin(float a,float b,float k)
{
float h=clamp(. 5+. 5*(b-a)/k,0..1.);
return mix(b,a,h)-k*h*(1.-h);
}
float sdf(vec3 p){
vec3 p1=rotate(p,vec3(1.),uTime*uVelocityBox);
float box=sdBox(p1,vec3(3.));
float sphere=sdSphere(p,3.);
float sBox=smin(box,sphere,3.);
float mixedBox=mix(sBox,box,uProgress);
return mixedBox;
}
Copy the code
Set the uProgress value to 0, and they are successfully pasted together
Set uProgress back to 1, and they are separated again
Dynamic fusion
Next is the animation of the dew drop, in fact, is the fusion of the graphic application of a displacement transformation
uniform float uAngle;
uniform float uDistance;
uniform float uVelocitySphere;
const float PI=3.14159265359;
float movingSphere(vec3 p,float shape){
float rad=uAngle*PI;
vec3 pos=vec3(cos(rad),sin(rad),0.)*uDistance;
vec3 displacement=pos*fract(uTime*uVelocitySphere);
float gotoCenter=sdSphere(p-displacement,1.);
return smin(shape,gotoCenter,3.);
}
float sdf(vec3 p){
vec3 p1=rotate(p,vec3(1.),uTime*uVelocityBox);
float box=sdBox(p1,vec3(3.));
float sphere=sdSphere(p,3.);
float sBox=smin(box,sphere,3.);
float mixedBox=mix(sBox,box,uProgress);
mixedBox=movingSphere(p,mixedBox);
return mixedBox;
}
Copy the code
Matcap map
Is the default material too earthy? We have some cool Matcap stickers to support it
uniform sampler2D uTexture;
vec2 matcap(vec3 eye,vec3 normal){
vec3 reflected=reflect(eye,normal);
float m=2.8284271247461903*sqrt(reflected.z+1.);
return reflected.xy/m+. 5;
}
float fresnel(float bias,float scale,float power,vec3 I,vec3 N)
{
return bias+scale*pow(1.+dot(I,N),power);
}
void main(){
...
if(depth<end){
vec3 pos=eye+depth*ray;
vec3 normal=calcNormal(pos);
vec2 matcapUv=matcap(ray,normal);
color=texture2D(uTexture,matcapUv).rgb;
float F=fresnel(0..4..3.2,ray,normal);
color=mix(color,bg,F); }... }Copy the code
After arranging matcap and Fresnel formula, did you feel cool instantly? !
The project address
Ray Marching Gooey Effect
This article is participating in the “Nuggets 2021 Spring Recruitment Campaign”, click to see the details of the campaign