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 进行序列帧动画的添加和调试。最终效果以下:markdown

感谢一块儿工做的前端同事李战帮助一块儿进行工程化开发及项目总结的支持,也感谢其余合做方在开发过程当中的支持和配合。网络

相关文章
相关标签/搜索