這是一篇關於glsl在web動效交互中的應用。文章質量比較高,翻譯過來你們一塊兒學習借鑑。css
原文連接:tympanus.net/codrops/201…html
學習如何使用噪聲在着色器中建立粘稠的懸停效果。git
做爲Flash的替代者webGL在近幾年隨着像Three.js, PIXI.js, OGL.js這樣的庫而變得愈來愈火。它們對於建立空白板很是有用,惟一的限制只有你的想象力。咱們看到愈來愈多的WebGL建立的效果微妙地集成到交互界面中,以進行懸停,滾動或顯示效果。好比 Hello Monday 或者是 cobosrl.co.web
在本教程中,咱們將使用Three.js建立特殊的粘稠紋理,將其用於在懸停時顯示另外一幅圖像。你如今就能夠點擊演示連接,去看看真實的效果!對於演示自己,我建立了一個更實際的示例,該示例顯示了帶有圖像的水平可滾動佈局,其中每一個圖像都有不一樣的效果。你能夠單擊圖像,它將變換爲更大的版本,同時顯示一些其餘內容(Mock出的內容)。咱們將會帶你瞭解這個效果最有趣的部分,這樣你就能夠知道它是如何工做的,而且能夠本身建立更多的效果!npm
我假設你對Javascript, Three.js以及着色器有必定的瞭解。若是你不瞭解,那麼你能夠先看看 Three.js documentation, The Book of Shaders, Three.js Fundamentals 或者 Discover Three.js.canvas
**注意:**本教程涵蓋了許多部分。若是願意,能夠跳過HTML / CSS / JavaScript部分,直接轉到着色器部分。異步
在咱們建立有趣的東西以前,須要在HTML中插入圖片。在HTML / CSS中設置初始位置和尺寸,比在JavaScript中定位全部內容更容易處理場景大小。此外,樣式部分應該只在CSS中定義,而不要在Javascript中。例如,若是咱們的圖片在桌面端的比例爲16:9,而在移動設備上的比例爲4:3,咱們只應該使用CSS來處理。 JavaScript將僅用於請求更新數據。函數
// index.html
<section class="container">
<article class="tile">
<figure class="tile__figure">
<img data-src="path/to/my/image.jpg" data-hover="path/to/my/hover-image.jpg" class="tile__image" alt="My image" width="400" height="300" />
</figure>
</article>
</section>
<canvas id="stage"></canvas>
複製代碼
// style.css
.container {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100vh;
z-index: 10;
}
.tile {
width: 35vw;
flex: 0 0 auto;
}
.tile__image {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
canvas {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100vh;
z-index: 9;
}
複製代碼
正如你在上面看到的,咱們已經建立了一個位於在屏幕居中的圖像。稍後咱們將利用data-src和data-hover屬性,經過延遲加載在腳本中加載這兩個圖像。佈局
讓咱們從不那麼容易但也不算難的部分開始吧!首先,咱們將建立場景,燈光和渲染器。
// Scene.js
import * as THREE from 'three'
export default class Scene {
constructor() {
this.container = document.getElementById('stage')
this.scene = new THREE.Scene()
this.renderer = new THREE.WebGLRenderer({
canvas: this.container,
alpha: true,
})
this.renderer.setSize(window.innerWidth, window.innerHeight)
this.renderer.setPixelRatio(window.devicePixelRatio)
this.initLights()
}
initLights() {
const ambientlight = new THREE.AmbientLight(0xffffff, 2)
this.scene.add(ambientlight)
}
}
複製代碼
這是一個很是基本的場景。可是咱們在場景中還須要一個基本的元素:相機。咱們有兩種能夠供選擇的相機:正射或透視。若是咱們想讓圖片保持形狀不變,咱們能夠選擇第一種。可是對於旋轉效果,咱們但願在移動鼠標時具備必定的透視效果。
在帶有透視相機的Three.js(或者其餘用於WebGL的庫)中,屏幕上的10個單位值並不等於10px。所以,這裏的技巧是使用一些數學運算將1單位轉換爲1px,並更改視角以增長或減小失真效果。
// Scene.js
const perspective = 800
constructor() {
// ...
this.initCamera()
}
initCamera() {
const fov = (180 * (2 * Math.atan(window.innerHeight / 2 / perspective))) / Math.PI
this.camera = new THREE.PerspectiveCamera(fov, window.innerWidth / window.innerHeight, 1, 1000)
this.camera.position.set(0, 0, perspective)
}
複製代碼
咱們將透視值設置爲800,以便在旋轉平面時不會產生太大的變形。咱們增長的視角越大,咱們對扭曲的感知就越少,反之亦然。而後,咱們須要作的最後一件事是在每一幀中渲染場景。
// Scene.js
constructor() {
// ...
this.update()
}
update() {
requestAnimationFrame(this.update.bind(this))
this.renderer.render(this.scene, this.camera)
}
複製代碼
若是你的屏幕不是黑色的,則說明方法正確!
如上所述,咱們必須從DOM中的圖像上檢索一些其餘信息,例如其尺寸和在頁面上的位置。
// Scene.js
import Figure from './Figure'
constructor() {
// ...
this.figure = new Figure(this.scene)
}
複製代碼
// Figure.js
export default class Figure {
constructor(scene) {
this.$image = document.querySelector('.tile__image')
this.scene = scene
this.loader = new THREE.TextureLoader()
this.image = this.loader.load(this.$image.dataset.src)
this.hoverImage = this.loader.load(this.$image.dataset.hover)
this.sizes = new THREE.Vector2(0, 0)
this.offset = new THREE.Vector2(0, 0)
this.getSizes()
this.createMesh()
}
}
複製代碼
首先,咱們建立另外一個類,將場景做爲屬性傳遞給該類。咱們設置了兩個新的矢量,尺寸和偏移,用於存儲DOM圖像的尺寸和位置。
此外,咱們將使用TextureLoader來「加載」圖像並將其轉換爲紋理。咱們須要這樣作,由於咱們想在着色器中使用這些圖片。
咱們須要在類中建立一個方法來處理圖像的加載並等待回調。咱們可使用異步功能來實現這一目標,但對於本教程而言,咱們將其保持簡單。請記住,您可能須要出於自身目的對它進行一些重構。
// Figure.js
// ...
getSizes() {
const { width, height, top, left } = this.$image.getBoundingClientRect()
this.sizes.set(width, height)
this.offset.set(left - window.innerWidth / 2 + width / 2, -(top - window.innerHeight / 2 + height / 2))
}
// ...
複製代碼
咱們在getBoundingClientRect對象中獲取圖像信息。而後,將它們傳遞給兩個變量。這裏的偏移量用於計算屏幕中心與頁面上的對象之間的距離。(譯者:能夠補充解釋)
// Figure.js
// ...
createMesh() {
this.geometry = new THREE.PlaneBufferGeometry(1, 1, 1, 1)
this.material = new THREE.MeshBasicMaterial({
map: this.image
})
this.mesh = new THREE.Mesh(this.geometry, this.material)
this.mesh.position.set(this.offset.x, this.offset.y, 0)
this.mesh.scale.set(this.sizes.x, this.sizes.y, 1)
this.scene.add(this.mesh)
}
// ...
複製代碼
以後,咱們將在平面上設置值。如您所見,咱們在1px上建立了一個平面,該平面上有1行1列。因爲咱們不想使平面變形,因此咱們不須要不少面或頂點。所以,讓咱們保持簡單。
既然咱們能夠直接設置網格的大小,爲何要用縮放的方式來實現?
其實這麼作主要是爲了更加便於調整網格的大小。若是咱們以後要更改網格的大小,除了用scale沒有什麼更好的方法。雖然更改網格的比例更容易直接實現,可是用來調整尺寸並不太方便。(譯者:做者這裏實際上是一個很巧妙的作法:直接將原來的大小設置爲1x1,而後採用縮放API來讓網格變換爲實際大小,這樣縮放的比例也就等於實際的長寬值)
目前,咱們設置了MeshBasicMaterial,看來一切正常。
如今,咱們已經使用網格構建了場景,咱們想要獲取鼠標座標,而且爲了使事情變得簡單,咱們將其歸一化。爲何要歸一化?看看着色器的座標系統你就明白了。
如上圖所示,咱們已經將兩個着色器的值標準化了。爲簡單起見,咱們將轉化鼠標座標以匹配頂點着色器座標。
若是你在這裏以爲理解有困難, 我建議你去看一看 Book of Shaders 和 Three.js Fundamentals的各個章節。 二者都有很好的建議,並提供了許多示例來幫助你理解。
// Figure.js
// ...
this.mouse = new THREE.Vector2(0, 0)
window.addEventListener('mousemove', (ev) => { this.onMouseMove(ev) })
// ...
onMouseMove(event) {
TweenMax.to(this.mouse, 0.5, {
x: (event.clientX / window.innerWidth) * 2 - 1,
y: -(event.clientY / window.innerHeight) * 2 + 1,
})
TweenMax.to(this.mesh.rotation, 0.5, {
x: -this.mouse.y * 0.3,
y: this.mouse.x * (Math.PI / 6)
})
}
複製代碼
對於補間部分,我將使用GreenSock的TweenMax。這是有史以來最好的庫。並且很是適合咱們想要達到的目的。咱們不須要處理兩個狀態之間的轉換,TweenMax會爲咱們完成。每次移動鼠標時,TweenMax都會平滑更新位置座標和旋轉角度。
在進行後面的步驟以前還有一件事:咱們將材質從MeshBasicMaterial更新爲ShaderMaterial,並傳遞一些值(均勻值)和着色器代碼。
// Figure.js
// ...
this.uniforms = {
u_image: { type: 't', value: this.image },
u_imagehover: { type: 't', value: this.hover },
u_mouse: { value: this.mouse },
u_time: { value: 0 },
u_res: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }
}
this.material = new THREE.ShaderMaterial({
uniforms: this.uniforms,
vertexShader: vertexShader,
fragmentShader: fragmentShader
})
update() {
this.uniforms.u_time.value += 0.01
}
複製代碼
咱們傳遞了兩個紋理,以及鼠標的位置,屏幕的大小和一個名爲u_time
的變量,該變量將在每一幀進行遞增。
可是請記住,這不是最好的方法。咱們只須要當咱們將鼠標懸停在圖形上時增長,而沒必要在每一幀上增長。出於性能,最好僅在須要時更新着色器。
我不會解釋什麼是噪聲以及噪聲的來源。若是你有興趣,請探究《 The Shader of Shaders》中的相關章節,它進行了很好的解釋。
長話短說,噪聲是一個函數,它根據傳遞的值爲咱們提供介於-1和1之間的值。它將輸出隨機但卻又相關的值。
多虧了噪聲,咱們才能生成許多不一樣的形狀,例如地圖,隨機圖案等。
讓咱們從2D噪聲開始。僅經過傳遞紋理的座標,咱們就能夠獲得相似雲的紋理。
但事實上有好幾種噪聲函數。咱們使用3D噪聲,再給一個參數,例如…時間?噪聲圖形將隨着時間的流逝而變化。經過更改頻率和幅度,咱們能夠進行一些變化並增長對比度。
其次,咱們將建立一個圓。在片斷着色器中構建像圓形這樣的簡單形狀很是容易。咱們只是採用了《 The Shader of Shaders:Shapes》中的功能來建立一個模糊的圓,增長對比度和視覺效果!
最後,咱們將這兩個加在一塊兒,使用一些變量,讓它對紋理進行「切片」:
這個混合以後的結果是否是很讓人興奮,讓咱們深刻到代碼層面繼續探究!
咱們這裏其實不須要頂點着色器,這是咱們的代碼:
// vertexShader.glsl
varying vec2 v_uv;
void main() {
v_uv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
複製代碼
Three.js的ShaderMaterial提供了一些有用的默認變量,便於初學者使用:
在這裏,咱們只是將UV座標從頂點着色器傳遞到片斷着色器。
讓咱們使用 The Book of Shaders中的函數來構建圓並添加一個變量來控制邊緣的模糊性。
此外,咱們將用鼠標位置來同步圓心座標。這樣,只要咱們將鼠標移到圖像上,圓就會跟隨鼠標移動。
// fragmentShader.glsl
uniform vec2 u_mouse;
uniform vec2 u_res;
float circle(in vec2 _st, in float _radius, in float blurriness){
vec2 dist = _st;
return 1.-smoothstep(_radius-(_radius*blurriness), _radius+(_radius*blurriness), dot(dist,dist)*4.0);
}
void main() {
vec2 st = gl_FragCoord.xy / u_res.xy - vec2(1.);
// tip: use the following formula to keep the good ratio of your coordinates
st.y *= u_res.y / u_res.x;
vec2 mouse = u_mouse;
// tip2: do the same for your mouse
mouse.y *= u_res.y / u_res.x;
mouse *= -1.;
vec2 circlePos = st + mouse;
float c = circle(circlePos, .03, 2.);
gl_FragColor = vec4(vec3(c), 1.);
}
複製代碼
正如咱們在上面看到的,噪聲函數具備多個參數,併爲咱們生成了逼真的雲圖案。那麼咱們是如何獲得的呢?
對於這一部分,我將使用glslify和glsl-noise,以及兩個npm包來包含其餘功能。它使咱們的着色器更具可讀性,而且隱藏了不少咱們根本不會使用的顯示函數。
// fragmentShader.glsl
#pragma glslify: snoise2 = require('glsl-noise/simplex/2d')
//...
varying vec2 v_uv;
uniform float u_time;
void main() {
// ...
float n = snoise2(vec2(v_uv.x, v_uv.y));
gl_FragColor = vec4(vec3(n), 1.);
}
複製代碼
經過更改噪聲的幅度和頻率(好比於sin / cos函數),咱們能夠更改渲染。
// fragmentShader.glsl
float offx = v_uv.x + sin(v_uv.y + u_time * .1);
float offy = v_uv.y - u_time * 0.1 - cos(u_time * .001) * .01;
float n = snoise2(vec2(offx, offy) * 5.) * 1.;
複製代碼
但這並時間的函數!它失真了,咱們想要出色的效果。所以,咱們將改成使用noise3d並傳遞第三個參數:時間。
float n = snoise3(vec3(offx, offy, u_time * .1) * 4.) * .5;
複製代碼
只要將它們疊加在一塊兒,咱們就能夠看到隨時間變化的有趣的形狀。
爲了解釋其背後的原理,讓咱們假設噪聲就像是在-1和1之間浮動的值。可是咱們的屏幕沒法顯示負值或大於1(純白色)的像素,所以咱們只能看到0到1之間的值。
咱們的圓形則像這樣:
相加以後的近似結果:
咱們很是白的像素是可見光譜以外的像素。
若是咱們減少噪聲並減去少許噪聲,它將逐漸沿波浪向下移動,直到其消失在可見顏色的範圍以內。
float n = snoise(vec3(offx, offy, u_time * .1) * 4.) - 1.;
複製代碼
咱們的圓形仍然存在,只是可見度比較低。若是咱們增長乘以它的值,它將造成更大的對比。
float c = circle(circlePos, 0.3, 0.3) * 2.5;
複製代碼
咱們就實現咱們最想要的效果了!可是正如你看到的,仍然缺乏一些細節。並且咱們的邊緣一點也不銳利。
爲了解決這個問題,咱們將使用 built-in smoothstep function。
float finalMask = smoothstep(0.4, 0.5, n + c);
gl_FragColor = vec4(vec3(finalMask), 1.);
複製代碼
藉助此功能,咱們將在0.4到0.5之間切出一部分圖案。這些值之間的間隔越短,邊緣越銳利。
最後,咱們能夠將混合兩個紋理用做遮罩。
uniform sampler2D u_image;
uniform sampler2D u_imagehover;
// ...
vec4 image = texture2D(u_image, uv);
vec4 hover = texture2D(u_imagehover, uv);
vec4 finalImage = mix(image, hover, finalMask);
gl_FragColor = finalImage;
複製代碼
咱們能夠更改一些變量以產生更強的粘稠效果:
// ...
float c = circle(circlePos, 0.3, 2.) * 2.5;
float n = snoise3(vec3(offx, offy, u_time * .1) * 8.) - 1.;
float finalMask = smoothstep(0.4, 0.5, n + pow(c, 2.));
// ...
複製代碼
在這裏能夠找到完整的源碼
很高興你能讀到這。這篇教程並不完美,我可能忽略了一些細節,可是我但願你仍然喜歡本教程。基於此,你能夠盡情的使用更多變量,嘗試其餘噪聲函數,並嘗試使用鼠標方向或滾動發揮你的想象力來實現其餘效果!