【愚公系列】2022年09月 微信小程序-three.js加载3D模型

举报
愚公搬代码 发表于 2022/09/30 22:39:09 2022/09/30
【摘要】 @TOC 前言Three.js 是一款运行在浏览器中的 3D 引擎,你可以用它创建各种三维场景,包括了摄影机、光影、材质等各种对象。一个典型的 Three.js 程序至少要包括渲染器(Renderer)、场景(Scene)、照相机(Camera),以及你在场景中创建的物体。Three.js相关文档:http://docs.thingjs.com/ 一、Three.js的使用安装第三方包:np...

@TOC


前言

Three.js 是一款运行在浏览器中的 3D 引擎,你可以用它创建各种三维场景,包括了摄影机、光影、材质等各种对象。

一个典型的 Three.js 程序至少要包括渲染器(Renderer)、场景(Scene)、照相机(Camera),以及你在场景中创建的物体。

Three.js相关文档:http://docs.thingjs.com/
在这里插入图片描述

一、Three.js的使用

安装第三方包:npm i --save threejs-miniprogram

1.3D模型的绘制

<view style="display:inline-block;">
  <button size="mini" type="primary" class="btn" data-action="Walking" bindtap="play"></button>
  <button size="mini" type="primary" class="btn"  data-action="WalkJump" bindtap="play"></button>
  <button size="mini" type="primary" class="btn"  data-action="Sitting" bindtap="play"></button>
  <button size="mini" type="primary" class="btn"  data-action="Standing" bindtap="play"></button>
</view>
<canvas
  type="webgl"
  id="webgl"
  style="width: 100%; height: 450px;"
  bindtouchstart="touchStart"
  bindtouchmove="touchMove"
  bindtouchend="touchEnd"
></canvas>
import { createScopedThreejs } from 'threejs-miniprogram'

const { renderModel } = require('../../../lib/test-cases/model')

const app = getApp()

Page({
  data: {},
  onLoad: function () {
    wx.createSelectorQuery()
      .select('#webgl')
      .node()
      .exec((res) => {
        const canvas = res[0].node
        this.canvas = canvas
        const THREE = createScopedThreejs(canvas)
        
        this.fadeToAction = renderModel(canvas, THREE)//3d model
        console.log(renderOrbit);
      })
  },
  play(e){
    let action = e.currentTarget.dataset.action
    this.fadeToAction(action)
  },
  touchStart(e) {
    this.canvas.dispatchTouchEvent({...e, type:'touchstart'})
  },
  touchMove(e) {
    this.canvas.dispatchTouchEvent({...e, type:'touchmove'})
  },
  touchEnd(e) {
    this.canvas.dispatchTouchEvent({...e, type:'touchend'})
  }
})

二、3D模型相关js文件

import { registerGLTFLoader } from '../loaders/gltf-loader'
import registerOrbit from "./orbit"

