先上最终效果,我的博客虽然干货很少,不过逼格还是要有的。 effect.jpg

CSS3 是不够实现我的骚气哒~

在建博客前需要看了很多大佬的博客,大佬们的虽然简洁的界面,但内容的深度,文章的逼格,令人神往,无需博客外包装的浮华,就让大家争相阅读。 很久没自己设计网站,审美下降了不少,在几天不断的浏览中,定下了要比大家装的好,就得用大家不太常用的技术栈,three.js是我最终选择的装逼方案。

开始装逼

寻找模型

希望通过 3D 的效果达到 装逼 的目的,需要先建模,这玩意学起来一时半会是不可能的,所以就开始寻找免费非商用的 3D 模型,在免费的素材中寻找了许多的素材,最终选择了热爱的 MineCraft 的人物模型。

SketchFab(https://sketchfab.com/)

zombie_sketchfab.png

下载模型选择 GLTF,我们找的示例让我们引用的即是 GLTFLoader。当然有时也会下载到一些导出有问题的,亦可以下载原始结构,再通过 3D 编辑器 重新导出,此处推荐 Blender3D,我选择的模型上传者也刚好同名,也不知道是不是该软件商提供的。

3d_download.png

尝试编写

之后就先开始了 three.js 的尝试性编写,首先就是要先看 three.js 的文档了,进入官网,里面有许多的优秀案例及示例。

three.js 文档 建议先在网上先迅速阅读了 3d 的相关基础知识,及大佬们写的手把手教你玩 three.js 之类的文章

我的学习方法很多时候是先抄作业的基础上尝试查阅属性、改变属性,我们可以先从示例找到可以抄的示例先,我选的模型刚好有相关的三个骨架动画,所以阅读此示例的源码,可以优先将下载的模型引入并动起来

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      html,
      body {
        height: 100%;
      }
      .bg-canvas {
        width: 100%;
        height: 100%;
      }
    </style>
  </head>
  <body>
    <div class="bg-canvas"></div>
    <script src="https://unpkg.com/three@0.131.3/build/three.min.js"></script>
    <script src="https://unpkg.com/three@0.131.3/examples/js/loaders/GLTFLoader.js"></script>
    <script>
      (function () {
        var scene, renderer, camera, container;
        var mixer, clock, actions;

        function init() {
          // 获取绘制容器及容器尺寸
          container = document.querySelector(".bg-canvas");
          var width = container.getBoundingClientRect().width;
          var height = container.getBoundingClientRect().height;

          // 实例化透视摄像机,我们看见的即是此摄像机的视角。
          camera = new THREE.PerspectiveCamera(45, width / height, 1, 2000);
          camera.position.set(0, 2, 16);

          // 让模型帧动画按此实例化的时钟更新
          clock = new THREE.Clock();

          // 实例化场景
          scene = new THREE.Scene();

          // 实例化半球光,先把场景按灯光颜色照亮
          var hemiLight = new THREE.HemisphereLight(0xf2f2f2, 0xf2f2f2);
          hemiLight.position.set(0, 50, 0);
          scene.add(hemiLight);

          // 实例化太阳光,照亮的同时,为模型添加上阴影
          var dirLight = new THREE.DirectionalLight(0xffffff);
          dirLight.position.set(0, 30, 0);
          dirLight.castShadow = true;
          dirLight.shadow.camera.near = 0.1;
          dirLight.shadow.camera.far = 60;
          scene.add(dirLight);

          var loader = new THREE.GLTFLoader();

          // 加载模型
          loader.load("./zombie.gltf", function (gltf) {
            var model = gltf.scene;
            model.traverse(function (object) {
              if (object.isMesh) {
                object.frustumCulled = false;
                // 投射阴影
                object.castShadow = true;
              }
            });
            model.position.set(0.6, 0, 2.5);
            model.scale.set(1.5, 1.5, 1.5);
            scene.add(model);

            // 实例化动画混合器
            mixer = new THREE.AnimationMixer(model);
            actions = {
              push_up: mixer.clipAction(gltf.animations[0]),
              idle: mixer.clipAction(gltf.animations[1]),
              walk: mixer.clipAction(gltf.animations[2]),
            };
            actions.idle.play();
            // loader 需要时间加载,所以我们的渲染动画行为需要在loader的回调函数内
            animate();
          });

          // 实例化 WebGL渲染器,相关配置可以对照着文档调整
          renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
          renderer.setPixelRatio(window.devicePixelRatio);
          renderer.setSize(width, height);
          renderer.outputEncoding = THREE.sRGBEncoding;
          renderer.shadowMap.enabled = true;

          renderer.setSize(width, height);
          container.appendChild(renderer.domElement);
        }

        // 动画循环
        function animate() {
          requestAnimationFrame(animate);
          var delta = clock.getDelta();
          mixer.update(delta);
          render();
        }

        function render() {
          renderer.render(scene, camera);
        }

        init();
      })();
    </script>
  </body>
</html>

zombie_demo.gif

上面示例完成后,我们通过抄作业,大致了解了如果加入模型,动画渲染,摄像头视角等,对照着相关的类,查阅一遍属性及函数,就可以大概掌握其用法啦。

皮肤纹理我忘记是在哪里拿到的了,可能是在别的下载网站找到模型中 纹理贴图的 UV 刚好一致的 图片,替换图片即可,需要的可自行下载

zombie_texture.png

增加元素

那只有一个主角相对单调,我们可以根据我们抄作业学习到的,加一些其它模型,丰富场景。

最终决定加入这个模型 carriage.png

// loader 是已实例化的加载器,无须多次实例化加载器加载模型
loader.load("/gltf/pumpkin.gltf", function (gltf) {
  var model = gltf.scene;
  model.traverse(function (object) {
    if (object.isMesh) {
      object.frustumCulled = false;
      object.castShadow = true;
    }
  });
  model.rotation.set(0, 1, 0);
  model.position.set(1, 0.1, -2.4);
  let scale = 0.33;
  model.scale.set(scale, scale, scale);
  scene.add(model);
});

场景还没有给他加地面,Zombie 和 南瓜车 还似悬在空中,需要再为场景加一块地。随意搜索了张 MineCraft 的草地贴图,在 three.js 示例 中找到有可能关联的示例,刚好第一个就有草地,用小学的英语发现了该代码片段。 example_ground.png

由于设想给这个 3D 效果加上些简单交互,为了不太违和,将这个效果似展厅的方式展现,可以通过按钮控制主体的旋转以及 Zombie 的动作切换,最终选择了 CylinderGeometry(圆柱几何体),完成主体的效果。

其中主体的效果我们还需要不断的调整他到一个合适的位置,且当操控旋转时,需要一起多个几何体一同旋转,所以我们需要给这 3 个主体 群组。

// 建立一个 3D对象,将主体效果的 3个主体 包括进来,统一操控
stage = new THREE.Object3D();
scene.add(stage);

// 新增的草地

var groundTexture = new THREE.TextureLoader().load("/gltf/textures/grass.png");
//设置纹理的水平与垂直应用重复渲染
groundTexture.wrapS = groundTexture.wrapT = THREE.RepeatWrapping;
//设置纹理的重复渲染次数
groundTexture.repeat.set(4, 4);
// 增加材质的清晰度
groundTexture.anisotropy = 16;
groundTexture.encoding = THREE.sRGBEncoding;

var bottomBase = new THREE.Mesh(new THREE.CylinderBufferGeometry(5, 5, 0.1, 32), new THREE.MeshLambertMaterial({ map: groundTexture }));
// 材质是否接收阴影
bottomBase.receiveShadow = true;
stage.add(bottomBase);

// ...
// 以及将 loader 回调中加入场景 scene.add(model)改为
stage.add(model);

demo_effect.png

增加危险隔离带

确认主体效果即布局后,由于 主体效果 主要在右下角,右上角总感觉有些空,适当的想像场景,最后想到了增加隔离带,黄色与绿色的搭配也意外的合适,也就顺带定下了博客的主色等颜色的设计。

最初采用的是 css3 应用 animationbackground-position 调整达到隔离带的动效,但该方案基础扎实的小伙伴会发现,易引起重绘,CPU 转的嗡嗡的,表示很淦。

所以最近也就抽空将 隔离带 改用 Three.js 完成。 首先我们先在官方的示例中尝试抄抄作业,但并没有找到相似关联的示例,我们通过前面抄作业的学习,大概思考出效果的实现方式,常见的自然就是纹理偏移、纹理动画了。

搜索到的纹理操控文章: Threejs 纹理对象 Texture 阵列、偏移、旋转(纹理动画)

最终隔离带的代码实现:

// 因纹理变量需要给animate函数引用,所以需要在外面声明此纹理变量
var warningTexture;

//...
//function init(){
warningTexture = new THREE.TextureLoader().load("/img/warning_zone.png");
warningTexture.wrapS = THREE.RepeatWrapping;
warningTexture.repeat.set(4, 1);

var warningBar = new THREE.Mesh(
  // PlaneGeometry平面缓冲几何体
  new THREE.PlaneGeometry((0.8 * (424 * 4)) / 60, 0.8),
  new THREE.MeshBasicMaterial({ map: warningTexture })
);
scene.add(warningBar);

//...
//function animate(){
//...
warningTexture.offset.x -= 0.007;
//}

未完待续

我的博客是基于 hexo 改自行尝试编写主题,还未整理成可开源的主题项目,后期有时间也争取分享出来供大家参考。

three.js 我还只是菜鸡,大佬看看热闹,不要笑我,有好的玩法,也望不吝赐教~哈哈哈哈。

附件:最终代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      html,
      body {
        height: 100%;
      }
      .bg-canvas {
        width: 100%;
        height: 100%;
      }
    </style>
  </head>
  <body>
    <div class="bg-canvas"></div>
    <script src="https://unpkg.com/three@0.131.3/build/three.min.js"></script>
    <script src="https://unpkg.com/three@0.131.3/examples/js/loaders/GLTFLoader.js"></script>
    <script>
      (function () {
        var scene, renderer, camera, container;
        var mixer, clock, actions, warningTexture;

        function init() {
          // 获取绘制容器及容器尺寸
          container = document.querySelector(".bg-canvas");
          var width = container.getBoundingClientRect().width;
          var height = container.getBoundingClientRect().height;

          // 实例化透视摄像机,我们看见的即是此摄像机的视角。
          camera = new THREE.PerspectiveCamera(45, width / height, 1, 2000);
          camera.position.set(0, 2, 16);

          clock = new THREE.Clock();

          // 实例化场景
          scene = new THREE.Scene();

          // 实例化半球光,先把场景按灯光颜色照亮
          var hemiLight = new THREE.HemisphereLight(0xf2f2f2, 0xf2f2f2);
          hemiLight.position.set(0, 50, 0);
          scene.add(hemiLight);

          var dirLight = new THREE.DirectionalLight(0xffffff);
          dirLight.position.set(0, 30, 0);
          dirLight.castShadow = true;
          dirLight.shadow.camera.near = 0.1;
          dirLight.shadow.camera.far = 60;
          scene.add(dirLight);

          warningTexture = new THREE.TextureLoader().load("./warning_zone.png");
          warningTexture.wrapS = THREE.RepeatWrapping;
          warningTexture.repeat.set(4, 1);

          var warningBar = new THREE.Mesh(
            // PlaneGeometry平面缓冲几何体
            new THREE.PlaneGeometry((0.8 * (424 * 4)) / 60, 0.8),
            new THREE.MeshBasicMaterial({ map: warningTexture })
          );
          warningBar.position.set(0, 1.5, 5.4);
          warningBar.rotation.set(0, -0.3, -0.1);
          scene.add(warningBar);

          stage = new THREE.Object3D();
          scene.add(stage);

          var groundTexture = new THREE.TextureLoader().load("./textures/grass.png");
          groundTexture.wrapS = groundTexture.wrapT = THREE.RepeatWrapping;
          groundTexture.repeat.set(4, 4);
          groundTexture.anisotropy = 16;
          groundTexture.encoding = THREE.sRGBEncoding;

          var bottomBase = new THREE.Mesh(new THREE.CylinderBufferGeometry(5, 5, 0.1, 32), new THREE.MeshLambertMaterial({ map: groundTexture }));
          bottomBase.receiveShadow = true;
          stage.add(bottomBase);

          var loader = new THREE.GLTFLoader();

          loader.load("./pumpkin.gltf", function (gltf) {
            var model = gltf.scene;
            model.traverse(function (object) {
              if (object.isMesh) {
                object.frustumCulled = false;
                object.castShadow = true;
              }
            });
            model.rotation.set(0, 1, 0);
            model.position.set(1, 0.1, -2.4);
            let scale = 0.33;
            model.scale.set(scale, scale, scale);
            stage.add(model);
          });
          // 加载模型
          loader.load("./zombie.gltf", function (gltf) {
            var model = gltf.scene;
            model.traverse(function (object) {
              if (object.isMesh) {
                object.frustumCulled = false;
                // 投射阴影
                object.castShadow = true;
              }
            });
            model.position.set(0.6, 0, 2.5);
            model.scale.set(1.5, 1.5, 1.5);
            stage.add(model);

            // 实例化动画混合器
            mixer = new THREE.AnimationMixer(model);
            actions = {
              push_up: mixer.clipAction(gltf.animations[0]),
              idle: mixer.clipAction(gltf.animations[1]),
              walk: mixer.clipAction(gltf.animations[2]),
            };
            actions.walk.play();
            animate();
          });

          // 实例化 WebGL渲染器,相关配置可以对照着文档调整
          renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
          renderer.setPixelRatio(window.devicePixelRatio);
          renderer.setSize(width, height);
          renderer.outputEncoding = THREE.sRGBEncoding;
          renderer.shadowMap.enabled = true;

          renderer.setSize(width, height);
          container.appendChild(renderer.domElement);
        }

        function animate() {
          requestAnimationFrame(animate);
          var delta = clock.getDelta();
          warningTexture.offset.x -= 0.007;
          mixer.update(delta);
          render();
        }

        function render() {
          renderer.render(scene, camera);
        }

        init();
      })();
    </script>
  </body>
</html>