2019雙十一拼圖遊戲 WebGL 揭祕

前言

WebGL 應用已經比較成熟,在網絡中也能找到不少精彩的應用。 本次活動中,應用 WebGL 技術,配合設計師,基於 C4D 軟件模型編輯,經過 Blender 進行標準化輸出,最終在 Web 端呈現交互。 在此咱們一塊兒看一下製做開發流程及不少吸引人的技術細節,以及對於新技術落地應用的主要模式。 主要大綱以下:前端

  1. 技術方案及學習資料參考
  2. WebGL 技術實現方案
  3. 總結

1. 技術方案及學習資料參考

1.1. 起源

WebGL於 2011年2月 落地於瀏覽器,最先是 Chrome9 和 Firefox4。 當時,Google Creative Lab 利用 WebGL 技術進行交互展現的頁面開發,體現了 WebGL 的強大力量。git

ROME 3 DREAMS OF BLACKgithub

如今不少主流瀏覽器對於 WebGL 也都支持了,而且可使用相同的體驗,臺式機,平板,手機,所以,咱們能夠藉助瀏覽器進行基於 WebGL 的跨平臺的開發。編程

1.2. WebGL 學習曲線

1.3. 遊戲參考

1.4. 最終指望效果

2. WebGL 技術實現方案

2.1. 技術選型

2.2. 方案實現

原理概述 瀏覽器

2.2.1 初始化

class THREERoot {
  constructor (wrapper, config) {
    // 配置解析
    // ... CONFIG
    
    // 場景初始化
    this.scene = new THREE.Scene()

    const { frustumSize, aspect } = this.config.OrthographicCamera

    // 攝像機初始化
    this.camera = new THREE.OrthographicCamera(
      -frustumSize * aspect / 2,
      frustumSize * aspect / 2,
      frustumSize / 2,
      -frustumSize / 2,
      this.config.OrthographicCamera.near,
      this.config.OrthographicCamera.far
    )
    
    // 渲染引擎初始化
    this.renderer = new THREE.WebGLRenderer({
      antialias: true,
      alpha: true
    })

    // 控制器初始化
    this.config.enableControls && 
      (this.controls = new THREE.TrackballControls(this.camera, this.config.controlsDomElement || this.renderer.domElement))
  }
  
  // 關鍵幀處理
  animate () {
    this.renderer.render(this.scene, this.camera)

    this.config.enableControls && this.controls.update()
    this.camera.updateMatrixWorld()
    this.camera.updateProjectionMatrix()

    this.animateCallback && this.animateCallback()
    this.animateReq = this.requestAnimationFrame.bind(window)(this.animate)
  }
}

const gameInit = () => {
  const { root } = gameConfig
  const {
    scene,
    camera
  } = root
  
  // 遊戲背景初始化
  scene.background = new THREE.Color(0xa0a0a0)
  
    
  // 輔助座標軸
  const axesHelper = new THREE.AxesHelper(5)
  scene.add(axesHelper)
  
  // 相機位置
  camera.up = new THREE.Vector3(0.0, 1.0, 0.0)
  camera.position.z = gameConfig.cameraRadius

  // 相機初始座標
  camera.userData = {
    standardPosition: camera.position.clone(),
    standardRotation: camera.rotation.clone()
  }
}

// 實例化運行
gameConfig.root = new THREERoot()
gameInit({ gameConfig })
gameConfig.root.animate()
複製代碼

2.2.2 模型和貼圖加載

// NOTE Blender 導出模型加載
const loader = new THREE.GLTFLoader()      
loader.load('./assets/demo.glb', function (gltf) {
    const textureLoader = new THREE.TextureLoader()
    // texture 貼圖加載,模型加載完成後能夠預運行
    const texture = textureLoader.load('./assets/demo.png')
    console.log('模型數據:', gltf)
})
複製代碼

2.2.3 模型數據解析展現

