惊呆了,3D的刮刮乐你见过吗?

453 阅读3分钟

本文正在参加「金石计划 . 瓜分6万现金大奖」 

前言

相信很多人都见过2D的刮刮乐,甚至不少同学都自己写过2d的刮刮乐开奖页面。我们通常是用canvas来实现,通过将上面一层蒙版清除,露出底下的HTML(中奖结果,大概率是再接再厉)。看了那么久的2d的相信大家都看腻了吧。今天就来做个3d的刮刮乐,玩玩吧。 (效果如下) Image_20221028092441.png

代码地址

code.juejin.cn/pen/7159077…

https://code.juejin.cn/pen/7159077867966431272

首先

在这里我使用threejs来实现这个3d的效果。首先,我开始搭建场景。基本的scene+light+renderer,渲染出一个threejs场景的canvas的dom元素加到html中。

   `container = document.createElement('div');
    document.body.appendChild(container);
    camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 3000);
    camera.position.set(0, 200, 350);
    camera.lookAt(0, 0, 0);scene = new THREE.Scene();
    
    const sun = new THREE.DirectionalLight(0xFFFFFF, 1.0);
    sun.position.set(300, 400, 175);
    scene.add(sun);

    const sun2 = new THREE.DirectionalLight(0x40A040, 0.6);
    sun2.position.set(- 100, 350, - 200);
    scene.add(sun2);

    renderer = new THREE.WebGLRenderer();
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(window.innerWidth, window.innerHeight);
    container.appendChild(renderer.domElement);

    stats = new Stats();
    container.appendChild(stats.dom);

    container.style.touchAction = 'none';  `

接下来,重头戏来了。

我们开始给场景添加刮刮乐的mesh。由于刮刮乐被刮开之后的形状的随机性,以及顶点数据不断变化的复杂性。我们这里使用shader(着色器)来实现。 和传统2d的刮刮乐一样,我们也是底下一层上面有中奖信息(或者再接再励),上面是一层蒙版。不同是这是两层mesh。底部的mesh,可以直接使用CanvasTexture,将文字信息贴到底层的plant上方的面,就可以了。这部分比较简单,就不赘述了。我们着重讲下顶上的蒙层究竟是如何实现:

首先排除threeBsp,因为鼠标在不断移动的时候,需要在蒙版上一个一个圆形扣除,数据的计算量非常大,导致刮刮乐直接变成卡卡乐。我们只能使用ShaderMaterial来实现。因为着色器的性能较好,直接gpu并行处理。

了解threejs的童鞋们应该知道高度图吧,我们在表示复杂的地形场景的时候,经常使用颜色的深浅来表示高度的层次。这里就是通过高度图的方式来进行抠图,直接把上面蒙层的高度(离屏渲染动态改变的高度图)改变,这样就可以露出了下面的开奖结果了。如此,我们只需要在高度图上,根据鼠标经过的点坐标,给高度图上绘制上颜色,整个功能就完成了。

思路捋顺了,事情已经完成了一大半,接下来就开始了枯燥的coding~~

1.定义一个离屏渲染的高度图

 this.heightmapVariable = this.gpuCompute.addVariable(
      'heightmap',
      `#include <common>

  		uniform vec2 mousePos;
  		uniform float mouseSize;
  		uniform float viscosityConstant;
  		uniform float heightCompensation;

  		void main()	{

  			vec2 cellSize = 1.0 / resolution.xy;

  			vec2 uv = gl_FragCoord.xy * cellSize;

  			// heightmapValue.x == height from previous frame
  			// heightmapValue.y == height from penultimate frame
  			// heightmapValue.z, heightmapValue.w not used
  			vec4 heightmapValue = texture2D( heightmap, uv );

  			// Get neighbours
  			vec4 north = texture2D( heightmap, uv + vec2( 0.0, cellSize.y ) );
  			vec4 south = texture2D( heightmap, uv + vec2( 0.0, - cellSize.y ) );
  			vec4 east = texture2D( heightmap, uv + vec2( cellSize.x, 0.0 ) );
  			vec4 west = texture2D( heightmap, uv + vec2( - cellSize.x, 0.0 ) );

  			// https://web.archive.org/web/20080618181901/http://freespace.virgin.net/hugo.elias/graphics/x_ggl.htm

  			// float newHeight = ( ( north.x + south.x + east.x + west.x ) * 0.5 - heightmapValue.y ) * viscosityConstant;
  			float  newHeight = -1.0;

  			// Mouse influence
  			float mousePhase = clamp( length( ( uv - vec2( 0.5 ) ) * BOUNDS - vec2( mousePos.x, - mousePos.y ) ) * PI  / mouseSize, 0.0, PI/8.0 );
  			newHeight -= ( cos( mousePhase ) - 1.0 ) * 32.0;

  			// heightmapValue.y = heightmapValue.x;
  			if (newHeight < heightmapValue.x) {
  				heightmapValue.x = newHeight-2.0;
  			}

  			gl_FragColor = heightmapValue;

  		}`
    );

2.通过uniforms传给着色器材质使用。 code.png

3.通过GPUComputationRenderer渲染高度图

//   this.gpuCompute = new GPUComputationRenderer(WIDTH, WIDTH, this.renderer);
//之前注册过这个属性
 this.gpuCompute.setVariableDependencies(this.heightmapVariable, [
        this.heightmapVariable,
      ]);

      this.heightmapVariable.material.uniforms['mousePos'] = {
        value: new THREE.Vector2(500, 500),
      };
      this.heightmapVariable.material.uniforms['mouseSize'] = { value: 400.0 };
      //连锁反应
      this.heightmapVariable.material.uniforms['viscosityConstant'] = {
        value: 0.98,
      };
      // this.heightmapVariable.material.uniforms[ "heightCompensation" ] = { value: 9 };
      this.heightmapVariable.material.defines.BOUNDS = BOUNDS.toFixed(1);

4.记录鼠标位置来改变高度图内容,并且改变蒙层

  render() {
      // Set uniforms: mouse interaction
      const uniforms = this.heightmapVariable.material.uniforms;
      if (this.mouseMoved) {
        this.raycaster.setFromCamera(this.mouseCoords, this.camera);

        const intersects = this.raycaster.intersectObject(this.meshRay);

        if (intersects.length > 0) {
          const point = intersects[0].point;
          uniforms['mousePos'].value.set(point.x, point.z);
        } else {
          uniforms['mousePos'].value.set(10000, 10000);
        }

        this.mouseMoved = false;
      } else {
        uniforms['mousePos'].value.set(10000, 10000);
      }

      // Do the gpu computation
      this.gpuCompute.compute();

      // Get compute output in custom uniform
      this.gglUniforms['heightmap'].value =
        this.gpuCompute.getCurrentRenderTarget(this.heightmapVariable).texture;

      // Render
      this.renderer.render(this.scene, this.camera);
    },

结束

到这里就结束了,是不是很简单。如果有啥疑问,欢迎评论区留言讨论,大家一起学习进步。