export function renderModel(canvas, THREE) {

  // glTF是一种开放格式的规范,用于更高效地传输、加载3D内容。
  // 是为了使用GLTFLoader,与下面的registerOrbit类似
  registerGLTFLoader(THREE)

  var container, stats, clock, gui, mixer, actions, activeAction, previousAction;
  var camera, scene, renderer, model, face, controls;
  var api = { state: 'Walking' };

  init();
  animate();

  function init() {

    // 创建透视相机,这一透视相机,被用来模拟人眼所看到的景象,这是3D场景渲染中使用最普遍的投影模式。
camera = new THREE.PerspectiveCamera(45, canvas.width / canvas.height, 0.25, 100);
// 设置相机位置
camera.position.set(- 5, 3, 10);
// 相机看向哪个坐标
camera.lookAt(new THREE.Vector3(0, 2, 0));

// 创建场景
// 一个放置物体、灯光和摄像机的地方。
scene = new THREE.Scene();
scene.background = new THREE.Color(0xe0e0e0);
// 雾,线性雾,雾的密度是随着距离线性增大的
scene.fog = new THREE.Fog(0xe0e0e0, 20, 100);

// Three.js时钟对象
// 是为了计时用的,相当于代替requestAnimationFrame返回时间差
clock = new THREE.Clock();

    // 创建光源
    // 半球光
    // 光源直接放置于场景之上,光照颜色从天空光线颜色,渐变到地面光线颜色。
    // 不能投射阴影。
    // skyColor : 0xffffff, groundColor : 0x444444,
var light = new THREE.HemisphereLight(0xffffff, 0x444444);
light.position.set(0, 20, 0);
scene.add(light);
// 平行光,常用平行光来模拟太阳光的效果
light = new THREE.DirectionalLight(0xffffff);
light.position.set(0, 20, 10);
scene.add(light);

    /// 构造带网格的大地辅助
    // 网格Mesh
    // 平面几何体PlaneGeometry,PlaneBufferGeometry是PlaneGeometry中的BufferGeometry接口,使用 BufferGeometry 可以有效减少向 GPU 传输上述数据所需的开销。
    // MeshPhongMaterial,一种用于具有镜面高光表面的材质。
var mesh = new THREE.Mesh(new THREE.PlaneBufferGeometry(2000, 2000), new THREE.MeshPhongMaterial({ color: 0x999999, depthWrite: false }));
mesh.rotation.x = - Math.PI / 2;
scene.add(mesh);
// 坐标格辅助对象,坐标格实际上是2维数组
var grid = new THREE.GridHelper(200, 40, 0x000000, 0x000000);
grid.material.opacity = 0.2;
grid.material.transparent = true;
scene.add(grid);

    // 创建加载器,加载模型文件
var loader = new THREE.GLTFLoader();
// .GLB 文件
// 文件类似于GLTF文件,因为它们可能包含嵌入式资源,也可能引用外部资源。如果一个.GLB 文件带有单独的资源,它们很可能是以下文件:
// 二进制(.BIN )文件-包含动画、几何图形和其他数据的一个或多个BIN文件。
// 着色器(GLSL)文件-一个或多个包含着色器的GLSL文件。
// 图像(.JPG 、.PNG 等)文件-包含三维模型纹理的一个或多个文件。

loader.load('https://threejs.org/examples/models/gltf/RobotExpressive/RobotExpressive.glb', function (gltf) {
  // gltf.animations; // Array<THREE.AnimationClip>
  // gltf.scene; // THREE.Group
  // gltf.scenes; // Array<THREE.Group>
  // gltf.cameras; // Array<THREE.Camera>
  // gltf.asset; // Object  
  model = gltf.scene;//三维物体的组
  scene.add(model);
  // 
  createGUI(model, gltf.animations)
}, undefined, function (e) {
  console.error(e);
});

    // 创建渲染器,渲染场景
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(wx.getSystemInfoSync().pixelRatio);
renderer.setSize(canvas.width, canvas.height);
renderer.gammaOutput = true;
renderer.gammaFactor = 2.2;

    // 创建控制器
    // Orbit controls,轨道控制器,可以使得相机围绕目标进行轨道运动
    // 表现就是可以使用鼠标或手指旋转物体
    // 在外部需要事件配合传入
    // registerOrbit是为了使用轨道控制器
const { OrbitControls } = registerOrbit(THREE)
controls = new OrbitControls( camera, renderer.domElement );

    camera.position.set( 5, 5, 10 );
    controls.update();
  }

  // 创建混合器
  // 处理动作
function createGUI(model, animations) {
  var states = ['Idle', 'Walking', 'Running', 'Dance', 'Death', 'Sitting', 'Standing'];
  var emotes = ['Jump', 'Yes', 'No', 'Wave', 'Punch', 'ThumbsUp'];
  // 创建帧动画混合器对象AnimationMixer,主要用于播放帧动画,可以播放所有子对象所绑定的帧动画,
  // 执行混合器对象AnimationMixer的方法.clipAction(clip)把包含关键帧数据的剪辑对象AnimationClip作为参数,会返回一个帧动画操作对象AnimationAction,通过AnimationAction对象的方法.play()可以播放帧动画。
  mixer = new THREE.AnimationMixer(model);
  actions = {};
  for (var i = 0; i < animations.length; i++) {
    var clip = animations[i];
// 取出帧动画操作对象AnimationAction,以备播放用
    var action = mixer.clipAction(clip);
    actions[clip.name] = action;
    // 
    if (emotes.indexOf(clip.name) >= 0 || states.indexOf(clip.name) >= 4) {
      // 暂停在最后一帧播放的状态
      action.clampWhenFinished = true;
      // 不循环播放
      action.loop = THREE.LoopOnce;
    }
    console.log('clip.name',clip.name);
    
  }
  // expressions
  // 检索对象的子类对象,然后返回第一个匹配到name的
  // 没有用到
  face = model.getObjectByName('Head_2');

  // 默认的动作
  activeAction = actions['WalkJump'];
  activeAction.play();
}

// 平滑切换动作
function fadeToAction(name, duration = 1) {
  previousAction = activeAction;
  
  activeAction = actions[name];
  if (previousAction !== activeAction) {
    previousAction.fadeOut(duration);
  }
  // 链式调用
  // TimeScale是时间的比例因子. 值为0时会使动画暂停。值为负数时动画会反向执行。默认值是1。
  // weight,动作的影响程度,取值范围[0, 1]。0 =无影响,1=完全影响,之间的值可以用来混合多个动作。默认值是1
  activeAction
    .reset()
    .setEffectiveTimeScale(1)//设置时间比例(timeScale)以及停用所有的变形
    .setEffectiveWeight(1)//设置权重weight,以及停止所有淡入淡出
    .fadeIn(duration)//在传入的时间间隔内,逐渐将此动作的权重weight,由0升到1
    .play();//让混合器激活动作
  }

  // 循环渲染场景
function animate() {
  // 简单说.getDelta ()方法的功能就是获得前后两次执行该方法的时间间隔
  // 返回间隔时间单位是秒
  var dt = clock.getDelta();
  if (mixer) mixer.update(dt);//更新混合器,是动画关键
  canvas.requestAnimationFrame(animate);
  controls.update()
  renderer.render(scene, camera);
}

  return fadeToAction
}
export function registerGLTFLoader(THREE) {

  /**
 * @author Rich Tibbett / https://github.com/richtr
 * @author mrdoob / http://mrdoob.com/
 * @author Tony Parisi / http://www.tonyparisi.com/
 * @author Takahiro / https://github.com/takahirox
 * @author Don McCurdy / https://www.donmccurdy.com
 */

  THREE.GLTFLoader = (function () {

    function GLTFLoader(manager) {

      this.manager = (manager !== undefined) ? manager : THREE.DefaultLoadingManager;
      this.dracoLoader = null;
      this.ddsLoader = null;

    }

    GLTFLoader.prototype = {

      constructor: GLTFLoader,

      crossOrigin: 'anonymous',

      load: function (url, onLoad, onProgress, onError) {

        var scope = this;

        var resourcePath;

        if (this.resourcePath !== undefined) {

          resourcePath = this.resourcePath;

        } else if (this.path !== undefined) {

          resourcePath = this.path;

        } else {

          resourcePath = THREE.LoaderUtils.extractUrlBase(url);

        }

        // Tells the LoadingManager to track an extra item, which resolves after
        // the model is fully loaded. This means the count of items loaded will
        // be incorrect, but ensures manager.onLoad() does not fire early.
        scope.manager.itemStart(url);

        var _onError = function (e) {

          if (onError) {

            onError(e);

          } else {

            console.error(e);

          }

          scope.manager.itemError(url);
          scope.manager.itemEnd(url);

        };

        var loader = new THREE.FileLoader(scope.manager);

        loader.setPath(this.path);
        loader.setResponseType('arraybuffer');

        if (scope.crossOrigin === 'use-credentials') {

          loader.setWithCredentials(true);

        }

        loader.load(url, function (data) {

          try {

            scope.parse(data, resourcePath, function (gltf) {

              onLoad(gltf);

              scope.manager.itemEnd(url);

            }, _onError);

          } catch (e) {

            _onError(e);

          }

        }, onProgress, _onError);

      },

      setCrossOrigin: function (value) {

        this.crossOrigin = value;
        return this;

      },

      setPath: function (value) {

        this.path = value;
        return this;

      },

      setResourcePath: function (value) {

        this.resourcePath = value;
        return this;

      },

      setDRACOLoader: function (dracoLoader) {

        this.dracoLoader = dracoLoader;
        return this;

      },

      setDDSLoader: function (ddsLoader) {

        this.ddsLoader = ddsLoader;
        return this;

      },

      parse: function (data, path, onLoad, onError) {

        var content;
        var extensions = {};

        if (typeof data === 'string') {

          content = data;

        } else {

          var magic = THREE.LoaderUtils.decodeText(new Uint8Array(data, 0, 4));

          if (magic === BINARY_EXTENSION_HEADER_MAGIC) {

            try {

              extensions[EXTENSIONS.KHR_BINARY_GLTF] = new GLTFBinaryExtension(data);

            } catch (error) {

              if (onError) onError(error);
              return;

            }

            content = extensions[EXTENSIONS.KHR_BINARY_GLTF].content;

          } else {

            content = THREE.LoaderUtils.decodeText(new Uint8Array(data));

          }

        }

        var json = JSON.parse(content);

        if (json.asset === undefined || json.asset.version[0] < 2) {

          if (onError) onError(new Error('THREE.GLTFLoader: Unsupported asset. glTF versions >=2.0 are supported. Use LegacyGLTFLoader instead.'));
          return;

        }

        if (json.extensionsUsed) {

          for (var i = 0; i < json.extensionsUsed.length; ++i) {

            var extensionName = json.extensionsUsed[i];
            var extensionsRequired = json.extensionsRequired || [];

            switch (extensionName) {

              case EXTENSIONS.KHR_LIGHTS_PUNCTUAL:
                extensions[extensionName] = new GLTFLightsExtension(json);
                break;

              case EXTENSIONS.KHR_MATERIALS_UNLIT:
                extensions[extensionName] = new GLTFMaterialsUnlitExtension();
                break;

              case EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS:
                extensions[extensionName] = new GLTFMaterialsPbrSpecularGlossinessExtension();
                break;

              case EXTENSIONS.KHR_DRACO_MESH_COMPRESSION:
                extensions[extensionName] = new GLTFDracoMeshCompressionExtension(json, this.dracoLoader);
                break;

              case EXTENSIONS.MSFT_TEXTURE_DDS:
                extensions[EXTENSIONS.MSFT_TEXTURE_DDS] = new GLTFTextureDDSExtension(this.ddsLoader);
                break;

              case EXTENSIONS.KHR_TEXTURE_TRANSFORM:
                extensions[EXTENSIONS.KHR_TEXTURE_TRANSFORM] = new GLTFTextureTransformExtension();
                break;

              default:

                if (extensionsRequired.indexOf(extensionName) >= 0) {

                  console.warn('THREE.GLTFLoader: Unknown extension "' + extensionName + '".');

                }

            }

          }

        }

        var parser = new GLTFParser(json, extensions, {

          path: path || this.resourcePath || '',
          crossOrigin: this.crossOrigin,
          manager: this.manager

        });

        parser.parse(onLoad, onError);

      }

    };

    /* GLTFREGISTRY */

    function GLTFRegistry() {

      var objects = {};

      return {

        get: function (key) {

          return objects[key];

        },

        add: function (key, object) {

          objects[key] = object;

        },

        remove: function (key) {

          delete objects[key];

        },

        removeAll: function () {

          objects = {};

        }

      };

    }

    /*********************************/
    /********** EXTENSIONS ***********/
    /*********************************/

    var EXTENSIONS = {
      KHR_BINARY_GLTF: 'KHR_binary_glTF',
      KHR_DRACO_MESH_COMPRESSION: 'KHR_draco_mesh_compression',
      KHR_LIGHTS_PUNCTUAL: 'KHR_lights_punctual',
      KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: 'KHR_materials_pbrSpecularGlossiness',
      KHR_MATERIALS_UNLIT: 'KHR_materials_unlit',
      KHR_TEXTURE_TRANSFORM: 'KHR_texture_transform',
      MSFT_TEXTURE_DDS: 'MSFT_texture_dds'
    };

    /**
     * DDS Texture Extension
     *
     * Specification:
     * https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/MSFT_texture_dds
     *
     */
    function GLTFTextureDDSExtension(ddsLoader) {

      if (!ddsLoader) {

        throw new Error('THREE.GLTFLoader: Attempting to load .dds texture without importing THREE.DDSLoader');

      }

      this.name = EXTENSIONS.MSFT_TEXTURE_DDS;
      this.ddsLoader = ddsLoader;

    }

    /**
     * Lights Extension
     *
     * Specification: PENDING
     */
    function GLTFLightsExtension(json) {

      this.name = EXTENSIONS.KHR_LIGHTS_PUNCTUAL;

      var extension = (json.extensions && json.extensions[EXTENSIONS.KHR_LIGHTS_PUNCTUAL]) || {};
      this.lightDefs = extension.lights || [];

    }

    GLTFLightsExtension.prototype.loadLight = function (lightIndex) {

      var lightDef = this.lightDefs[lightIndex];
      var lightNode;

      var color = new THREE.Color(0xffffff);
      if (lightDef.color !== undefined) color.fromArray(lightDef.color);

      var range = lightDef.range !== undefined ? lightDef.range : 0;

      switch (lightDef.type) {

        case 'directional':
          lightNode = new THREE.DirectionalLight(color);
          lightNode.target.position.set(0, 0, - 1);
          lightNode.add(lightNode.target);
          break;

        case 'point':
          lightNode = new THREE.PointLight(color);
          lightNode.distance = range;
          break;

        case 'spot':
          lightNode = new THREE.SpotLight(color);
          lightNode.distance = range;
          // Handle spotlight properties.
          lightDef.spot = lightDef.spot || {};
          lightDef.spot.innerConeAngle = lightDef.spot.innerConeAngle !== undefined ? lightDef.spot.innerConeAngle : 0;
          lightDef.spot.outerConeAngle = lightDef.spot.outerConeAngle !== undefined ? lightDef.spot.outerConeAngle : Math.PI / 4.0;
          lightNode.angle = lightDef.spot.outerConeAngle;
          lightNode.penumbra = 1.0 - lightDef.spot.innerConeAngle / lightDef.spot.outerConeAngle;
          lightNode.target.position.set(0, 0, - 1);
          lightNode.add(lightNode.target);
          break;

        default:
          throw new Error('THREE.GLTFLoader: Unexpected light type, "' + lightDef.type + '".');

      }

      // Some lights (e.g. spot) default to a position other than the origin. Reset the position
      // here, because node-level parsing will only override position if explicitly specified.
      lightNode.position.set(0, 0, 0);

      lightNode.decay = 2;

      if (lightDef.intensity !== undefined) lightNode.intensity = lightDef.intensity;

      lightNode.name = lightDef.name || ('light_' + lightIndex);

      return Promise.resolve(lightNode);

    };




	return {
		OrbitControls,
		MapControls
	}
}

export default registerOrbit

三、效果图

在这里插入图片描述

四、总结

three.js画一个图形主要经历如下八个步骤:

  • 1.创建透视相机
  • 2.创建场景
  • 3.创建光源
  • 4.构造辅助网格
  • 5.创建加载器,加载模型文件
  • 6.创建渲染器,渲染场景
  • 7.创建控制器
  • 8.循环渲染场景
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。