本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!javascript
事情是这样的,前段时间外包工头
老杨又来找我了,说某汽车大品牌要开发一个网页展厅,但愿能够在网页里360度展现它家新款汽车的3d模型,还要可让用户DIY汽车部件的颜色。html
可能不少朋友看完此文后会以为两周时间还挺充裕,但其实不是,做为丙方没什么话语权,常常要配合甲方反复修改,不少时候改来改去最后拖到上线前一晚没办法了直接上,一个campaign site的生命周期也不长,最长也就在线上待1-3个月。前端
嘿嘿,时间紧,预算多!java
我心想报价四个W,再给他留点砍价空间,后端
谁知道老杨一口答应,还说完事要请我去XX人间api
我猜他起码要从客户那赚10个W微信
先看最终效果,大家以为值四个W吗?markdown
也就是以前的文章《三种前端实现VR全景看房的方案!说不定哪天就用得上!》里提到的用
threejs
来实现的app
本文的目标是让你们看完以后能够马上上手用起来,既然要用3d引擎,那咱们理解了一些3d的基本知识后,再看threejs
的API文档效率就会很高。不管什么3d引擎,都不外乎由如下几种基本元素构成dom
scene
)一个容器,容纳着除渲染器之外的三维世界里的一切。场景的元素采用右手笛卡尔坐标系,x轴正方向向右,y轴正方向向上,z轴由屏幕从里向外
camera
)就像人的眼睛,在一个空间里能够看向任意方向,能够经过参数调节可视角度和可视距离。
通常咱们使用符合物理世界近大远小真实状况的透视相机PerspectiveCamera
,还有一些特殊状况,须要远近大小是同样的,那就要用正交相机OrthographicCamera
PerspectiveCamera( fov : Number, aspect : Number, near : Number, far : Number )
//构造函数参数
//fov:视场角
//aspect:视场宽高比(通常用 画布宽/画布 高便可)
//near:能看多近
//far:能看多远
//这几个参数决定了哪些scene里的三维顶点会被渲染/绘制出来
复制代码
renderer
)将
camera
在scene
里看到的内容渲染/绘制到画布上
geometry
)3D世界里的全部物体都是
点组成面
,面组成几何体
。相信你们对如下标准的几何体比较熟悉
面
是由点构成的,面
又能够组成各式各样的几何体。以球体举例,球体面上的点越多,球就越圆。但点越多,运算量也会越大...
另外咱们通常说的3d模型
就是一个或多个几何体,只是有的3d模型文件里除了包含几何体还能够包含一些额外的信息,好比贴图,材质等...须要在读取模型文件时解析出来
light
)3d引擎在没有手动建立光的状况下会默认有个
环境光
,否则你什么都看不到。常见的灯光有如下几种类型
texture
)想象一下你手里有一个立方体,你用一张A4纸包裹上立方体的全部面,并在上面画画。你画的内容就是
贴图
。
有一些类型的贴图会和光照发生反应...后面咱们用到的时候再说
material
)延续贴图里的想象,你用白卡纸画画,仍是用油纸画画,呈现出来的质感是不一样的对不对,这就是
材质
!下面五个球的颜色都是同样的,而材质从左至右分别是
有了这些基础知识,再来使用threejs就很容易上手了。能够说在3dmax等软件中调出来的90%的效果,用threejs都能找到对应的配置参数。
//<div id='container' style="width:100%;height: 100%;"></div>
var scene, camera, renderer;
function init(){
scene = new THREE.Scene();
//这里参数不懂的同窗回去看基本知识里的camera部分
camera = new THREE.PerspectiveCamera(90, document.body.clientWidth / document.body.clientHeight, 0.1, 100);
//camera的位置在x0,y0,z3,还记得迪尔卡右手坐标系吗?
camera.position.set(0, 0, 3);
renderer = new THREE.WebGLRenderer();
renderer.setSize(document.body.clientWidth, document.body.clientHeight);
document.getElementById("container").appendChild(renderer.domElement);
var controls = new THREE.OrbitControls(camera, renderer.domElement);
//等待添加模型
loop();
}
function loop() {
requestAnimationFrame(loop);
renderer.render(scene, camera);
}
window.onload = init;
复制代码
如今咱们能够先添加一个标准几何体来试试看,好比咱们添加一个立方体来试试看
const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const material = new THREE.MeshBasicMaterial( {color: 0x00ff00} );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );
复制代码
很显然,场景是生效的...你们注意看源码块中的注释
回到我们的项目上来,品牌方给的是一个很是精细的模型,文件量有好几百兆,数百万面(triangles
)。
我说这可用不了,你得减面
还得给我转成引擎能支持的格式gltf
或obj
根据个人评估,要想在移动端网页里流畅运行,最多不能超过10万面
外包工头老杨说,你也别让客户给你弄了,他们都不会
我知道你懂,你就给弄了算了,我给你加【5K】
加5K你让我怎么好意思拒绝呢...
而后,我花25美刀
巨资在sketchfab
上购买了一个模型
再稍微改改就能知足要求,固然sketchfab也有免费模型
但毕竟收了老杨5K,不花点钱我内心略感不安
呐 :p
根据实际的需求,好比车窗要透明能够看到内饰
,因此车窗就得单独给有透明属性的材质。车轮,灯罩,车网,车架,车身等等都要拆成独立的几何体
才能独立配置材质
。
梳理好模型结构后,咱们就要准备模型文件了
3d模型的文件格式有不少,但threejs
里经常使用的基本是
老牌通用3d模型文件,不包含贴图,材质,动画等信息。
由OpenGL官方维护团队推出的现代3d模型通用格式,能够包含几何体、材质、动画及场景、摄影机等信息,而且文件量还小。有3D模型界的JPEG之称。
原项目中我使用的是OBJ格式,本文里咱们使用GLTF格式。利用threejs提供的editor,咱们能够将模型的格式进行转换并导出。
经过GLTFLoader,咱们能够加载一个.gltf
格式的3d模型文件。须要注意的是,这些Loader都以插件的形式存在,须要引入相应的XXXLoader.js
才能使用
//<script src="js/GLTFLoader.js"></script>
//放到以前添加立方体的代码处
const loader = new THREE.GLTFLoader();
loader.load(
'images/model.gltf',
function ( gltf ) {
scene.add( gltf.scene );
},
function ( xhr ) {
//侦听模型加载进度
console.log( ( xhr.loaded / xhr.total * 100 ) + '% loaded' );
},
function ( error ) {
//加载出错时的回调
console.log( 'An error happened' );
}
);
复制代码
经过这个代码能够遍历查看模型里的几何体列表
console.log(gltf.scene.children);
//能够用for,也能够用traverse api
//gltf.scene.children.traverse((child){});
复制代码
如今咱们来给几何体添加贴图,贴图怎么作是设计师的专业。这里不过多的说,咱们只须要知道,这些贴图如何使用便可。
_col
)material.map,替代颜色
_nor
)material.normalMap,让细节程度较低的表面生成高细节程度的精确光照方向和反射效果
_occ
)material.aoMap,用来描绘物体和物体相交或靠近的时候遮挡周围漫反射光线的效果
material.envMap,用于模拟材质反射周围环境的效果
咱们如今先把这些贴图文件统一加载到内存里
var allTexture;
function loadAllTexture(cb){
allTexture = {};
var loadIndex = 0;
var textures = [
"skymap",
"shache_occ",
"shache_nor",
"shache_col",
"neishi_occ",
"neishi_nor",
"mennei_col",
"luntai_nor",
"luntai_col",
"lungu_occ",
"lungu_nor",
"lungu_col",
"linjian_occ",
"linjian_nor",
"linjian_col",
"floor",
"deng_occ",
"deng_nor",
"deng_col",
"cheshen_occ",
"cheshen_nor",
"chejia_occ",
"chejia_nor",
"chedengzhao_nor"
];
function loadNextTexture(){
var textureName = textures[loadIndex];
loadTexture("images/textures/"+textureName+".jpg",function(texture){
if(loadIndex<textures.length-1){
allTexture[textureName] = {
texture:texture
};
loadIndex++;
loadNextTexture();
}else{
if(cb)cb();
}
});
}
loadNextTexture();
}
function loadTexture(filepath,cb){
const textureLoader = new THREE.TextureLoader();
textureLoader.load(filepath,cb);
}
复制代码
而后根据名称手动一一对应,好比咱们先把车轮毂的贴图给加上
for(var i=0;i<gltf.scene.children[0].children.length;i++){
var modelObj = gltf.scene.children[0].children[i];
if(modelObj.name=="smart_lungu0"||modelObj.name=="smart_lungu1"||modelObj.name=="smart_lungu2"||modelObj.name=="smart_lungu3"){
modelObj.material = new THREE.MeshStandardMaterial();
modelObj.material.map = allTexture["lungu_col"].texture;
modelObj.material.normalMap = allTexture["lungu_nor"].texture;
modelObj.material.aoMap = allTexture["lungu_occ"].texture;
}
}
复制代码
咱们继续把车轮的贴图给加上
else if(modelObj.name=="smart_chelun0"||modelObj.name=="smart_chelun1"||modelObj.name=="smart_chelun2"||modelObj.name=="smart_chelun3"){
modelObj.material = new THREE.MeshStandardMaterial();
modelObj.material.map = allTexture["luntai_col"].texture;
modelObj.material.normalMap = allTexture["luntai_nor"].texture;
}
复制代码
其他的材质贴图都如此添加上,后续固然还有不少材质的细节是能够去调整的,但这是个细活儿,这里主要重点分享下玻璃的反射和透明
,金属漆的反光
天窗和前挡风玻璃的透明度以及基底颜色是不一样的
else if(child.name=="smart_boli"){
child.material=new THREE.MeshPhongMaterial();
child.material.color = new THREE.Color( 0x333333 );
child.material.transparent=true;
child.material.opacity=.2;
}else if(child.name=="smart_tianchuang"){
child.material=new THREE.MeshPhongMaterial();
child.material.color = new THREE.Color( 0x000 );
child.material.transparent=true;
child.material.opacity=.5;
}
复制代码
仔细看看动图里前挡风和天窗透明度的差别
想真的去反射真实的环境?你别想多了,用envMap作个假的看起来就很能够了...
child.material.envMap=allTexture["skymap"].texture;
//环境反射贴图envMap的映射方式,这里用的是一个叫等量矩形投影的映射方法
child.material.envMap.mapping = THREE.EquirectangularReflectionMapping;
//环境反射贴图的强度
child.material.envMapIntensity=1;
复制代码
仔细看动图里的前挡风玻璃,是否是反射了什么东西?看过《三种前端实现VR全景看房的方案!说不定哪天就用得上!》的小伙伴们,记得这张图么?
使用
MeshStandardMaterial
材质,经过调节metalness
,roughness
的值来调节金属的质感
child.material = new THREE.MeshStandardMaterial();
child.material.color=new THREE.Color(0x70631B);
child.material.metalness = 0.44;
child.material.roughness = 0;
复制代码
毕竟是个在线展厅,在车身周围得呈现一些信息点,点击后能够弹窗显示更多信息对吧。实现方式一样在VR全景的文章中提到过了,就是
Sprite
+Raycast
//frame只是一个标记,叫什么都行
var poiPosArray=[
{x:-1.47,y:0.87,z:-0.36,frame:1},
{x:-1.46,y:0.49,z:-0.69,frame:2},
{x:1.5,y:.7,z:0,frame:8},
{x:0.33,y:1.79,z:0,frame:3},
{x:0,y:0.23,z:0.96,frame:4},
{x:0.73,y:1.38,z:-0.8,frame:5},
{x:-.1,y:1.17,z:0.88,frame:6},
{x:-1.16,y:0.16,z:0.89,frame:7}
],poiObjects=[];
function setupInfoPoint(){
const pointTexture = new THREE.TextureLoader().load("images/point.png");
var group = new THREE.Group();
var materialC = new THREE.SpriteMaterial( { map: pointTexture, color: 0xffffff, fog: false } );
for ( var a = 0; a < poiPosArray.length; a ++ ) {
var x = poiPosArray[a].x;
var y = poiPosArray[a].y-.5;
var z = poiPosArray[a].z;
var sprite = new THREE.Sprite( materialC );
sprite.scale.set( .15, .15, 1 );
sprite.position.set( x, y, z );
sprite.idstr="popup_"+poiPosArray[a].frame;
group.add( sprite );
poiObjects.push(sprite);
}
scene.add( group );
document.body.addEventListener("click",function (event) {
event.preventDefault();
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
raycaster.setFromCamera( mouse, camera );
var intersects = raycaster.intersectObjects( poiObjects );
if(intersects.length>0){
var popIndex=parseInt(intersects[ 0 ].object.idstr.substr(6,1));
console.log(popIndex);
}
});
}
复制代码
既然咱们用了threejs
,因此咱们就要在threejs
里把UI作出来吗?这么想的话,会把本身累死。要知道在3d场景里作2d的UI可不算是一件容易的事,还要实现UI的一些用户行为(点击,拖动等)的话就更麻烦了...因此咱们直接用html
来作UI就好啦~
到这里,这个3D汽车展厅的核心部分你已经学会(fei)了吧!
以上只是对threejs
一个很是粗浅的使用,threejs
能实现的酷炫效果远远不止于此,但愿本文能让你开始对Web3D开发产生兴趣,若是以为本文还不错,请点赞收藏关注吧~
BTW:明明是个笨驰
的smart,怎么说是BMW呢?由于本故事纯属虚构撒,请勿对号入座。本文中全部聊天记录均为使用微信对话生成器伪造。
微信搜索并关注公众号“大帅老猿
”,回复“smart3d
”得到本文所有源码