本期论点: 元宇宙爆火的当下,webGL及其衍生技术将会如何参与共建元宇宙?
一. threejs 学习之路
1.threejs 基础
既然是模拟的现实世界,那必然有共通之处!
复制代码
- 我们存在于现实宇宙,Models存在于他们的世界,那就,从为他们初始化一个宇宙开始吧
import * as THREE from "three";
// 主场景
// 在vue里 非响应式变量可声明data之外,提高性能呢
const scene = new THREE.Scene();
复制代码
这里的 Scene 就相当于我们的三维世界,能包含一切事物,除了特殊的 时间 !
那怎么赋予我们创建的Models世界 时间维度呢? 那得看时间在宇宙中起了一个什么作用,才能在计算机中模仿不是吗.
时间静止会发生什么?(各位绅士请出门右转,这里没有你想要的)
假如意识也静止了 那么一切都永恒了. 假如意识依旧在活跃,想控制身体,但你还是动不了,除非时间主宰一个个普朗克时间的开放时间流动,这像什么?
渲染器!
渲染器是用来渲染出场景的,而场景包含光源,相机,模型,控制器等等,这一切构成了他们的世界 那就,初始化一个吧,让models可以一帧一帧的动起来
initRender() {
// 把你的渲染器渲染的场景挂载到哪一个dom上?
container = this.$refs.container;
// 创建渲染器 全局的renderer
// webGLRenderer 是最常用的渲染器 其他的还有基于css3D的css3DRenderer ,和基于canvas的canvasRenderer
renderer = new THREE.WebGLRenderer({
antialias: true, // 抗锯齿
precision: "highp", // 着色精度选择
logarithmicDepthBuffer: true, // 消除模型交错闪烁
preserveDrawingBuffer: true // 是否可以提取canvas 绘图的缓冲
// shadowMap: true, // 开启阴影渲染 大量计算
// alpha: true // 画布是否透明
});
// 设置场景显示区背景色
renderer.setClearColor(0x000000, 0);
// 场景显示区尺寸 (铺满)
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置像素比 如果移动端掉帧 去掉这个试试
renderer.setPixelRatio(window.devicePixelRatio);
// 可以如此设置样式 例如确保cssRender与render不遮盖
// renderer.domElement.style.position = "absolute";
// renderer.domElement.style.top = 0;
// renderer.domElement.style.zIndex = "1";
// 挂载dom
container.appendChild(renderer.domElement);
},
...
复制代码
"好黑啊,我们这是在哪里? 蔚" 金克斯摸着辫子疑惑的说
"站在光里的大人物,看不见身处黑暗的我们,咱们得去寻找光" 蔚拉住了金克斯的手
"这次... 这次我一定能帮上忙的!" 金克斯看向蔚的方向
"我一直相信你,我们一起 打破这黑夜!"
那就,为她们初始化一个光源吧
// 初始化灯光
initLight() {
//环境光 意思就是,为什么明明我在屋子里背光,按理说一片黑暗才对,但是还有光呢,环境反射的光
ambientLight = new THREE.AmbientLight(0xf1e2e2, 1.25);
// 加入世界里去 不加入不会渲染哦
scene.add(ambientLight);
//点光源 白色灯光,亮度0.6
pointLight = new THREE.PointLight(0xbfbfbf, 0.6);
//设置点光源位置,改变光源的位置
pointLight.position.set(0, 40, 80);
scene.add(pointLight);
// 还有聚光灯和平行光 用法同上
},
复制代码
峡谷动物园 "蔚! 你看你看,那个小猴子像不像我做的小猴子炸弹"
...
"蔚? 你为什么一直盯着天空看" 金克斯抬头看了看,好奇的问
零散的云彩,随风飘摇
"我有一种被操纵的感觉,很奇怪,很真实,就像是..玩偶?" 蔚沉思道
"不,你就是你,你就是 蔚" 金克斯抱住蔚的手说
蔚抱住了金克斯 "嗯 ! 我们永远是姐妹,金克斯的含义就是金克斯"
如何全方位监视? 毕竟我们是第三人称上帝视角 得有个金手指不是吗
那就,添加一个相机把
// 加载相机 与 相机控制
initCamera() {
// 透视相机 与此对应的 是平行视角相机 而 我们看世界就是远小近大的透视相机
// 具体参数详解搜一下吧 很简单
camera = new THREE.PerspectiveCamera(
30, // 视界 大部分是 30-90 比如游戏就可以调节视野大小
window.innerWidth / window.innerHeight, // 投影的宽高比
1, // 我的理解 : 距离相机多近就不渲染了?
100000 // 距离相机多远就不渲染了?
);
// 相机位置
camera.position.x = 697.1343985659603; // 是不是觉得这么多小数怎么出来的? 很吓人?
camera.position.y = 1784.0457888299613;// 后边调试那里会说到 如何快速开发
camera.position.z = 1566.095679557605; // 嗯
//相机以Y轴方向为上方(当值为1时即为上) 限制在y轴只能在正方向,比如加了地面元素之后,别跑到地下穿模了
camera.up.x = 0;
camera.up.y = 1;
camera.up.z = 0;
//相机看向哪个坐标 (原点)
camera.lookAt({
x: 0,
y: 0,
z: 0
});
//加载相机的鼠标操作 左键上下左右拖动 右键平移 滑轮缩放
// 引入 import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"; // 相机控制
controls = new OrbitControls(camera, renderer.domElement);
//设置相机初始焦点
controls.target = new THREE.Vector3(x, y, z); //(0,1,0);
//是否可以缩放
controls.enableZoom = true;
//是否自动旋转
controls.autoRotate = false;
//最大纵向旋转角度
controls.maxPolarAngle = Math.PI / 2;
//是否开启右键拖拽
controls.enablePan = true;
// 设置旋转速度
controls.rotateSpeed = 1;
// 使动画循环使用时阻尼或自传,意思是否有惯性
controls.enableDamping = true;
// 设置相机距离原点的最近距离
controls.minDistance = 1;
// 设置相机距离原点的最远距离
controls.maxDistance = 20000;
},
复制代码
看到这里,迷糊了吗,我们来捋一下 首先我们创建了 Scene,来存放世界元素,都有什么? 光源,相机,相机控制,和模型. 首先初始化一个Renderer,确保世界是能运动的,然后我们加入light,加入环境光让我们看清世界里的元素,加入点光源或其他光源,让模型有立体感,然后我们加入Camera 与 Controls ,确保我们能用鼠标全方位查看模型,其实到了这里,主要场景搭建完了,还差最关键的一步,把场景渲染出来!
介绍一个关键函数 requestAnimationFrame()
;
作用是让浏览器自动在最优时间内执行下一帧动画
那么我们渲染器输出的动画帧,就需要用这个函数递归调用来输出图像并顺便优化性能
startAnimate() {
// 更新相机位置
controls.update();
// 更新Tween 动画帧
if (TWEEN) {
TWEEN.update();
}
// 也可以不单独写
this.render();
// 存起来可用于清理循环动画 这很消耗资源
ranimationID = requestAnimationFrame(this.startAnimate);// 递归调用
},
render() {
// 调用render函数 渲染场景
renderer.render(scene, camera);
// 如果两个场景渲染器 当然也需要更新
if (rendererCSS) {
rendererCSS.render(cssScene, camera);
}
},
复制代码
到了这里 我们的画面应该是这样的
?
对 啥也没有 除了一个场景的背景色(0x000000)
那咋办? 你问我 我问谁啊 我当时可是急死了
不急,我们先加一个天空盒(什么是天空盒去百度一下)压压惊 上边的都是函数可以汇总在一个里
// 初始化3D 场景
init3dScene() {
// 创建一个组
this.whole = new THREE.Group();
this.yuanquGroup = new THREE.Group();
this.yuanquBuildGroup = new THREE.Group();
// 初始化场景渲染器
this.initRender();
// 初始化天空盒
this.initScene();
// 初始化相机与相机控制
this.initCamera();
// 加载灯光
this.initLight();
// 视角控制 待会说
this.animateVis();
// 启动渲染
this.startAnimate();
// 加载模型 也待会说
this.loadMainScene();
},
// 加载辅助场景
initScene() {
// 当你需要去更新你场景中对象矩阵的时候,会涉及到计算,如果只是静态对象,并且操作不频繁,你可以关闭matrixAutoUpdate参数,手动去更新矩阵
scene.matrixAutoUpdate = false;
// 辅助坐标系
var axes = new THREE.AxisHelper(20);
scene.add(axes);
// 设置背景
// 添加天空盒第一种方式
/** 我从官网扒的星空天空盒
skyBox: [
"skybox/dark-s_px.jpg",
"skybox/dark-s_nx.jpg",
"skybox/dark-s_py.jpg",
"skybox/dark-s_ny.jpg",
"skybox/dark-s_pz.jpg",
"skybox/dark-s_nz.jpg"
],*/
scene.background = new THREE.CubeTextureLoader().load(this.skyBox);
},
复制代码
成功了!
让我们看看效果!(有什么屏幕录制gif的神器求推荐)
2.threejs 学习资料
分享一些我的收藏
- Threejs 零基础入门
- Threejs 官网
- 体验一下极致的web3D 应用 yyds
- three.js 实现图片粒子爆炸特效
- three.js 动效方案
- Gltf 格式详解 很详细了属于是
- 嗯 一个很炫酷的3D 博客
- 使用Three.js实现3D楼盘展示
- 关于图片 文字 canvas 纹理的不错博客
- 一个在网页中嵌入3d模型的东东
3.threejs 调试
直接上结论,在谷歌浏览器里加一个
复制代码
然后在调试工具里就有啦,修改模型的位置,材质,之类的都会实时修改,舒服了,前文里提到的高精度的小数
其实就是打印的camera.position,当你需要某个位置的参数,在render()里打印一下好了,暂未发现更好的解决办法
复制代码
二. 3D 模型加载与 css3DRenderer
加载3d模型,首先要添加相应的 loader 文件,这些js文件,都在官网example演示文件夹里
这里就以 gltf 3d文件为例,演示一下3d模型加载
首先了解一下gltf格式
我的理解是: 通过一个json格式文件,描述贴图资源与3d模型对应位置信息的文件
里边的内容就是一些json描述的配置,而贴图资源可以内联为imag base64编码 也可以单独分出一个.bin文件存储这些编码,也可以引用图片文件
这是内联的
这是有bin文件和图片的
知道了gltf 文件格式,那我们怎么引入到场景中呢
好,直接上成品代码(这里用了本地的gltf资源文件,是内联格式的)
小二 ! 上酒 !
loader.js 文件
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader"; // .gltf 文件 loader
const loader = {
// 关于代码写法欢迎指教 我是前端1年小白
//加载GLTF文件 只加载模型,没有动画
loadGLTFOnly(objs, callback) {
let loader = new GLTFLoader();
let tempArr = [];
objs.forEach((one) => {
// 放在本地一样 找到gltf文件即可
loader.load('http://你家服务器地址/cdn/' + one.gltfUrl, function (gltf) {
let model = gltf.scene;
model.name = one.name;
console.log(model.name + ' done!');
// 返回每一个加载的model 加入到组中
callback(model);
});
});
}
}
export default loader;
复制代码
就这么简单? 对啊 毕竟前人都为我们写好loader和教程了 怎么用? 上边提到一个组的概念,其实就是比如你加载一个人体模型,分个组吧,把耳鼻口分到头组,把左右手分到手组,如果老板喜欢无面人,那你就把头组的耳鼻口 remove掉,如果老板说我喜欢刑天,那就把头组去掉,嘿嘿 我们返回的model就可以直接加到组里了,当然你要是想直接Scene.add(yourModels) 也不是不可以!
下面我们在原有的基础上加入两个模型,瞅瞅效果,是不是还是黑的?
增加一个initMainScene() 函数到你的init3dScene()中
// 加载主要场景
initMainScene() {
let elements = [];
elements = [
{
gltfUrl: "gltf/超大的地面场地.gltf",
name: "cdcd",
level: "园区"
},
{
gltfUrl: "gltf/周边的建筑.gltf",
name: "buildings",
level: "园区"
}
];
loader.loadGLTFOnly(elements, model => {
// 园区的模型放在这个组里
this.yuanquGroup.add(model);
});
// 全部模型 便于管理
this.whole.add(this.yuanquGroup);
// 不要漏了这一步
scene.add(this.whole);
},
复制代码
成功了! 不黑了!
我们成功加入了3D模型,并且让他们的世界有了光!
关于css3DRendeer 用到的并不会很多,最震撼的效果就是 官网的 元素周期表了
Thress js 3DRenderer 实现炫酷元素周期表
在项目中的使用 会与主场景 webGl renderer 发生一些不可描述的作用 而且 css3D 场景与 主场景 并不会产生模型的遮挡,css3D 模型一直在主场景模型之前,我还没解决这个问题 比如这样
就很难受 所以尽量只用一个渲染器 一个Scene 我这里怎么用的css3D?
其实就是把dom 节点,喂到 cssRenderer 嘴里,剩下的它全包了
然后拿到dom节点
let node = document.getElementById(elementId).cloneNode(true);
然后用 CSS3DSprite 或者 CSS3DObject 转化一下
cssObject = new CSS3DSprite(node); cssObject = new CSS3DObject(node);
把cssobject加入到cssScene中就行了,不详解了
三. 实现地球入场动画
这不得先上图?
复制代码
其实很简单,主要就是相机动画
animation.js
import { TWEEN } from "three/examples/jsm/libs/tween.module.min.js"; // 动画补帧器
import * as THREE from "three";
const animate = {
//相机移动实现漫游等动画
animateCamera(camera, controls, newP, newT, time = 2000, callBack) {
var tween = new TWEEN.Tween({
x1: camera.position.x, // 相机x
y1: camera.position.y, // 相机y
z1: camera.position.z, // 相机z
x2: controls.target.x, // 控制点的中心点x
y2: controls.target.y, // 控制点的中心点y
z2: controls.target.z, // 控制点的中心点z
});
tween.to(
{
x1: newP.x,
y1: newP.y,
z1: newP.z,
x2: newT.x,
y2: newT.y,
z2: newT.z,
},
time
);
tween.onUpdate(function (object) {
camera.position.x = object.x1;
camera.position.y = object.y1;
camera.position.z = object.z1;
controls.target.x = object.x2;
controls.target.y = object.y2;
controls.target.z = object.z2;
controls.update();
});
tween.onComplete(function () {
controls.enabled = true;
callBack();
});
tween.easing(TWEEN.Easing.Cubic.InOut);
tween.start();
},
}
export default animate;
复制代码
gif掉帧严重,原动画还是很流畅的.
关于实现
稍微麻烦点的就是得打印出来每个位置点,传进去分几段的动画终点
穿云动画用到一个组件 "image-sprite": "^1.0.0"
这里感谢一个开源项目 这个动画借鉴了很多 大佬
四. 实现园区管理雏形
其实这才是web3d的主打亮点吧
从智慧城市到智慧园区,包括虚拟博物馆和3D的产品展示,都能直接体现web3D的独特价值-真实直观
复制代码
1. 模型加载与场景视角切换
gltf文件导出时应该是可以设置位置信息的,加载之后有一个默认位置,因此园区的模型位置, 你可以手动修改position.x/y/z,最好的就是在建模导出时就确定好相对位置
加载图片资源可以用 resource-loader
一款通用的资源加载器
加载模型时的回调 可以用 manager = new THREE.LoadingManager();
管理
//资源加载控制
manager = new THREE.LoadingManager();
manager.onProgress = function (item, loaded, total) {
console.log(((loaded / total) * 100).toFixed(2) + "%");
};
...
// 加载器接收一个manager对象作为参数
let loader = new GLTFLoader(manager);
复制代码
关于场景切换 目前我的解决方案就是在不断add 或者 remove 组中的成员,相应的视图也会刷新
,有多种控制器供选择,找了个图
来源
我们常用的就是轨道控制器,可以实现基本拖拽缩放事件,其他控制器可以根据场景选择
2. 精灵模型
我们加载完模型之后,一般都需要在3D场景中展示一些信息,这就需要精灵模型了
复制代码
简而言之,就是一种一直朝向摄像头的模型,我们只能看见正面,看不见反面,在Three中有一套系统的解决方案
let texture = new THREE.TextureLoader().load(item.url);
let spriteMaterial = new THREE.SpriteMaterial({
map: texture, // 加载精灵材质的背景
opacity: 1
});
let sprite = new THREE.Sprite(spriteMaterial);// 创建精灵模型
// 设置基本信息 就ok了
sprite.scale.set(item.scale.x, item.scale.y, item.scale.z);
sprite.position.set(item.position.x, item.position.y, item.position.z);
sprite.name = item.name;
复制代码
也有css3DSprite 可以将css 转为3d 精灵模型 用法简单
3. canvas转纹理
最基本的包含文字图片的canvas转为纹理贴图,
然后从Echart图标转为纹理,
从视频<video>标签转为视频纹理,
将flv rtsp 直播或监控视频流转为视频纹理
复制代码
文字图片:
首先感谢一下 上文提到的 收藏第九条的作者大大 直接上代码
// 获取一个canvas 图文纹理
getTextCanvas(w, h, textArr, imgUrl) {
// canvas 宽高最好是2的倍数
let width = w;
let height = h;
// 创建一个canvas元素 获取上下文环境
let canvas = document.createElement('canvas');
let ctx = canvas.getContext('2d');
canvas.width = width;
canvas.height = height;
// 设置样式
ctx.textAlign = 'start';
ctx.fillStyle = 'rgba(44, 62, 80, 0.65)';
ctx.fillRect(0, 0, width, height);
//添加背景图片,进行异步,否则可能会过早渲染,导致空白
return new Promise((resolve, reject) => {
let img = new Image();
img.src = require('@/assets/images/' + imgUrl);
img.onload = () => {
//将画布处理为透明
ctx.clearRect(0, 0, width, height);
//绘画图片
ctx.drawImage(img, 0, 0, width, height);
ctx.textBaseline = 'middle';
// 由于文字需要自己排版 所以循环一下
// item 是自定义的文字对象 包含文字内容 字体大小颜色 位置信息等
textArr.forEach(item => {
ctx.font = item.font;
ctx.fillStyle = item.color;
ctx.fillText(item.text, item.x, item.y, 1024);
})
resolve(canvas)
};
//图片加载失败的方法
img.onerror = (e) => {
reject(e)
}
});
},
复制代码
主要就是对于图片加载的异步处理,保证渲染之前canvas加载完毕,函数可以直接食用哦
比如这样
复制代码
this.getTextCanvas(1024, 512, element.textArr, element.imgUrl).then((canvas) => {
let textMap = new THREE.CanvasTexture(canvas); // 关键一步
let textMaterial = new THREE.MeshLambertMaterial({ // 关于材质并未讲解 实操即可熟悉 这里是漫反射类似纸张的材质,对应的就有高光类似金属的材质.
map: textMap, // 设置纹理贴图
transparent: true,
side: THREE.DoubleSide,// 这里是双面渲染的意思
});
let planeGeometry = new THREE.PlaneBufferGeometry(planeWidth, planeHeight); // 平面3维几何体PlaneGeometry
let planeMesh = new THREE.Mesh(planeGeometry, textMaterial);
复制代码
结果是这样的
Echart图表:
不多说 上代码 soEasy
复制代码
// 获取一个 echart 图表 canvas
getEchart(w, h, option,) {
let canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
return new Promise((resolve, reject) => {
let myChart = echarts.init(canvas, 'dark');
try {
// 很显然 用过Echart的都知道这个option是啥 不多说
myChart.setOption(option, false);
myChart.on(
'finished',
() => {
resolve(canvas);
}
)
} catch (e) {
reject(e)
}
});
},
复制代码
结果图:
我知道你们想问什么!
确实没法交互,因为已经转成纹理贴图了,如果有大佬知道如何交互,希望不吝赐教
视频:
其实更简单,但是我测试的时候 有巨坑 帮你们排雷了
坑1: video 标签的muted问题,muted让video静音播放可以解决谷歌浏览器设置了 autoplay 却不自动播放的问题
解释1: 经过一番探sou究suo,感觉是浏览器的安全策略,默认禁止网页获取音频权限,
需要申请,就是左上角弹个小框是否允许网页播放音频之类的,所以导致autopay不管用了
坑2: 因为我这个之全屏幕web3D ,其他2D 组件都飘在主场景canvas上,
所以我第一次测试Video为了美感把他visible隐藏了,结果打死也渲染不出来视频纹理!
最后发现video组件只有在视窗内, 或者设置了z-index = -1,隐藏在组件下,才能播放...才能实时渲染出视 频纹理
解释2:未知
复制代码
// 获取一个视频纹理
getVideoTexture(id) {
// video 标签的ID
let video = document.getElementById(id);
let _texture = new THREE.VideoTexture(video);
_texture.minFilter = THREE.LinearFilter;
_texture.wrapS = _texture.wrapT = THREE.ClampToEdgeWrapping;
_texture.format = THREE.RGBFormat;
return _texture;
},
复制代码
结果图
直播流:
关于直播流吧,rtmp流 依赖于flash,我在谷歌上没有实现,目前测试的一个flv地址,可以跑通,用的是flv.js
复制代码
具体使用方法,参考flvjs教程
if (flvjs.isSupported()) {
var videoElement = document.getElementById("videoElement");
var flvPlayer = flvjs.createPlayer(
{
type: "flv",
isLive: true,
hasAudio: true,
hasVideo: true,
url: this.url
},
{
autoCleanupSourceBuffer: true,
enableWorker: true,
enableStashBuffer: false,
stashInitialSize: 128
}
);
flvPlayer.attachMediaElement(videoElement);
flvPlayer.load();
flvPlayer.play();
}
复制代码
其实原理 依旧是依赖于Video 标签 ,只不过是利用flv.js 将直播流实时的用video 标签播放出来,然后后面的就是转视频纹理的流程了
五. 优化与展望
最近看了ThingJs 官网的实例开发项目,发现原来web3D 还有这么多玩法,自己还需努力学习,但是我感觉web3D 性能依旧不足以支持 什么智慧城市 ,而且大部分数据展示,也只是拿3D模型当背景板,并没有结合的很好,感觉web3D 是不是缺少一个很实在的落地点呢,个人想法,会不会与元宇宙,碰撞出火花. 最后,都看到这儿了,若是觉得文章尚可,求一个免费的赞喽...