<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          手把手教你原生 JS+WebGL 實(shí)現(xiàn)3D圖片效果!

          共 8475字,需瀏覽 17分鐘

           ·

          2020-04-19 23:21

          (給前端大學(xué)加星標(biāo),提升前端技能.

          作者:oj8kay

          my.oschina.net/codingDog/blog/3164412

          海外黨玩F***book的時(shí)候可能有接觸過(guò)這個(gè)酷炫的3d圖片效果:

          d7b858f0b5dc013caefeb6569f69ff97.webp

          只要通過(guò)客戶端的這個(gè)入口——

          fdcb2989f62d64d3ec3c70c826079c5d.webp

          或者網(wǎng)頁(yè)版的這個(gè)入口——

          74b84dcfc49633f094f01e0bc8d461e8.webp

          就能生成。不知道咋玩的請(qǐng)參考官方的幫助手冊(cè)。今天就教大家手?jǐn)]出一個(gè)這樣的功能,不要擔(dān)心,所有代碼加起來(lái)不超過(guò)200行并且不使用任何第三方庫(kù)。雖然canvas2D也能做出這個(gè)效果,但是基于這種像素級(jí)操作的性能考慮,WebGL顯然是更好的方案,我前面的有些教程也用到了WebGL,核心的API我就不做過(guò)多介紹,直接詳細(xì)地注釋在最終的代碼里面了,代碼仍然使用WebGL 1.0版本。

          老規(guī)矩,還是先介紹原理,推薦有想法的讀者略過(guò)教程,自己直接根據(jù)原理去擼出來(lái),因?yàn)槲疫€是秉持著話癆的特色,想到什么說(shuō)什么,教程中摻雜一些自己的干貨,對(duì)一些人來(lái)說(shuō)可能過(guò)于啰嗦。夾,哈吉咩馬修!(工地日語(yǔ)

          非死不可客戶端在上傳圖片的時(shí)候你有兩種可選操作:

          一種是上傳帶深度通道的圖片,即圖片的每個(gè)像素是RGB-D格式,如果你是蘋果手機(jī)可能在相機(jī)里會(huì)有人像模式或景深模式,拍出來(lái)的照片在本地是heic格式的文件,一般這種就是帶深度信息的(有興趣的可以去維基了解下這種heif編碼的圖片,可以做到很多神奇的事)。通常有TOF鏡頭的手機(jī)都能拍出這種圖片,但是不知道為啥F***book似乎只支持三星系列和自己發(fā)布的安卓機(jī)?

          另一種辦法就是上傳兩張圖,一張普通的RGB像素的原圖,一張灰度圖,只要灰度圖的文件名和原圖一樣,加上_depth的后綴即可。比如666.jpg和666_depth.jpg。這也是F***book網(wǎng)頁(yè)版唯一支持的方式。這個(gè)灰度圖的門道可就多了,也是我們后面代碼實(shí)現(xiàn)的核心。開(kāi)發(fā)過(guò)游戲的一定知道深度貼圖,或者陰影貼圖/光照貼圖,其實(shí)都是類似的玩意,這種貼圖存儲(chǔ)了原圖每個(gè)像素的深度信息,貼圖的每個(gè)像素的R值就是原圖的z軸偏移,因?yàn)橐话闵疃荣N圖的R、G、B通道的值相同,所以表現(xiàn)出來(lái)的就是一張灰度圖。

          如何獲取深度貼圖呢?如果你有heic格式的帶深度信息的照片,可以用PS抽取出z通道的信息(windows上的PS不支持),如果你啥都沒(méi)有,我會(huì)在下個(gè)教程嘗試“教”你一下如何在PS中繪制出深度貼圖,或者使用谷歌提供的一個(gè)人工智能程序來(lái)生成,我也會(huì)寫入下個(gè)教程,親測(cè)匹配程度還是挺高的~

          具體是怎么產(chǎn)生3D效果的呢?深度貼圖中,顏色越淺(值越小)表示深度約低,通過(guò)深度貼圖的深度值來(lái)對(duì)原圖的采樣位置進(jìn)行偏移,比如當(dāng)你把貼圖往左偏移,然后使用偏移的距離乘上原圖的某個(gè)坐標(biāo)在貼圖上的深度值得到的結(jié)果來(lái)對(duì)原圖進(jìn)行采樣,就會(huì)得到不同的點(diǎn)在不同的深度偏移的大小不同的情況,距離越近的偏移越小,距離越遠(yuǎn)的偏移越大,是不是很符合我們生活中的常識(shí)?事實(shí)上,拋棄主觀感知,從底層角度考慮,最終展現(xiàn)出來(lái)的效果其實(shí)就是一部分的像素點(diǎn)被壓縮了,一部分的像素點(diǎn)被拉伸了。不知道大家有沒(méi)有用過(guò)live2D或者Spine、龍骨等工具做出來(lái)的動(dòng)畫,就是這種:

          8968f935e8703de3a6cfdc68644dc818.webp

          剛剛所說(shuō)的底層變化是不是和這種網(wǎng)格動(dòng)畫很像,其實(shí)都是對(duì)圖片的變形來(lái)達(dá)到3D效果,就單張圖的變化而言,他們的唯一區(qū)別就是蒙皮動(dòng)畫是手動(dòng)key幀(或者是骨骼綁定——這個(gè)以后有機(jī)會(huì)談?wù)劊?D圖片是通過(guò)深度貼圖自動(dòng)生成。

          廢話終于說(shuō)完,下面開(kāi)始編碼,先設(shè)置一下基礎(chǔ)樣式:

          * {
          margin: 0;
          padding: 0;
          }
          body {
          width: 100vw;
          height: 100vh;
          position: relative;
          background-color: #000;
          }
          canvas {
          position: absolute;
          left: 50%;
          top: 50%;
          transform: translate3d(-50%, -50%, 0);
          }

          然后引入glMatrix函數(shù)庫(kù)用于操作矩陣(雖然,之前說(shuō)好的不依賴第三方庫(kù),不過(guò)坐標(biāo)換算確實(shí)挺煩~免得程序太長(zhǎng)還有寫一堆注釋~其實(shí)換算也不難,看過(guò)上一篇教程的應(yīng)該自己實(shí)現(xiàn)問(wèn)題也不大~~原諒我標(biāo)題黨 ???)

          <script src="./gl-matrix-min.js">script>

          我已經(jīng)下載好了,想要消息了解這個(gè)函數(shù)庫(kù)的可以去glMatrix官網(wǎng),這個(gè)庫(kù)非常小,未壓縮前也就100多K。

          頂點(diǎn)著色器(shader_vertex.vert)的代碼:

          attribute vec2 a_pos;
          attribute vec2 a_uv;
          uniform mat4 u_proj;
          varying vec2 v_uv;
          void main() {
          v_uv = a_uv; // 將紋理坐標(biāo)傳遞到片元著色器
          gl_Position = u_proj * vec4(a_pos, 0.0, 1.0);
          }

          片元著色器的代碼:

          precision highp float;
          uniform sampler2D u_sampler;
          varying vec2 v_uv;
          void main() {
          gl_FragColor = texture2D(u_sampler, v_uv);
          }

          直接貼上繪制靜態(tài)圖的代碼:


          init()

          async function init () {

          const { mat4 } = glMatrix

          const PAGE_WIDTH = document.body.clientWidth
          const PAGE_HEIGHT = document.body.clientHeight
          // 設(shè)置畫布寬高
          const CANVAS_WIDTH = 900
          const CANVAS_HEIGHT = 900
          const canvas = document.createElement('canvas')
          canvas.width = CANVAS_WIDTH
          canvas.height = CANVAS_HEIGHT
          document.body.appendChild(canvas)

          const gl = canvas.getContext('webgl')

          gl.viewport(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT)

          // 加載圖片(原圖和深度圖尺寸一致)
          const image = new Image()
          image.src = './sennpai.jpg'
          await new Promise(resolve => image.complete ? resolve() : (image.onload = e => resolve()))

          // 若圖片寬高超出限制,以類似 background-size:contain 的方式將圖片縮放居中
          let ratio = 1
          if (image.height > CANVAS_HEIGHT) {
          ratio = CANVAS_HEIGHT / image.height
          }
          if (image.width * ratio > CANVAS_WIDTH) {
          ratio = CANVAS_WIDTH / image.width
          }

          const imgWidth = image.width * ratio
          const imgHeight = image.height * ratio

          // 獲取頂點(diǎn)著色器源碼
          let res = await fetch('./shader_vertex.vert', { method: 'get', })
          let shaderSrc = await res.text()
          // 創(chuàng)建頂點(diǎn)著色器
          const vs = gl.createShader(gl.VERTEX_SHADER)
          gl.shaderSource(vs, shaderSrc)
          gl.compileShader(vs)
          // 獲取著色器信息
          if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) {
          // 打印編譯失敗日志
          console.error(`Error compile shader:\n${shaderSrc}\n=====error log======\n${gl.getShaderInfoLog(vs)}`)
          gl.deleteShader(vs)
          return null
          }

          // 獲取片元著色器源碼
          res = await fetch('./shader_fragment.frag', { method: 'get', })
          shaderSrc = await res.text()
          // 創(chuàng)建片元著色器
          const fs = gl.createShader(gl.FRAGMENT_SHADER)
          gl.shaderSource(fs, shaderSrc)
          gl.compileShader(fs)
          if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) {
          console.error(`Error compile shader:\n${shaderSrc}\n=====error log======\n${gl.getShaderInfoLog(fs)}`)
          gl.deleteShader(fs)
          return null
          }

          // 創(chuàng)建program
          const prg = gl.createProgram()
          gl.attachShader(prg, vs)
          gl.attachShader(prg, fs)
          gl.linkProgram(prg)
          gl.useProgram(prg)

          // 設(shè)置投影矩陣
          const projMat4 = mat4.create()
          /**
          * ortho(out, left, right, bottom, top, near, far)
          */

          mat4.ortho(projMat4, -CANVAS_WIDTH / 2, CANVAS_WIDTH / 2, -CANVAS_HEIGHT / 2, CANVAS_HEIGHT / 2, 0, 500)
          // 獲取投影矩陣的地址
          const uProj = gl.getUniformLocation(prg, 'u_proj')
          // 將投影矩陣傳入
          gl.uniformMatrix4fv(uProj, false, projMat4)

          // 使用頂點(diǎn)數(shù)組創(chuàng)建vbo
          const vertexList = new Float32Array([
          // x y u v
          -imgWidth / 2, imgHeight / 2, 0, 0,
          -imgWidth / 2, -imgHeight / 2, 0, 1,
          imgWidth / 2, imgHeight / 2, 1, 0,
          imgWidth / 2, -imgHeight / 2, 1, 1,
          ])
          // 獲取數(shù)組每個(gè)元素的大小(用于計(jì)算步長(zhǎng))
          const PER_ELEMENT_SIZE = vertexList.BYTES_PER_ELEMENT
          const buffer = gl.createBuffer()
          /**
          * 綁定緩沖區(qū)
          * @param target 數(shù)據(jù)類型
          * @param buffer 緩沖區(qū)對(duì)象
          */

          gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
          /**
          * 向緩沖區(qū)寫入數(shù)據(jù)
          * @param target 數(shù)據(jù)類型
          * @param data 數(shù)據(jù)(這里是類型化數(shù)組)
          * @param usage 繪制方式(用于幫助webgl優(yōu)化)
          */

          gl.bufferData(gl.ARRAY_BUFFER, vertexList, gl.STATIC_DRAW)
          // 獲取頂點(diǎn)坐標(biāo)變量在著色器中的地址
          const aPos = gl.getAttribLocation(prg, 'a_pos')
          /**
          * 將緩沖區(qū)對(duì)象分配給attribute變量
          * @param location:變量的存儲(chǔ)地址
          * @param size:每個(gè)頂點(diǎn)分量個(gè)數(shù),若個(gè)數(shù)比變量的數(shù)量少,則按照gl.vertexAttrib[1234]f的規(guī)則來(lái)補(bǔ)全
          * @param type:指定數(shù)據(jù)類型
          * @param normalized:是否需要?dú)w一化
          * @param stride:相鄰兩個(gè)頂點(diǎn)之間的字節(jié)數(shù)(只有一種數(shù)據(jù)則為0)
          * @param offset:數(shù)據(jù)的偏移量(單位字節(jié),只有一種數(shù)據(jù)則為0)
          */

          gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, PER_ELEMENT_SIZE * 4, 0)
          // 允許aPos訪問(wèn)VBO
          gl.enableVertexAttribArray(aPos)
          // 獲取紋理坐標(biāo)變量在著色器中的地址
          const aUV = gl.getAttribLocation(prg, 'a_uv')
          gl.vertexAttribPointer(aUV, 2, gl.FLOAT, false, PER_ELEMENT_SIZE * 4, PER_ELEMENT_SIZE * 2)
          // 允許aUV訪問(wèn)VBO
          gl.enableVertexAttribArray(aUV)

          // 使用完后解綁VBO
          gl.bindBuffer(gl.ARRAY_BUFFER, null)

          // 創(chuàng)建紋理對(duì)象
          const texture = gl.createTexture()
          // 激活0號(hào)紋理單元
          gl.activeTexture(gl.TEXTURE0)
          // 綁定并開(kāi)啟0號(hào)紋理單元
          gl.bindTexture(gl.TEXTURE_2D, texture)
          // 指定縮小算法
          gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
          // 指定放大算法
          gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
          // 指定水平方向填充算法
          gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
          // 指定垂直方向填充算法
          gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
          /**
          * 通過(guò)0號(hào)紋理單元將圖片分配給紋理對(duì)象
          * target 指定為2D紋理
          * level 金字塔紋理
          * internalFormat 圖片內(nèi)部格式
          * format 紋理格式(必須與internalFormat相同)
          * type 紋理數(shù)據(jù)類型
          * image 圖片
          */

          gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image)
          // 獲取紋理對(duì)象在著色器中的地址(使用uniform,因?yàn)槊總€(gè)頂點(diǎn)操作的都是同一個(gè)紋理)
          const uSampler = gl.getUniformLocation(prg, 'u_sampler')
          // 指定從0號(hào)紋理單元獲取紋理
          gl.uniform1i(uSampler, 0)

          // 渲染循環(huán)
          function loop () {
          gl.clearColor(0.0, 0.0, 0.0, 1.0)
          gl.clear(gl.COLOR_BUFFER_BIT) // 清空顏色緩沖區(qū)
          gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
          requestAnimationFrame(loop)
          }

          loop()

          }

          都是一些常規(guī)的操作,具體api的作用寫入注釋里就不做多解釋了。

          接下來(lái)把我們的深度貼圖傳入著色器,主要是這幾個(gè)步驟:

          ①獲取加載完成的圖片對(duì)象:

          const depthImage = new Image()
          depthImage.src = './sennpai_depth.jpg'
          await new Promise(resolve => depthImage.complete ? resolve() : (depthImage.onload = e => resolve()))

          因?yàn)槿绻麨g覽器如果已經(jīng)緩存了圖片不一定會(huì)觸發(fā)onload事件。所以我們先通過(guò)complete屬性來(lái)判斷圖片的加載狀態(tài)是否為已完成。

          ②修改片元著色器代碼,通過(guò)深度貼圖對(duì)原圖來(lái)進(jìn)行采樣:

          precision highp float;
          uniform sampler2D u_sampler;
          uniform sampler2D u_sampler_depth;// 深度貼圖采樣器
          uniform vec2 u_offset;// 深度貼圖的偏移
          varying vec2 v_uv;
          void main() {
          float depth = texture2D(u_sampler_depth, v_uv).r;// 獲取深度信息
          gl_FragColor = texture2D(u_sampler, v_uv + depth * u_offset);
          }

          獲取貼圖的R通道的值作為深度值

          ③通過(guò)另一個(gè)紋理單元(如1號(hào)紋理單元)將貼圖傳入片元著色器:

          // 同理,創(chuàng)建深度貼圖的紋理
          const depthTexture = gl.createTexture()
          // 綁定并開(kāi)啟1號(hào)紋理單元
          gl.activeTexture(gl.TEXTURE1)
          gl.bindTexture(gl.TEXTURE_2D, depthTexture)
          gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
          gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
          gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
          gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
          gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, depthImage)
          const uSamplerDepth = gl.getUniformLocation(prg, 'u_sampler_depth')
          // 指定從1號(hào)紋理單元獲取紋理
          gl.uniform1i(uSamplerDepth, 1)

          這時(shí)候看到最后效果沒(méi)有任何變化,因?yàn)槲覀冞€沒(méi)有對(duì)貼圖進(jìn)行偏移,u_offset默認(rèn)值是vec(0.0,0.0)。

          接下來(lái)可以給頁(yè)面綁定mousemove事件,我這里限定了u,v最大的偏移量為0.05,把渲染循環(huán)函數(shù)放到事件回調(diào)中:

          const uOffset = gl.getUniformLocation(prg, 'u_offset')
          const scale = 0.1
          document.body.onmousemove = e => {
          gl.uniform2f(uOffset, scale * (e.pageX / PAGE_WIDTH - 0.5), scale * (e.pageY / PAGE_HEIGHT - 0.5))
          loop()
          }
          // 繪制循環(huán)
          function loop () {
          gl.clearColor(0.0, 0.0, 0.0, 1.0)
          gl.clear(gl.COLOR_BUFFER_BIT) // 清空顏色緩沖區(qū)
          gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
          // requestAnimationFrame(loop)
          }

          ok,大功告成,預(yù)覽一下效果圖——

          bdcc8a31616ca26b1911ad8647eef580.webp

          完整源碼項(xiàng)目在后臺(tái)回復(fù):3D,即可獲取03588663960bc315f65cdbd29081cbef.webp

          分享前端好文,點(diǎn)亮?在看?f09b41096dc935eb62f8bc976a00630a.webp

          瀏覽 108
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  伊人青青操 | 一级爱爱免费视频 | 欧美成人影视在线 | 后入少妇视频 | www.一区二区三区 |