gltf.scene.traverse(function (child) {
  if (child.isMesh) {
    const positions = child.geometry.attributes.position.array
    const normals = child.geometry.attributes.normal.array
    const uvs = child.geometry.attributes.uv.array
    const indexs = child.geometry.index.array

    const group = new THREE.Group()

    for (let i = 0, l = indexs.length; i < l; i += 3) {
      // 點座標索引
      const points = [
        [indexs[i + 0] * 3, indexs[i + 0] * 3 + 1, indexs[i + 0] * 3 + 2],
        [indexs[i + 1] * 3, indexs[i + 1] * 3 + 1, indexs[i + 1] * 3 + 2],
        [indexs[i + 2] * 3, indexs[i + 2] * 3 + 1, indexs[i + 2] * 3 + 2]
      ]
      // 貼圖座標索引
      const coordinates = [
        [indexs[i + 0] * 2, indexs[i + 0] * 2 + 1],
        [indexs[i + 1] * 2, indexs[i + 1] * 2 + 1],
        [indexs[i + 2] * 2, indexs[i + 2] * 2 + 1]
      ]
      
      // 點座標
      const positionsTraverse = new Float32Array([
        positions[ points[0][0] ], positions[ points[0][1] ], positions[ points[0][2] ],
        positions[ points[1][0] ], positions[ points[1][1] ], positions[ points[1][2] ],
        positions[ points[2][0] ], positions[ points[2][1] ], positions[ points[2][2] ]
      ])
      // 法線座標
      const normalsTraverse = new Float32Array([
        normals[ points[0][0] ], normals[ points[0][1] ], normals[ points[0][2] ],
        normals[ points[1][0] ], normals[ points[1][1] ], normals[ points[1][2] ],
        normals[ points[2][0] ], normals[ points[2][1] ], normals[ points[2][2] ]
      ])
      // uv貼圖座標
      const uvsTraverse = new Float32Array([
        uvs[ coordinates[0][0] ], uvs[ coordinates[0][1] ],
        uvs[ coordinates[1][0] ], uvs[ coordinates[1][1] ],
        uvs[ coordinates[2][0] ], uvs[ coordinates[2][1] ]
      ])

      // 頂點着色緩衝對象
      const geometry = new THREE.BufferGeometry()
      geometry.setAttribute('position', new THREE.BufferAttribute(positionsTraverse, 3))
      geometry.setAttribute('normal', new THREE.BufferAttribute(normalsTraverse, 3))
      geometry.setAttribute('uv', new THREE.BufferAttribute(uvsTraverse, 2))
      geometry.setIndex([0, 1, 2])

      // 材質對象
      const material = new THREE.MeshBasicMaterial( { wireframe: true, color: 0xffaa00 } )
      material.side = THREE.DoubleSide
      const mesh = new THREE.Mesh(geometry, material)
      // NOTE 這裏 的 1 / 100 比例縮放是 blender 導出以後的參數修正
      // 相對於 c4d 來講的話,scale 是 1
      mesh.scale.set(gameConfig.scale, gameConfig.scale, gameConfig.scale)

      group.add(mesh)
    }
    group.name = gameConfig.groupName
    scene.add(group)
  }
})
複製代碼

2.2.4 球體座標變換

// NOTE 這裏先建立球體進行座標變換
const phi = Math.acos(-1 + (2 * i) / l)
const theta = Math.sqrt(l * Math.PI) * phi
const meshGroup = new THREE.Group()
meshGroup.name = gameConfig.meshGroupName
meshGroup.position.setFromSphericalCoords(2, phi, theta)

meshGroup.add(mesh)
group.add(meshGroup)
複製代碼

2.2.5 更改每一個 mesh 旋轉中心而且面向 camera

// NOTE 更改每一個三角面的中心點
geometry.computeBoundingBox()
var center = new THREE.Vector3()
geometry.boundingBox.getCenter(center)
geometry.center()
mesh.position.copy(center)
mesh.position.multiplyScalar(gameConfig.scale)

mesh.geometry.lookAt(camera.position)
複製代碼
root.animateCallback = () => {
    if (child.isMesh) {
      if (!child.children.length) {
        child.lookAt(camera.position)
      }
    }
  })
}
複製代碼

2.2.6 迴歸原來位置並添加紋理

// Mesh 迴歸原來位置
mesh.position.x -= meshGroup.position.x
mesh.position.y -= meshGroup.position.y
mesh.position.z -= meshGroup.position.z

mesh.geometry.lookAt(camera.position)
複製代碼
// 紋理頂點着色
const textureVertex = ` #ifdef GL_ES precision mediump float; #endif // attribute float size; varying vec2 vUv; void main() { vUv = uv; vec4 modelViewPosition = modelViewMatrix * vec4(position, 1.0); gl_Position = projectionMatrix * modelViewPosition; } `
// 紋理片元着色
const textureFragment = ` #ifdef GL_ES precision mediump float; #endif uniform vec2 u_resolution; uniform float u_time; uniform vec3 u_position; uniform float instensity; varying vec2 vUv; // u_texture uniform sampler2D u_texture; void main (void) { vec2 uv = gl_FragCoord.xy / u_resolution; vec4 textureColor = texture2D(u_texture, vUv); gl_FragColor = vec4(textureColor.rgb * instensity, textureColor.a); } `

// Material 建立
const material = new THREE.ShaderMaterial({
  uniforms: {
    u_texture: {
      type: 'sampler2D',
      value: texture
    },
    u_resolution: new THREE.Uniform(new THREE.Vector2()),
    instensity: { type: 'f', value: 1.0 }
  },
  fragmentShader: textureFragment,
  vertexShader: textureVertex
})
複製代碼

2.2.7 添加攝像機變換及隨機交互參數

// 每一個 Mesh 配置隨機偏移參數
mesh.userData = {
    rotationRandom: Math.random() * 2 - 1,
    positionRandom: Math.random() - 0.5,
    rotation: camera.rotation.clone()
}
複製代碼
// 每一個 Mesh 進行隨機位置偏移
const {
    rotationRandom,
    positionRandom,
    rotation
} = child.userData

if (!child.children.length) {
    // NOTE blender 導出模型 gltf2.0 選項必定要勾選 +Y up
    child.rotation.z = (rotation.x - Math.sin(x)) * rotationRandom * rotationOffset
    child.position.z = gameConfig.positionOffset * positionRandom
    child.lookAt(camera.position)
}
複製代碼

3. 總結

在完成開發步驟以後,可以使用一些動畫庫對 Mesh 進行序列幀動畫的添加和調試。最終效果以下:網絡

感謝一塊兒工做的前端同事李戰幫助一塊兒進行工程化開發及項目總結的支持,也感謝其餘合做方在開發過程當中的支持和配合。app

相關文章
相關標籤/搜索