先上一个demo代码:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <style> html, body { margin: 0; height: 100%; } canvas { display: block; } </style> </head> <body> <script src="../lib/three.min.js"></script> <script src="../lib/stats.min.js"></script> <script src="../lib/OrbitControl.js"></script> <!-- 顶点着色器 --> <script id="vertex-shader" type="x-shader/x-vertex"> void main(){ gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); } </script> <script id="fragment-shader-8" type="x-shader/x-fragment"> uniform float time; uniform vec2 resolution; void main( void ) { float x = gl_FragCoord.x; float y = gl_FragCoord.y; float fy = sin(x / 100. + time * 5.) * 100. + 250.; float opacity = 0.0; if(y > fy){ opacity = 1.0; } gl_FragColor = vec4(1.,1.,1.,opacity); } </script> <script type="module"> var renderer; var clock ; function initRender() { clock = new THREE.Clock(); renderer = new THREE.WebGLRenderer({antialias: true,alpha:true}); renderer.setSize(window.innerWidth, window.innerHeight); //告诉渲染器需要阴影效果 //renderer.shadowMap.enabled = true; //renderer.shadowMap.type = THREE.PCFSoftShadowMap; // 默认的是,没有设置的这个清晰 THREE.PCFShadowMap renderer.setClearColor(0xffffff); document.body.appendChild(renderer.domElement); } var camera; function initCamera() { camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(0, 0, 150); camera.lookAt(new THREE.Vector3(0, 0, 0)); } var scene; function initScene() { scene = new THREE.Scene(); var bgTexture = new THREE.TextureLoader().load("../texture/starfiled.jpeg"); scene.background = bgTexture; } function initLight() { var hemisphereLight1 = new THREE.HemisphereLight(0xffffff, 0x444444, 2); hemisphereLight1.position.set(0, 200, 0); scene.add(hemisphereLight1); } var plane; function addplane(){ var planeGeometry = new THREE.PlaneGeometry(450,250) var meshMaterial = createMaterial("vertex-shader", "fragment-shader-8"); plane = new THREE.Mesh(planeGeometry,meshMaterial); scene.add(plane); } //创建ShaderMaterial纹理的函数 function createMaterial(vertexShader, fragmentShader) { var vertShader = document.getElementById(vertexShader).innerHTML; //获取顶点着色器的代码 var fragShader = document.getElementById(fragmentShader).innerHTML; //获取片元着色器的代码 //配置着色器里面的attribute变量的值 var attributes = {}; //配置着色器里面的uniform变量的值 var uniforms = { time: {type: 'f', value: 1.0}, scale: {type: 'f', value: 0.2}, alpha: {type: 'f', value: 0.6}, resolution: {type: "v2", value: new THREE.Vector2(window.innerWidth, window.innerHeight)} }; var meshMaterial = new THREE.ShaderMaterial({ uniforms: uniforms, defaultAttributeValues : attributes, vertexShader: vertShader, fragmentShader: fragShader, transparent: true }); return meshMaterial; } //初始化性能插件 var stats; function initStats() { stats = new Stats(); document.body.appendChild(stats.dom); } //用户交互插件 鼠标左键按住旋转,右键按住平移,滚轮缩放 var controls; function initControls() { controls = new THREE.OrbitControls(camera, renderer.domElement); // 如果使用animate方法时,将此函数删除 //controls.addEventListener( 'change', render ); // 使动画循环使用时阻尼或自转 意思是否有惯性 controls.enableDamping = true; //动态阻尼系数 就是鼠标拖拽旋转灵敏度 //controls.dampingFactor = 0.25; //是否可以缩放 controls.enableZoom = true; //是否自动旋转 controls.autoRotate = false; controls.autoRotateSpeed = 3; //设置相机距离原点的最远距离 controls.minDistance = 1; //设置相机距离原点的最远距离 controls.maxDistance = 200; //是否开启右键拖拽 controls.enablePan = true; } function render() { var delta = clock.getDelta(); renderer.render(scene, camera); if(plane){ plane.material.uniforms.time.value += 0.01; } } //窗口变动触发的函数 function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); render(); renderer.setSize(window.innerWidth, window.innerHeight); } function animate() { //更新控制器 render(); //更新性能插件 stats.update(); controls.update(); requestAnimationFrame(animate); } function draw() { initScene(); initCamera(); initLight(); initRender(); addplane(); initControls(); initStats(); animate(); window.onresize = onWindowResize; } draw(); </script> </body> </html>
一、页面结构介绍
后续的shader代码效果,也基本上会基于上述html页面代码;
我们看到,我们是用three的一个PlaneGeometry类画了一张面板(你可以把它看作我们今后写shader的画板),然后在这个面板上应用shader材质,这个材质呈现什么样的效果全靠我们去写这个shader啦。比如说,上述代码就是写了一个正余弦波,并且利用一个递增的变量实现波的移动效果。
虽然这个shader很简单,但是还是有必要解析一下,顺便温习一下高中学的三角函数。
二、demo中着色器代码解析
<!-- 顶点着色器 --> <script id="vertex-shader" type="x-shader/x-vertex"> void main(){ gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); } </script> <!-- 片元着色器 --> <script id="fragment-shader-8" type="x-shader/x-fragment"> uniform float time; uniform vec2 resolution; void main( void ) { float x = gl_FragCoord.x; float y = gl_FragCoord.y; float fy = sin(x / 100. + time * 5.) * 100. + 250.; float opacity = 0.0; if(y > fy){ opacity = 1.0; } gl_FragColor = vec4(1.,1.,1.,opacity); } </script>
顶点着色器就是把plane的顶点通过矩阵变换转成屏幕上的像素点;
然后在片元着色器中对这些顶点进行逐个着色(如果有1w个顶点,那么这个片元着色器代码就会跑1w次,因为是在GPU中并行地跑,所以很快),gl_FragCoord就是当前操作的顶点,而gl_FragColor算出来的颜色就是用来在该顶点输出到屏幕的颜色值;
所以片元着色器就是给你一个顶点gl_FragCoord(屏幕上的像素点),你来决定输出什么颜色gl_FragColor!
我们看这段main函数里面的代码:
float fy = sin(x / 100. + time * 5.) * 100. + 250.;
是不是很熟悉?
ok,到这里,正弦波就算出来了,但是颜色呢?因为根据每个x,我们算出对应正弦波上的y值,对应屏幕上每个x来说,都有n个y与之对应,那么上面的做法是,在正弦波上面的这些点的颜色的透明通道值为1.0,在下面的这些点的颜色的透明通道值为0,然后再加上横轴的偏移量θ,这里是变量time,由于time是变化的,所以就出现了上图所示的效果。
以此类推,我们还能写出指数函数、幂函数等的图像
二、幂函数
顶点着色器共用的,就不贴了
<!-- 片元着色器 --> <script id="fragment-shader-8" type="x-shader/x-fragment"> uniform float time; uniform vec2 resolution; void main( void ) { float x = gl_FragCoord.x; float y = gl_FragCoord.y; float fy = pow((x - time * 50.)/10.,2.0) + 200.; float opacity = 0.0; if(y > fy){ opacity = 1.0; } gl_FragColor = vec4(1.,1.,1.,opacity); } </script>
其余的曲线方程如果你有兴趣可以自己试着写写看