2D SDF 圆形波纹效果
- 关于本Shader教程
- 前四篇地址,请按顺序学习
- 本博客使用模板代码中的Shader模板
- 绘制第一圈波纹
- 绘制多圈波纹
- fract函数
- 让光波动起来
- 使用uniform控制最终效果
- 追加uniform,以及lil.gui控制器
- 修改片元着色器
- 最终效果
- 完整源码
关于本Shader教程
- 本教程着重讲解Shadertoy的shader和Threejs的Shader,与原生WebGLShader略有不同,如果需要学习原生WebGL的shader,请参考《WebGL编程指南》
- 本人的shader水平也比较基础,文章中所写代码,不一定是最佳的代码,思路也不一定是最好的思路,所以一切本人的Shader教程下,所有的代码及思路以及学习建议均仅供参考,且目前本教程可能不适用于WebGPU,如果有大佬路过看到本人文章,觉得有可以指点之处,可以在下面留言,我们一起进步
- 数学水平不行的人,尤其是高中数学都及格不了的,不建议入坑Shader
- 本教程会在讲解片元着色器时,使用Shadertoy来编写demo,所以教程中会出现一部分Shadertoy的代码
- 本段内容将会出现在本人所有的【进阶教程-着色器篇】的文章中
前四篇地址,请按顺序学习
【Threejs进阶教程-着色器篇】1. Shader入门(ShadertoyShader和ThreejsShader入门)
【Threejs进阶教程-着色器篇】2. Uniform的基本用法与Uniform的调试
【Threejs进阶教程-着色器篇】3. Uniform的基本用法2与基本地球昼夜效果
【Threejs进阶教程-着色器篇】4. 2D SDF(一) SDF的基本用法
本博客使用模板代码中的Shader模板
请各位自取【模板代码】用于编写Threejs Demo的模板代码
绘制第一圈波纹
上一篇中我们讲到了圆形的sdf,我们依然在片元着色器中引入sdCircle这个函数
<script type="x-shader/x-fragment" id="fragmentShader">varying vec2 vUv;float sdCircle( vec2 p, float r ){return length(p) - r;}void main(){gl_FragColor = vec4(1.0,0.0,0.0,1.0);}
</script>
一般来说,咱们看到的波纹效果,应该是这样的
我们先考虑绘制第一圈波纹,可以看出,越靠近边缘的地方,就越亮,越靠近中心的地方就越暗,因为在项目上,波纹基本上都是透明的,所以本次我们以操作透明度的方式来开发这个效果
<script type="x-shader/x-fragment" id="fragmentShader">varying vec2 vUv;float sdCircle( vec2 p, float r ){return length(p) - r;}void main(){vec2 aUv = vUv - 0.5;vec3 color = vec3(1.0,0.0,0.0);float alp = sdCircle(aUv, 0.5);gl_FragColor = vec4(color,alp);}
</script>
首先我们先分析上面的代码
先对vUv - 0.5,将坐标系平移到中心
然后创建一个三维向量,作为颜色来使用
然后我们使用sdf函数,计算透明度,计算结果如上图所示
根据被移动后的坐标系,我们可以推测出,越接近圆心的,length的取值会越小,再-0.1的时候,就会越小,透明度小于0时会显示为0,所以一直到边缘处,u=0.5,v=0.5的时候,最后计算下来的透明度约为 sqrt( 0.5 * 0.5 * 2 ) ≈ 0.707
绘制多圈波纹
那么,我们可以此时,通过把uv * 4.0,来扩大坐标系,达到调整边缘的效果
//vec2 aUv = vUv - 0.5; //旧代码vec2 aUv = (vUv - 0.5) * 4.0;//新代码
但是这样我们又有了新问题,此时坐标系变成了 uv的最大值 = 0.5 * 4 = 2.0,那么,在uv都为2.0的地方,透明度是多少呢? 计算一下就知道了 sqrt(2 * 2 * 2) = 2.82,透明度最大值是1,超过1了按照1来显示,那么,我们可以考虑只取小数部分
fract函数
fract只取小数部分,可以作为周期函数来使用
fract函数的图像大致如图所示,如果要做类似波纹的效果,且不是圆滑的变化规则,可以使用fract函数来做
我们接下来用fract函数来处理我们的效果
<!-- 片元着色器内容 -->
<script type="x-shader/x-fragment" id="fragmentShader">varying vec2 vUv;float sdCircle( vec2 p, float r ){return length(p) - r;}void main(){vec2 aUv = (vUv - 0.5) * 4.0;vec3 color = vec3(1.0,0.0,0.0);float alp = fract(sdCircle(aUv, 0.5));gl_FragColor = vec4(color,alp);}
</script>
我们其实可以再对uv放大一点,比如说放大到10
让光波动起来
然后我们还需要让它动起来,因为fracrt本身也是周期性函数,那么我们可以通过追加iTime的方式,让fract函数的计算结果发生改变
我们对几何体和材质也稍微做一下修改
let uniforms = {iTime:{value:0}}function addMesh() {//常规情况下,planeGeometry,circleGeometry是与z轴垂直的,改成与y轴垂直,只需要沿着x轴让几何体旋转-90度即可//let geometry = new THREE.PlaneGeometry(10,10).rotateX(-Math.PI/2);//为了让效果更适用于圆形,我们从方形的planeGeometry换成CircleGeometrylet geometry = new THREE.CircleGeometry(5,32).rotateX(-Math.PI/2);let material = new THREE.ShaderMaterial({uniforms,vertexShader:document.getElementById('vertexShader').textContent,fragmentShader:document.getElementById('fragmentShader').textContent,transparent:true})let mesh = new THREE.Mesh(geometry,material);scene.add(mesh);}function render() {uniforms.iTime.value += 0.01;renderer.render(scene,camera);orbit.update();requestAnimationFrame(render);}
修改片元着色器响应动态变化
<script type="x-shader/x-fragment" id="fragmentShader">varying vec2 vUv;uniform float iTime;float sdCircle( vec2 p, float r ){return length(p) - r;}void main(){vec2 aUv = (vUv - 0.5) * 10.0;vec3 color = vec3(1.0,0.0,0.0);//有fract控制,最终数值只会取小数部分float alp = fract(sdCircle(aUv, 0.5) - iTime);gl_FragColor = vec4(color,alp);}
</script>
使用uniform控制最终效果
首先,我们分析一下现在代码中的常量,一般来说常量都可以单独拎出来做uniform
我们光波的圈数由 aUv后面乘的10.0来控制
我们光波的颜色由vec3 color来控制
我们光波的扩散速度,可以给iTime乘一个常量来控制
我们的光波宽度,可以通过对alp做pow计算来控制
那么,我们接下来添加这四个uniform,并且添加GUI控制
如果看不懂接下来的代码,请移步前三篇,全部的代码在前三篇都有解释
追加uniform,以及lil.gui控制器
let uniforms = {iTime:{value:0},iFreq:{value:10.0},//频率,光波圈数iColor:{value:new THREE.Color('#ff0000')},//光波颜色iSpeed:{value:1},//扩散速度iPower:{value:2},//光波强度}function addMesh() {//常规情况下,planeGeometry,circleGeometry是与z轴垂直的,改成与y轴垂直,只需要沿着x轴让几何体旋转-90度即可//let geometry = new THREE.PlaneGeometry(10,10).rotateX(-Math.PI/2);//为了让效果更适用于圆形,我们从方形的planeGeometry换成CircleGeometrylet geometry = new THREE.CircleGeometry(5,32).rotateX(-Math.PI/2);let material = new THREE.ShaderMaterial({uniforms,vertexShader:document.getElementById('vertexShader').textContent,fragmentShader:document.getElementById('fragmentShader').textContent,transparent:true})let mesh = new THREE.Mesh(geometry,material);scene.add(mesh);let param = {color:"#ff0000" //lil.gui读取threejs的颜色比较麻烦,个人习惯在这里单独写一个来控制};let gui = new GUI();gui.add(uniforms.iFreq,'value',0,50,0.01).name('光波圈数');gui.add(uniforms.iPower,'value',0,50,0.01).name('光波强度');gui.add(uniforms.iSpeed,'value',0,50,0.01).name('扩散速度');gui.addColor(param,'color').name('光波颜色').onChange(v=>{uniforms.iColor.value = new THREE.Color(v);})}function render() {uniforms.iTime.value += 0.01;renderer.render(scene,camera);orbit.update();requestAnimationFrame(render);}
修改片元着色器
<script type="x-shader/x-fragment" id="fragmentShader">varying vec2 vUv;uniform float iTime;uniform float iFreq;uniform vec3 iColor;uniform float iSpeed;uniform float iPower;float sdCircle( vec2 p, float r ){return length(p) - r;}void main(){vec2 aUv = (vUv - 0.5) * iFreq;float alp = fract(sdCircle(aUv, 0.5) - iTime * iSpeed);alp = pow(alp,iPower);//对扩散的光波pow可以减少光波有颜色的部分的宽度gl_FragColor = vec4(iColor,alp);}
</script>
最终效果
完整源码
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title><style>body{width:100vw;height: 100vh;overflow: hidden;margin: 0;padding: 0;border: 0;}</style>
</head>
<body><script type="importmap">{"imports": {"three": "../three/build/three.module.js","three/addons/": "../three/examples/jsm/"}}</script><script type="x-shader/x-vertex" id="vertexShader">varying vec2 vUv;void main(){vUv = vec2(uv.x,uv.y);vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );gl_Position = projectionMatrix * mvPosition;gl_Position = projectionMatrix * modelMatrix * viewMatrix * vec4( position, 1.0 );}</script>
<script type="x-shader/x-fragment" id="fragmentShader">varying vec2 vUv;uniform float iTime;uniform float iFreq;uniform vec3 iColor;uniform float iSpeed;uniform float iPower;float sdCircle( vec2 p, float r ){return length(p) - r;}void main(){vec2 aUv = (vUv - 0.5) * iFreq;float alp = fract(sdCircle(aUv, 0.5) - iTime * iSpeed);alp = pow(alp,iPower);//对扩散的光波pow可以减少光波有颜色的部分的宽度gl_FragColor = vec4(iColor,alp);}
</script><script type="module">import * as THREE from "../three/build/three.module.js";import {OrbitControls} from "../three/examples/jsm/controls/OrbitControls.js";import {GUI} from "../three/examples/jsm/libs/lil-gui.module.min.js";window.addEventListener('load',e=>{init();addMesh();render();})let scene,renderer,camera;let orbit;function init(){scene = new THREE.Scene();renderer = new THREE.WebGLRenderer({alpha:true,antialias:true});renderer.setSize(window.innerWidth,window.innerHeight);document.body.appendChild(renderer.domElement);camera = new THREE.PerspectiveCamera(50,window.innerWidth/window.innerHeight,0.1,2000);camera.add(new THREE.PointLight());camera.position.set(15,15,15);scene.add(camera);orbit = new OrbitControls(camera,renderer.domElement);orbit.enableDamping = true;scene.add(new THREE.GridHelper(10,10));}let uniforms = {iTime:{value:0},iFreq:{value:10.0},//频率,光波圈数iColor:{value:new THREE.Color('#ff0000')},//光波颜色iSpeed:{value:1},//扩散速度iPower:{value:2},//光波强度}function addMesh() {//常规情况下,planeGeometry,circleGeometry是与z轴垂直的,改成与y轴垂直,只需要沿着x轴让几何体旋转-90度即可//let geometry = new THREE.PlaneGeometry(10,10).rotateX(-Math.PI/2);//为了让效果更适用于圆形,我们从方形的planeGeometry换成CircleGeometrylet geometry = new THREE.CircleGeometry(5,32).rotateX(-Math.PI/2);let material = new THREE.ShaderMaterial({uniforms,vertexShader:document.getElementById('vertexShader').textContent,fragmentShader:document.getElementById('fragmentShader').textContent,transparent:true})let mesh = new THREE.Mesh(geometry,material);scene.add(mesh);let param = {color:"#ff0000" //lil.gui读取threejs的颜色比较麻烦,个人习惯在这里单独写一个来控制};let gui = new GUI();gui.add(uniforms.iFreq,'value',0,50,0.01).name('光波圈数');gui.add(uniforms.iPower,'value',0,50,0.01).name('光波强度');gui.add(uniforms.iSpeed,'value',0,50,0.01).name('扩散速度');gui.addColor(param,'color').name('光波颜色').onChange(v=>{uniforms.iColor.value = new THREE.Color(v);})}function render() {uniforms.iTime.value += 0.01;renderer.render(scene,camera);orbit.update();requestAnimationFrame(render);}</script>
</body>
</html>