首頁>技術>

本文主要從以下幾個方面進行講述:

建立沒有光照效果的立方體;擴充套件lambert材質,建立有光照效果的立方體;

適用人群:對THREE.js和glsl有基本瞭解的人。

建立沒有光照效果的立方體

本示例會建立一個前後左右面是純色,上下面是貼圖的立方體。該部分的內容主要包括以下部分:

建立bufferGeometry;自定義shaderMaterial,在shaderMaterial裡面判斷是用純色還是貼圖;建立mesh。建立bufferGeometry

因為想更深入的瞭解THREE.js的實現原理,所以這塊沒有直接使用BoxBufferGeometry,而是自己定義頂點資訊:

const geometry = new THREE.BufferGeometry()const position = [ // 每個面兩個三角形,每個三角形三個頂點,每個頂點三個座標值,所以一個三角形是3*3=9個值,一個面是3*3*2=18個值  -1, -1, 1, 1, -1, 1, 1, 1, 1, // front face  1, 1, 1, -1, 1, 1, -1, -1, 1,  1, -1, 1, 1, -1, -1, 1, 1, -1, // right face  1, 1, -1, 1, 1, 1, 1, -1, 1,  1, -1, -1, -1, -1, -1, -1, 1, -1, // back face  -1, 1, -1, 1, 1, -1, 1, -1, -1,  -1, -1, -1, -1, -1, 1, -1, 1, 1, // left face  -1, 1, 1, -1, 1, -1, -1, -1, -1,  -1, 1, 1, 1, 1, 1, 1, 1, -1, // top face  1, 1, -1, -1, 1, -1, -1, 1, 1,  1, -1, 1, -1, -1, 1, -1, -1, -1, // bottom face  -1, -1, -1, 1, -1, -1, 1, -1, 1]// 定義了一個長寬高都是2的立方體,所以上面xyz的座標要麼是1,要麼是-1geometry.setAttribute('position', new THREE.BufferAttribute(Float32Array.from(position), 3))

然後,給每個頂點新增顏色資訊,每個頂點既可以是純色也可以是貼圖,純色需要rgb三個分量,貼圖需要uv兩個分量,所以每個頂點至少需要三個分量來表示。

那麼,如何判斷這個頂點是純色還是貼圖呢?我們當然可以再使用一個數組來表示。但是注意到上面貼圖只需要兩個分量,那麼我們就可以利用第三個分量來判斷。glsl語言裡面rgb色值的範圍是0-1,所以我們可以使用這個範圍之外的值表示這是一個貼圖。

那取什麼值呢?我們這個立方體定義了上下面是貼圖,也就是貼圖不只一個,那麼這個值還要能推匯出是第幾個貼圖。我這裡設定了一個textureBaseIndex為2的變數。

const colors = []const textureBaseIndex = 2for (let i = 0; i < 12; i++) {  switch (i) {    case 0: // front color    case 1:      colors.push(1, 0, 0, 1, 0, 0, 1, 0, 0) // 紅      break    case 2: // right color    case 3:      colors.push(0, 1, 0, 0, 1, 0, 0, 1, 0) // 綠      break    case 4: // back color    case 5:      colors.push(0, 0, 1, 0, 0, 1, 0, 0, 1) // 藍      break;    case 6: // left color    case 7:      colors.push(1, 1, 0, 1, 1, 0, 1, 1, 0) // 黃      break    case 8: // top texture uv,前兩個分量表示uv,第三個分量表示取第幾個紋理,在紋理實際索引值的基礎上加上textureBaseIndex      colors.push(0, 0, textureBaseIndex + 0, 1, 0, textureBaseIndex + 0, 1, 1, textureBaseIndex + 0)      break    case 9:      colors.push(1, 1, textureBaseIndex + 0, 0, 1, textureBaseIndex + 0, 0, 0, textureBaseIndex + 0)      break    case 10: // bottom texture uv,前兩個分量表示uv,第三個分量表示取第幾個紋理,在紋理實際索引值的基礎上加上textureBaseIndex      colors.push(1, 1, textureBaseIndex + 1, 0, 1, textureBaseIndex + 1, 0, 0, textureBaseIndex + 1)      break    case 11:      colors.push(0, 0, textureBaseIndex + 1, 1, 0, textureBaseIndex + 1, 1, 1, textureBaseIndex + 1)      break  }}geometry.setAttribute('color', new THREE.BufferAttribute(Float32Array.from(colors), 3))
自定義shanderMaterial

頂點著色器的程式碼比較簡單,把color屬性透過varying變數vColor傳給片元著色器:

function getVertexShader () {  return `    attribute vec3 color;    varying vec3 vColor;    void main () {      vColor = color;      gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );    }  `}

接下來是片元著色器,主要有以下幾點:

透過vColor.z判斷是純色還是貼圖;把貼圖資訊透過sampler2D陣列傳入,然後在根據vColor.z獲取陣列下標的時候,前面在生成下標的時候加了一個textureBaseIndex,所以用的時候得先減去;透過下標獲取sampler2D陣列中的某一項的時候,不能直接使用textures[index],glsl要求[]裡面的內容必須是Integral constant expression,所以使用了一個generateSwitch函式動態生成一系列if程式碼;

完整程式碼如下:

function getFragmentShader (textureLength, textureBaseIndex) {  function generateSwitch () {    let str = ''    for (let i = 0; i < textureLength; i++) {      str += `${str.length ? 'else' : ''} if (index == ${i}) {        gl_FragColor = texture2D(textures[${i}], vec2(vColor.x, vColor.y));      }      `    }    return str  }  return `    ${textureLength ? `      uniform sampler2D textures[${textureLength}];    ` : ''}    varying vec3 vColor;    void main () {      ${textureLength ? `        if (vColor.z <= 1.0) {          gl_FragColor = vec4(vColor, 1.0);        } else {          int index = int(vColor.z) - ${textureBaseIndex};          ${generateSwitch()}        }` : `        gl_FragColor = vec4(vColor, 1.0);        `      }    }  `}

生成自定義材質:

const textures = [  new THREE.TextureLoader().load('./textures/colors.png'), // 頂面貼圖  new THREE.TextureLoader().load('./textures/colors.png') // 底面貼圖]const material = new THREE.ShaderMaterial({  uniforms: {    textures: { value: textures } // 片元著色器中會使用  },  vertexShader: getVertexShader(),  fragmentShader: getFragmentShader(textures.length, textureBaseIndex)})
建立mesh

這步就比較簡單了,建立一個mesh,並新增到場景中:

const mesh = new THREE.Mesh(geometry, material)scene.add(mesh)

這樣,立方體就建立好了。本例使用了基本的WebGLRenderer,Scene,PerspectiveCamera,沒有特殊處理,這裡就不再寫了。實現效果截圖如下:front/right/top面效果截圖

back/left/bottom面效果截圖

擴充套件lambert材質,建立有光照效果的立方體

我的實際應用場景中的物體是lambert材質,也就是MeshLambertMaterial。所以,下面的例項程式碼以擴充套件lamert材質的光照效果為例。要想使用該實現方案,最好研究下THREE.js的原始碼。

THREE.js裡面預先定義了一系列材質,MeshLambertMaterial材質就是其中之一。這部分程式碼在src/renderers/shaders資料夾下面,ShaderLib.js裡面是材質的入口,比如MeshLambertMaterial:

const ShaderLib = {    lambert: {        uniforms: mergeUniforms( [ // uniform變數            UniformsLib.common,            UniformsLib.specularmap,            UniformsLib.envmap,            UniformsLib.aomap,            UniformsLib.lightmap,            UniformsLib.emissivemap,            UniformsLib.fog,            UniformsLib.lights,            {                emissive: { value: new Color( 0x000000 ) }            }        ] ),        vertexShader: ShaderChunk.meshlambert_vert, // 頂點著色器程式碼        fragmentShader: ShaderChunk.meshlambert_frag // 片元著色器程式碼    },}

ShaderChunk和ShaderLib資料夾下面就是實際的著色器程式碼,區別是ShaderLib是THREE.js給我們直接使用的,ShaderChunk是更細粒度的程式碼。ShderLib裡面的不同材質有很多共有的程式碼,所以這個共有的程式碼就提取成一個個ShaderChunk,達到複用的目的。一個材質是由多個ShaderChunk生成的。我們可以開啟ShaderLib/meshlambert_vert.glsl.js檔案,會發現裡面有很多#include語句,這些語句最後會被替換為實際的ShaderChunk裡面的片段。

我們看到shaders資料夾下面只是定義了材質的結構以及glsl程式碼片段,那麼,完整效果的程式碼是在哪生成的呢?src/renderers/webgl/WebGLProgram.js檔案。

列一下這個檔案我瞭解的一些知識點:

首先根據我們建立材質時的引數,定義一些#define變數,新增在著色器程式碼的前面;解析ShaderLib裡面的程式碼,把#include語句替換為實際程式碼,參見resolveIncludes函式;

更重要的是,ShaderLib裡面預定義的一些材質,掛在了THREE變數上,這樣我們就可以獲得原始程式碼,並透過修改部分glsl程式碼達到擴充套件材質的目的。

比如,上面的那個例子,首先改造一下頂點著色器:

在預設的lambert頂點著色器程式碼前面新增屬性變數和varying變數;在main函數里面給varying變數賦值;具體插在原始main函式的哪一行看你的需求;
function getVertexShader () {  let shader = `    attribute vec3 color;    varying vec3 vColor;  ` + THREE.ShaderLib.lambert.vertexShader  const index = shader.indexOf('#include <uv_vertex>')  shader = shader.slice(0, index) + `    vColor = color;  ` + shader.slice(index)  return shader}

片元著色器的改造如下:

在預設的lambert片元著色器程式碼前面新增uniform變數和varying變數;在main函數里面插入我們的程式碼,插入位置我選在了#include <color_fragment>後面,因為這個程式碼片段和我現在的修改做了類似的事情,所以插在這個位置是可以的。注意,此時就不是直接給gl_FragColor賦值了,而是把效果加在diffuseColor變數上。實際開發的時候,具體修改哪個值就得參考THREE.js原始碼了。
function getFragmentShader (textureLength, textureBaseIndex) {  function generateSwitch () {    let str = ''    for (let i = 0; i < textureLength; i++) {      str += `${str.length ? 'else' : ''} if (index == ${i}) {        diffuseColor *= texture2D(textures[${i}], vec2(vColor.x, vColor.y));      }      `    }    return str  }  let shader = `    uniform sampler2D textures[${textureLength}];    varying vec3 vColor;  ` + THREE.ShaderLib.lambert.fragmentShader  const index = shader.indexOf('#include <color_fragment>')  shader = shader.slice(0, index) + `    ${textureLength ? `      if (vColor.z <= 1.0) {        diffuseColor.rgb *= vColor;      } else {        int index = int(vColor.z) - ${textureBaseIndex};        ${generateSwitch()}      }` : `      diffuseColor.rgb *= vColor;      `    }  ` + shader.slice(index)  return shader}

然後,建立著色器:

修改一下uniform變數,把lambert預設的uniform變數也新增進去;新增lights引數為true,否則程式碼報錯;THREE原始碼預設diffuse是0xeeeeee,覆蓋一下,修改為0xffffff;
const material = new THREE.ShaderMaterial({  uniforms: THREE.UniformsUtils.merge([    THREE.ShaderLib.lambert.uniforms,    {      textures: { value: textures }    },    {      diffuse: {        value: new THREE.Color(0xffffff)      }    }  ]),  vertexShader: getVertexShader(),  fragmentShader: getFragmentShader(textures.length, textureBaseIndex),  lights: true})

這個時候重新整理頁面,會發現是一個黑色的立方體,這是因為我們還沒有新增光源:

const light = new THREE.DirectionalLight( 0xffffff ); // 平行光light.position.set( 1, 1, 1 );scene.add( light );const ambient = new THREE.AmbientLight(0xffffff, 0.7); // 環境光scene.add(ambient)

之所以新增兩個光源是因為發現:

環境光不受幾何物體法線影響;平行光受幾何物體法線影響;

新增上述程式碼後,如果把環境光註釋掉,會發現材質還是黑色的,這是因為上面建立的geometry沒有法線資訊,所以需要使用下面的方法新增一下法線資訊:

geometry.computeVertexNormals()

最終效果截圖如下:front/right/top面效果截圖,同時受平行光和環境光影響

back/left/bottom面效果截圖,不在平行光照射範圍內,只受環境光影響

6
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • Elasticsearch故障排查指引(案例詳解)