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.color
vertexColors: 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 镜像重复排列(重复部分呈现镜像纹理):MirroredRepeatWrapping
texture1.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) {// right
currModel.rotation.y -= 0.04;
} else if (x < 0) {// left
currModel.rotation.y += 0.04;
}
} else {
if (y > 0) {// bottom
currModel.rotation.x += 0.04;
} else if (y < 0) {// top
currModel.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>