液晶球回归——HTML与Raymarching的结晶

6,804 阅读3分钟

呀哈喽!这里是alphardex。

之前我写过一篇液晶球相关的文章,里面是用顶点着色器和片元着色器来实现液晶球效果的,不过呢,之前同样写过一篇Raymarching相关的文章,于是乎我在想能不能把这两种方法给结合起来,实现一个第二版的液晶球呢,经过几天的实验,最终写出了如下的效果(全屏打开最佳):

code.juejin.cn/pen/7161774…

本文将简要地介绍一下实现这个效果的思路

液晶球部分

建模

首先用球体的SDF函数描画出一个球体

float sdSphere(vec3 p,float s)
{
    return length(p)-s;
}

1.png

噪声函数来扭曲球体,这里我用的是经典柏林噪声,感觉效果最好看

vec3 distort(vec3 p){
    float t=iTime*.5;
    
    float distortStr=1.6;
    vec3 distortP=p+cnoise(vec3(p*PI*distortStr+t));
    float perlinStr=cnoise(vec3(distortP*PI*distortStr*.1));
    
    vec3 dispP=p;
    dispP+=(p*perlinStr*.1);
    
    return dispP;
}

2.gif

创建2个球体,并用融合函数将它们融合起来

float opSmoothUnion(float d1,float d2,float k)
{
    float h=max(k-abs(d1-d2),0.);
    return min(d1,d2)-h*h*.25/k;
}

它们的位移由用户鼠标的位置所决定,创建2个uMouse并运用插值函数即可

let offsetX1 = 0;
let offsetY1 = 0;

let offsetX2 = 0;
let offsetY2 = 0;

this.update(() => {
  const mouse = new THREE.Vector2(
    this.iMouse.mouseScreen.x / window.innerWidth,
    this.iMouse.mouseScreen.y / window.innerHeight
  );

  const mouse1Lerp = params.mouse1Lerp;
  const mouse2Lerp = params.mouse2Lerp;

  offsetX1 = THREE.MathUtils.lerp(offsetX1, mouse.x, mouse1Lerp);
  offsetY1 = THREE.MathUtils.lerp(offsetY1, mouse.y, mouse1Lerp);

  offsetX2 = THREE.MathUtils.lerp(offsetX2, offsetX1, mouse2Lerp);
  offsetY2 = THREE.MathUtils.lerp(offsetY2, offsetY1, mouse2Lerp);

  sq.material.uniforms.uMouse1.value = new THREE.Vector2(offsetX1, offsetY1);
  sq.material.uniforms.uMouse2.value = new THREE.Vector2(offsetX2, offsetY2);
});

3.gif

光照

把球体的主色调变成黑色,在render函数中加上微光效果作为轮廓

if(t>tMax&&t<(tMax+GLOW)){
    vec3 glowColor=vec3(1.);
    float glowAlpha=map(t,tMax,tMax+GLOW,1.,0.);
    col=vec4(glowColor,glowAlpha);
}

4.png

用菲涅尔反射公式产生第一层的光照效果

float fresnel(float bias,float scale,float power,vec3 I,vec3 N)
{
    return bias+scale*pow(1.+dot(I,N),power);
}
float fOffset=-1.4*(1.-distanceMouse*2.);
float f=fOffset+fresnel(0.,1.,1.,I,nor)*1.44;
float f2=fOffset+fresnel(1.,1.,1.,rd,nor)*1.44;
vec3 fCol=vec3(saturate(pow(f-.8,3.)));
lin=blendScreen(lin,fCol);

5.png

引入一个环境贴图(我是直接在polyhaven上找的),将其采样成外围的光照

vec3 cubeTex=texture(uCubemap,vec3(screenUv,0.)).rgb;
vec3 cubeTexSat=saturation(cubeTex,6.);
vec3 cubeTexF=blendScreen(mix(vec3(0.),cubeTexSat,fCol),fCol);
lin=blendScreen(lin,cubeTexF);

6.png

可以用smoothstep平滑函数来产生更强的环境光效果

vec3 iri=vec3(0.);
float iriSrength=10.;
iri.r=smoothstep(cubeTexF.r*iriSrength,0.,.5);
iri.g=smoothstep(cubeTexF.g*iriSrength,0.,.5);
iri.b=smoothstep(cubeTexF.b*iriSrength,0.,.5);
lin=blendScreen(lin,iri);

vec3 iri2=vec3(0.);
iri2.r=smoothstep(0.,.25,cubeTexF.r);
iri2.g=smoothstep(0.,.25,cubeTexF.r);
iri2.b=smoothstep(0.,.25,cubeTexF.r);
lin=blendScreen(lin,iri2);

7.png

再次运用菲涅尔反射公式计算得来的值,在球体的中心也产生光

vec3 mf=vec3(0.);
float fFactor=pow(f+f2,1.24);
float invertFFactor=-fFactor+3.;
mf=vec3(invertFFactor);
mf*=.1;
lin=blendScreen(lin,mf);

8.png

最后与鼠标交互相结合,一个酷酷的液晶球就完成啦!

9.gif

与HTML相结合

最近刚写过一篇类似的文章,具体方法就不再赘述了。

唯一要提的一点是球体的内部其实是可以折射出HTML的内容的,这个是怎么实现的呢?答案是用离屏渲染,在kokomi.js中RenderTexture可以轻松做到这一点。创建另一个THREE.Scene场景对象,将所有的WebGL对象clone到这个场景中,创建RenderTexture,应用该场景对象,将其作为液晶球折射的材质即可,甚至可以加上RGBShift来扭曲颜色通道,看起来更酷哦。

vec3 refra=refract(vec3(0.,0.,-2.),nor,1./2.);
screenUv+=refra.xy*.015;

float offset=(.05*nor.x*.15+.002)*.8;
vec2 rUv=vec2(screenUv.x,screenUv.y+offset);
vec2 gUv=vec2(screenUv.x,screenUv.y);
vec2 bUv=vec2(screenUv.x,screenUv.y-offset);
vec3 rtTex=RGBShift(uRt,rUv,gUv,bUv).xyz;
lin=blendScreen(lin,rtTex);

vec3 rt2Tex=texture(uRt2,screenUv).xyz;
lin=blendScreen(lin,rt2Tex*uRt2Opacity);

10.gif

最后

希望本文能给你创作新特效的灵感,keep creating~