【愚公系列】2022年09月 微信小程序-three.js加载3D模型
【摘要】 @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)