3D场景必备:scene, renderer, light, camera, model
一个基本代码:
<script src="https://cdn.bootcdn.net/ajax/libs/three.js/r127/three.min.js"></script>var scene = new THREE.Scene();var camera = new THREE.PerspectiveCamera(75,window.innerWidth/window.innerHeight,0.1,1000);camera.position.set(0,0,50);var aLight = new THREE.AmbientLight(0xffffff,1);scene.add(aLight);var renderer = new THREE.WebGLRenderer({ });renderer.setSize(window.innerWidth,window.innerHeight);renderer.setSize(window.innerWidth,window.innerHeight);renderer.outputEncoding = THREE.sRGBEncoding;// 编码模式document.body.appendChild(renderer.domElement);renderer.render(scene,camera);
场景Scene
设置背景颜色背景图:
scene.background = new THREE.Color("#88B9DD");
scene.background = textureLoader.load();
设置背景透明:
var renderer = new THREE.WebGLRenderer( { alpha: true } );
renderer.setClearAlpha(0);
渲染器:
WebGLRenderer
属性:
灯光:
常用类型:
DirectionalLight 方向光
var dLight = new THREE.DirectionalLight(0x888888,1);
dLight.position.set(2,7,0);
dLight.target = box1;
scene.add(dLight)
AmbientLight 环境光
var light = new THREE.AmbientLight( 0x404040, 1.0 ); // soft white light
scene.add( light );
PointLight 点光源
var pLight = new THREE.PointLight(0xfff33f,1);
pLight.position.set(0,7,0);
scene.add(pLight);
相机:
PerspectiveCamera 远景相机
var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000 );
视角fov 宽高比aspect 近裁剪面near 远裁剪面 far
OrthographicCamera 正交相机
let width = window.innerWidth, height = window.innerHeight;
camera = new THREE.OrthographicCamera(-width/2, width/2, height/2, -height/2, 1, 1000);
left(-width/2) right(width/2) top(height/2) bottom(-height/2) near(1) far(1000)
常用于工程图等无需近大远小的项目
模型加载:
3D模型加载器: GLTFLoader, FBXLoader,
纹理加载器:TextureLoader
const gltfLoader = new GLTFLoader();gltfLoader.load(modelUrl,function(gltf){const model = gltf.scene;model.position.set(0,-30,150);model.scale.set(300,300,300);model.rotation.set(0.3,0,0);scene.add(model);});
自己创建模型:
创建3D模型
var textureLoader = new THREE.TextureLoader();
var boxGeo = new THREE.BoxGeometry(1,1,1);
var texture = textureLoader.load('./imgs/1.jpg');
var mat = new THREE.MeshLambertMaterial({map:texture,side: THREE.DoubleSide});
var box = new THREE.Mesh(boxGeo,mat);
scene.add(box);
Box的不同面设置为不同的材质纹理:
var matArr = [mat1,mat2,mat1,mat2,mat1,mat2];
var mesh = new THREE.Mesh(boxGeo,matArr);
自定义材质索引
var matArr2 = [mat1,mat2];
boxGeo.groups[3].materialIndex = 0;
boxGeo.groups[4].materialIndex = 0;
boxGeo.groups[5].materialIndex = 0;
var mesh = new THREE.Mesh(boxGeo,matArr2);
网格Mesh
获取某个子元素:
getObjectByName, getObjectById,
常用属性:
children, position, scale, rotation(quaternion), name, up, userData
常用方法:
traverse,.traverseVisible,lookAt,.add,.remove,.clone,.getWorldPosition,.getWorldRotation,.getWorldScale
显示与隐藏Layer
材质Material-类型:
基本材质
var matBasic = new THREE.MeshBasicMaterial({color:0xeeff00,wireframe:false});// 不受光照影响,就算没有光也可以显示出来
兰伯特材质
var matLambert = new THREE.MeshLambertMaterial({color:0xeeff00,wireframe:false});// 此材质必须有环境光才能显示出来,只有漫反射
高光材质
var matPhong = new THREE.MeshPhongMaterial({// 此材质必须有光才能显示出来,只有镜面反射color:0xeeff00,wireframe:false,specular:0x11ffee,shininess:10});// 高光材质 specular高光颜色 shininess 光照强度系数
精灵材质
var spriteMaterial = new THREE.SpriteMaterial({map: texture //设置精灵纹理贴图});
点材质
const pointMat = new THREE.PointsMaterial({// color: 0xff0000,// 使用顶点颜色数据渲染模型,不需要再定义color属性// 属性.vertexColors的默认值是THREE.NoColors,也就是说模型的颜色渲染效果取决于材质属性.color,// 如果把材质属性.vertexColors的值设置为THREE.VertexColors,渲染模型时就会使用几何体的顶点颜色数据geometry.attributes.colorvertexColors: THREE.VertexColors, // 以顶点颜色为准 size: 0.2});
标准材质:
const material = new THREE.MeshStandardMaterial({color: "#ffff00",map: doorColorTexture,// 色彩贴图alphaMap: doorAplhaTexture,// 透明度贴图,0(黑色)代表完全透明,1(白色)代表完全不透明,0.5(灰色)代表半透明transparent: true, aoMap: doorAoTexture,// 环境遮挡贴图,使纹理对光的穿透效果不同aoMapIntensity: 1,// 遮挡强度,该值乘以贴图以调整效果displacementMap: doorHeightTexture,// 置换贴图,使顶点位置发生位移(设置同时需要把顶点数量segment设置多一些)displacementScale: 0.1,// 该值乘以贴图以调整位移距离效果 未设置map似乎无效roughness: 1,// 粗糙度设置,1代表完全粗糙,0代表完全光滑 如果同时设置map会乘以map的值以调整map的显示效果roughnessMap: roughnessTexture,// 粗糙度贴图 1(白色)代表完全粗糙,0(黑色)代表完全光滑,0.5(灰色)代表半粗糙半光滑metalness: 1,// 金属度设置 1代表最强 0 代表最弱,0.5代表中间程度 如果同时设置map会乘以贴图以调整金属度效果metalnessMap: metalnessTexture,// 金属度贴图 normalMap: normalTexture,// 法线向量(三个数)对应一个色彩值(三个数),用色彩值形成图片代表法线向量 使光线照射上去凹凸位置看起来不同(光线折射方向不同) 不设置将导致金属部分看起来无凹凸感opacity: 0.3,side: THREE.DoubleSide,});
常用属性map,color,side
纹理贴图
var textureLoader = new THREE.TextureLoader();
textureLoader.load('./imgs/1.jpg',function(texture){// 异步,var mat = new THREE.MeshLambertMaterial({map:texture,side: THREE.DoubleSide});var box = new THREE.Mesh(boxGeo,mat);scene.add(box);});
属性:
// 纹理贴图重复模式 默认ClampToEdgeWrapping 重复排列:RepeatWrapping 镜像重复排列(重复部分呈现镜像纹理):MirroredRepeatWrappingtexture1.wrapS = THREE.RepeatWrapping;texture1.wrapT = THREE.RepeatWrapping;// uv两个方向纹理重复数量texture1.repeat.set(1,2);// 纹理偏移设置// texture1.offset = new THREE.Vector2(0.2,0);texture1.offset.set(0.2,0);texture1.rotation = Math.PI / 4;// 纹理旋转texture1.center.set(0.5,0.5);// 纹理旋转中心(默认0,0)// texture纹理显示设置 当纹理像素不足以覆盖模型的时候,怎么渲染(使用最近的像素值--会显示方块形像素点/线性渲染--会模糊)texture.minFilter = THREE.NearestFilter;texture.minFilter = THREE.LinearFilter;// texture纹理显示设置 当纹理像素数量多于覆盖像素点的时候,怎么渲染texture.magFilter = THREE.NearestFilter;texture.magFilter = THREE.LinearFilter;
cube纹理(了解)
const cubeTextureLoader = new THREE.CubeTextureLoader();
const envMap = cubeTextureLoader.load(['texture/pisa/px.png','texture/pisa/nx.png','texture/pisa/py.png','texture/pisa/ny.png','texture/pisa/pz.png','texture/pisa/nz.png',]);scene.background = envMap;scene.environment = envMap;
hdr纹理(了解)
const rgbeLoader = new THREE.RGBELoader();
rgbeLoader.loadAsync('texture/yuanlin.hdr').then(texture => {texture.mapping = THREE.EquirectangularReflectionMapping;scene.background = texture;scene.environment = texture;
});
形状Geometry
类型:BufferGeometry,BoxGeometry,TextGeometry,PlaneGeometry等
var bufferGeo = new THREE.BufferGeometry();
const positions = new Float32Array([0, 0, 0, //顶点1坐标0.5, 0, 0, //顶点2坐标0, 1, 0, //顶点3坐标0, 0, 0, //顶点4坐标0, 0, 1, //顶点5坐标0.5, 0, 0, //顶点6坐标]);
const colors = new Float32Array([1, 0, 0, //顶点1颜色0, 1, 0, //顶点2颜色0, 0, 1, //顶点3颜色1, 1, 0, //顶点4颜色0, 1, 1, //顶点5颜色1, 0, 1, //顶点6颜色]);
// 物体有漫反射、镜面反射,太阳光照在一个物体表面,物体表面与光线夹角位置不同的区域明暗程度不同
// WebGL中为了计算光线与物体表面入射角,首先要计算物体表面每个位置的法线方向,
// 没有法向量数据,点光源、平行光等带有方向性的光源不会起作用(物体无法参与光照计算)
// 两个三角形表面法线不同,即使光线方向相同,明暗依然不同,在分界位置将有棱角感。
// 顶点法向量数据和顶点位置数据、顶点颜色数据都是一一对应的。
const normals = new Float32Array([0, 0, 1, //顶点1法向量0, 0, 1, //顶点2法向量0, 0, 1, //顶点3法向量0, 1, 0, //顶点4法向量0, 1, 0, //顶点5法向量0, 1, 0, //顶点6法向量
])
// 创建顶点索引数组的时候,可以根据顶点的数量选择类型数组Uint8Array、Uint16Array、Uint32Array。
// 对于顶点索引而言选择整型类型数组,对于非索引的顶点数据,需要使用浮点类型数组Float32Array等
const indexes = new Uint16Array([// 用于解决重复顶点的问题,重复顶点不需要重复设置位置和法线数据,只需要用索引指向对应的数据即可// 0对应第1个顶点位置数据、第1个顶点法向量数据// 1对应第2个顶点位置数据、第2个顶点法向量数据// 索引值3个为一组,表示一个三角形的3个顶点0, 1, 2,0, 2, 3,])
bufferGeo.setAttribute("position", new THREE.BufferAttribute(positions, 3));
bufferGeo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
bufferGeo.setAttribute('normal', new THREE.BufferAttribute(normals,3));
bufferGeo.setAttribute('index', new THREE.BufferAttribute(indexes,1));
const points = [];
points.push( new THREE.Vector3( - 1, 0, 0 ) );
points.push( new THREE.Vector3( 0, 1, 0 ) );
points.push( new THREE.Vector3( 1, 0, 0 ) );
const bufferGeo2 = new THREE.BufferGeometry().setFromPoints( points );// 绑定顶点到空几何体
const rectHalfWidth = 0.5,rectHalfHeight = 1;
const rectShape = new THREE.Shape();
rectShape.moveTo(-rectHalfWidth,rectHalfHeight);
rectShape.lineTo(rectHalfWidth,rectHalfHeight);
rectShape.lineTo(rectHalfWidth,-rectHalfHeight);
rectShape.lineTo(-rectHalfWidth,-rectHalfHeight);
rectShape.lineTo(-rectHalfWidth,rectHalfHeight);
const shapeGeo = new THREE.ShapeGeometry(rectShape);
组Group
var tags = new THREE.Group();
精灵:
var spriteMaterial = new THREE.SpriteMaterial({map: texture //设置精灵纹理贴图});
var sprite = new THREE.Sprite(spriteMaterial);
sprite.scale.set(textureScale[0] * 1.5, textureScale[1] * 1.5, textureScale[2] * 1.5);
sprite.position.set(pos.x, 12, pos.z);
sprite.name = name;
model.add(sprite)
模型变换
<script src="js/OrbitControls.js"></script>
var controls = new THREE.OrbitControls(camera)
controls.enableZoom = true//controls.autoRotate = true;controls.minDistance = 10;controls.maxDistance = 300;controls.maxPolarAngle = 1.5;controls.minPolarAngle = 1.5;controls.enablePan = false;animate()function animate(){controls.update()requestAnimationFrame(animate);renderer.render(scene,camera);}
数学工具
向量:
三维向量Vector3,Vector4,Color
类似于js {x: 0, y: 0, z: 0}
方法:
setX,setY,setZ,copy,add,sub,multiplyScalar,divideScalar,dot,normalize,floor,ceil,round,roundToZero,addScalar,divide,min,max,multiply,toArray
欧拉角Euler:
方法:
set,copy,clone,equals
鼠标拾取:Raycaster
let mousePosition = new THREE.Vector2();
mousePosition.x = (touch.x / window.innerWidth) * 2 - 1;
mousePosition.y = -(touch.y / window.innerHeight) * 2 + 1;
var raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mousePosition, camera);
let intersects = raycaster.intersectObjects(scene.children, true);
if (intersects.length > 0) {
}
播放动画Amimation
const clock = new THREE.Clock;const mixer = new THREE.AnimationMixer(fbx);
const action = mixer.clipAction(fbx.animations[0]);
action.play();
timer = setInterval(() => {mixer.update(clock.getDelta());
}, 10);
模型规范与案例讲解:
gltf-UnityTestUtil
<!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>gltf模型导出规范工具</title><style>*{padding: 0;margin: 0;overflow: hidden;}.info{position: fixed;left: 0;top: 0;}.tip{color: orange;}</style>
</head>
<body><div class="info"><h3>使用说明:</h3><ol><li> 打开 <a href="https://blog.csdn.net/qq_34568700/article/details/107139489" target="_blank">链接</a>这个网址根据文章设置浏览器跨域(如已设置请忽略)</li><li>将3D模型放入gltf文件夹</li><li>在当前网址拼接“?name=文件名”,例如:gltfTest.html?name=yu</li><li>鼠标拖动可以移动模型,用于检查移动灵敏度和坐标轴方向;虚拟摇杆旋转3D模型来检查模型的旋转中心点是否正确等问题;鼠标滚轮缩放模型检查缩放速度</li></ol><h3>模型规范检查清单</h3><ul><li>文件夹层级:name/name.gltf </li><li>文件(上述中的name值)命名: 全英文,描述这个3D模型的名字 ,<span class="tip">禁止出现中文,空格,特殊字符</span></li><li>文件导出类型:gltf</li><li>模型初始位置:位于原点(参考红绿线条原点位置)</li><li>模型初始大小:统一单位(cm)大小:可完整显示在浏览器中的合理大小</li><li>模型初始角度:便于观察模型特征的合理角度(所有鱼头方向保持统一方向)</li><li>模型旋转中心点:旋转中心点位置为模型重心位置;</li></ul></div><script src="./js/three.js"></script><script src="./js/loaders/GLTFLoader.js"></script><script>(function(){var winWidth = window.innerWidth,winHeight = window.innerHeight;var name = GetQueryString('name');var dirImpulse = [], currModel;var mOnDown = false,mLastPosition = new THREE.Vector2();var scene = new THREE.Scene();var camera = new THREE.OrthographicCamera(winWidth / -2, winWidth / 2, winHeight / 2, winHeight/-2, 0.01,500);camera.position.z = 200;camera.lookAt(new THREE.Vector3(0,0,0))scene.add(new THREE.AmbientLight(0xffffff, 1));var gltfLoader = new GLTFLoader();gltfLoader.load(`./gltf/${name}/${name}.gltf`, (obj) => {let m = obj.scene;scene.add(m);currModel = m;let clock = new THREE.Clock();let mixer = new THREE.AnimationMixer(m); // 创建混合器let AnimationAction = mixer.clipAction(obj.animations[0]); // 返回动画操作对象AnimationAction.timeScale = 1.5;AnimationAction.play();setInterval(() => {mixer.update(clock.getDelta());}, 10);});var axesHelper = new THREE.AxesHelper( 300 );scene.add( axesHelper ); var renderer = new THREE.WebGLRenderer({antialias: true,alpha: true,// preserveDrawingBuffer: true});renderer.setSize(winWidth,winHeight);// 不在这里设置,而在css里设置模型将变的模糊renderer.outputEncoding = THREE.sRGBEncoding;document.body.appendChild(renderer.domElement);ani()function ani(){renderer.render(scene,camera);requestAnimationFrame(ani);let [x,y] = dirImpulse;if (Math.abs(x) > Math.abs(y)) {if (x > 0) {// rightcurrModel.rotation.y -= 0.04;} else if (x < 0) {// leftcurrModel.rotation.y += 0.04;}} else {if (y > 0) {// bottomcurrModel.rotation.x += 0.04;} else if (y < 0) {// topcurrModel.rotation.x -= 0.04;}}}document.body.addEventListener('mousedown', mouseDown, false);document.body.addEventListener('mousemove', mouseMove, false);document.body.addEventListener('mouseup', mouseUp, false);window.onmousewheel = document.onmousewheel = wheel;function mouseDown(e) {mOnDown = true;mLastPosition.set(e.pageX , e.pageY);}function mouseMove(e){if (mOnDown) {let currX = e.pageX || e.touches[0].pageX;let currY = e.pageY || e.touches[0].pageY;let deltaX = currX - mLastPosition.x;let deltaY = currY - mLastPosition.y;currModel.position.x += deltaX;currModel.position.y -= deltaY;mLastPosition.set(currX,currY);}}function mouseUp(e) {//设置bool值mOnDown = false;mLastPosition.set(0,0);}function wheel(event){var delta = 0;if (!event) event = window.event;if (event.wheelDelta) {//IE、chrome浏览器使用的是wheelDelta,并且值为“正负120”delta = event.wheelDelta/120; if (window.opera) delta = -delta;//因为IE、chrome等向下滚动是负值,FF是正值,为了处理一致性,在此取反处理} else if (event.detail) {//FF浏览器使用的是detail,其值为“正负3”delta = -event.detail/3;}if (delta)currModel.scale.addScalar(delta);}initRocker();// 绘制摇杆function initRocker(){let outerDiameter = 100;// 外圆直径let innerDiameter = 35;// 内圆直径let outerRadius = outerDiameter / 2;let innerRadius = innerDiameter / 2;let centerNum = (outerDiameter - innerDiameter) / 2;// 内圆位置let rockerBox = document.createElement('div');setStyle(rockerBox,{width: `${outerDiameter}px`,height: `${outerDiameter}px`,borderRadius: `${outerRadius}px`,position: 'fixed',bottom: '2rem',right: '4rem',zIndex: 100,background: 'url("./imgs/rocker-bg.png") no-repeat center',backgroundSize: 'contain'});document.body.appendChild(rockerBox);let rockerBtn = document.createElement('div');setStyle(rockerBtn,{position: 'absolute',width: `${innerDiameter}px`,height: `${innerDiameter}px`,left: `${centerNum}px`,bottom: `${centerNum}px`,borderRadius: `${innerRadius}px`,background: '#fbbb1d',});rockerBox.appendChild(rockerBtn);// 添加移动监控事件let startPos = {x:0,y:0};let disX = 0,disY = 0;function onDown(e){e.stopPropagation();startPos.x = e.clientX || e.touches[0].clientX;startPos.y = e.clientY || e.touches[0].clientY;document.addEventListener('mousemove',onMove,false);document.addEventListener('touchmove',onMove,false);}function onMove(e){e.stopPropagation();let clientX = e.clientX || e.touches[0].clientX;let clientY = e.clientY || e.touches[0].clientY;let maxNum = centerNum + 5;disX = (clientX - startPos.x) ;disY = (clientY - startPos.y) ;// 圆心位置 (100,100) (div.style.x + 40, div.style.y + 40)disX = disX > maxNum ? maxNum : (disX < -maxNum ? -maxNum : disX);disY = disY > maxNum ? maxNum : (disY < -maxNum ? -maxNum : disY);if ((Math.pow(disX,2) + Math.pow(disY,2)) > Math.pow(maxNum,2)) {if (disY > 0) {disY = Math.sqrt(Math.pow(maxNum, 2) - Math.pow(disX,2));} else if (disY < 0) {disY = -Math.sqrt(Math.pow(maxNum, 2) - Math.pow(disX,2));}}rockerBtn.style.transform = `translate(${disX}px,${disY}px)`;}rockerUp = function (e){e.stopPropagation();document.removeEventListener('mousemove',onMove,false);document.removeEventListener('touchmove',onMove,false);disX = 0;disY = 0;rockerBtn.style.transform = 'translate(0,0)';}rockerBtn.addEventListener('mousedown',onDown,false);document.body.addEventListener('mouseup',rockerUp,false);rockerBtn.addEventListener('touchstart',onDown,false);document.body.addEventListener('touchend',rockerUp,false);function moveFrame(){requestAnimationFrame(moveFrame);dirImpulse = [disX, disY];}moveFrame();};// 工具函数 function setStyle(dom,options,fn){new Promise(function(resolve,reject){for (let key in options){dom.style[key] = options[key];}resolve();}).then(res => {if (fn) {fn()}}).catch(err => {console.log(err)})} function GetQueryString(name) {var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");var r = window.location.search.substr(1).match(reg);if (r != null)return r[2]; //注意这里不能用js里面的unescape方法return null;}}())</script>
</body>
</html>