3D知识入门

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);

3D在H5页面里本质上就是一个canvas元素,所以,可以与html,css同时存在的,通过设置z-index值来控制div和canvas的显示层级
一般要在canvas上显示button之类的东西,button有个统一的父级标签div,但div设置固定定位,宽高要设置为0,子元素使用绝对定位来设置位置。这样做的目的是,我们一般需要通过手指操作来控制3D模型的缩放旋转和移动,所以需要canvas可以检测到手指操作事件,如果其他的div整个覆盖住了canvas,则手指操控事件就无法使用了

场景Scene

设置背景颜色背景图:

scene.background = new THREE.Color("#88B9DD");
scene.background = textureLoader.load();

设置背景透明:

var renderer = new THREE.WebGLRenderer( { alpha: true } );
renderer.setClearAlpha(0);

设置雾:

 scene.fog = new THREE.FogExp2(0xffffff, 0.01) // 定义雾化颜色和浓度

雾化效果,可以做出 场景中的物体距离摄像机越远越模糊。

渲染器:

WebGLRenderer
基于摄像机和场景提供的信息,调用底层API执行真正的场景绘制工作
属性:
alphaantialiasdepthlogarithmicDepthBufferautoClearautoClearColorautoClearDepthautoClearStencil(模板缓存),sortObjectsclippingPlanesshadowMap
方法:
getContextgetContextAttributesgetPixelRatiosetPixelRatiogetSizesetSizesetViewportsetScissorsetClearColorgetClearColorgetClearAlphaclearrender ( scene, camera, renderTarget, forceClear )setRenderTargetsetMaterialFacesclearTarget

灯光:

决定材质如何显示以及阴影
常用类型:

AmbientLight 环境光
var light = new THREE.AmbientLight( 0x404040, 1.0 ); // soft white light
scene.add( light );

属性为灯光的颜色和强度

DirectionalLight 方向光
var dLight = new THREE.DirectionalLight(0x888888,1);
dLight.position.set(2,7,0);
dLight.target = box1;
scene.add(dLight)

属性为灯光的颜色和强度,位置,和目标方向
其他属性:shadow,castShadow,

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
具备眼睛看到的近大远小的效果
同等情况下,fov越大(视野),模型看起来越小,fov越小,模型看起来越大,一般设置75就好
aspect设置一般设置为屏幕的宽高比就好,如果设置不合适会有宽或者高的拉伸效果
近裁剪面和远裁剪面,超出近裁剪面和远裁剪面的部分会被切掉无法显示,这个设置为合适的值就好。
当加载进来一个模型,发现看不到,一般来调整下模型的缩放值,跳转相机的位置来检查模型

model.scale.set(100,100,100);

属性:
focus,zoom,
方法:
setViewOffset,updateProjectionMatrix,clone,

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)

常用于工程图等无需近大远小的项目.摄像机的距离对渲染没有任何影响
相机常用方法

camera.lookAt(new THREE.Vector3(0,0,0));
camera.lookAt(model.position);

设置相机的位置

camera.position.set(0,0,100);

属性:zoom
方法:
setViewOffsetupdateProjectionMatrixclone

模型加载:

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);
       });

模型加载进来后我们常常需要对模型做一些操作
加载进来的模型数据类似于JS对象,存储着各种属性方法,记录了模型的各个部件,以及各个部件的顶点数据,法向量数据,材质数据,纹理数据等等
我们常常需要改变模型的位置缩放旋转值,
有时候也会需要改变某个部件的颜色值纹理等,或者使用Layer属性改变某个部件的显示隐藏
遍历模型的子元素使用traverse

model.traverse(item => {
	if(item.name.includes('part1')){
		item.layers.set(0)
	} else {
		item.layers.set(1);
	}
})

播放动画Amimation

const clock = new THREE.Clock;

const mixer = new THREE.AnimationMixer(gltf);
const action = mixer.clipAction(gltf.animations[0]);
action.loop = THREE.LoopOnce; //不循环播放
action.clampWhenFinished = true; //暂停在最后一帧播放的状态
action.timeScale = 1.5;
action.play();
timer = setInterval(() => {
  mixer.update(clock.getDelta());
}, 10);

模型动画一般是模型师做好的,当3D模型加载进来后我们使用动画播放类来操控动画播放。
动画可能存在多个,我们可以使用代码来控制要播放哪一个动画,或者进行多个动画混合
可以控制动画播放速度,和从哪里开始播放
属性有:
clampWhenFinished,enabled,loop,paused,repetitions,timeScale(0-1),time,weight
方法:
fadeIn,fadeOut,reset,setDuration,setLoop,startAt,play,stop,

模型控制

<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);
    }

OrbitControls是THREE.js提供的一个类,用于操控3D模型,通过设置一些属性来决定是否允许缩放以及缩放的最大值最小值,是否允许移动,是否允许旋转以及各个旋转轴上的可旋转角度

自己创建模型:

创建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);

3D模型Mesh的基本属性有Geometry形状,和材质Material,
顾名思义形状决定了模型的形状,比如圆形,正方形,面片,圆柱形或者各种各异的形状,基本属性有顶点坐标,顶点索引,顶点法向量,顶点颜色等。任何形状的基础都是三角形,即便是圆柱,基本组成形式也是三角形,当三角形的数量无限多,圆柱看起来就无限精细,渲染成本也更大。三角形数量越少,圆柱看起来越粗糙,渲染成本越低。
常常可见一个属性叫segment,这个属性意思为分段数,就是决定了模型的顶点数量。
所谓材质,就是比如木头,玻璃,他们的粗糙度看起来不同,对光线的反射效果不同,最终看起来具有不同的质感。材质具备一些属性,如map,map是纹理,决定材质的花纹,比如一个笔记本的花纹,可以画着一个老虎,可以画着一座山,map决定了物体的外观。

Mesh

在这里插入图片描述
获取某个子元素:
getObjectByName, getObjectById,

常用属性:
children, position, scale, rotation(quaternion), name, up, userData
常用方法:
traverse,traverseVisible,lookAt,add,remove,clone,getWorldPosition,getWorldRotation,getWorldScale
显示与隐藏layers

材质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
颜色贴图map和color属性颜色值会混合。如果没有特殊需要,设置了颜色贴图.map,不用设置color的值材质对象的父类Material封装了一个.visible属性,通过该属性可以控制是否隐藏该材质对应的模型对象。

// 隐藏网格模型mesh,visible的默认值是true
mesh.material.visible =false;// 注意如果mesh2和mesh的.material属性指向同一个材质,mesh2也会跟着mesh隐藏

注意:如果多个模型引用了同一个材质,如果该材质.visible设置为false,意味着隐藏绑定该材质的所有模型

BoxMesh的不同面设置为不同的材质纹理(了解):
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);

纹理贴图

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坐标
        ]);
const colors = new Float32Array([
                 1, 0, 0, //顶点1颜色
                  0, 1, 0, //顶点2颜色
                  0, 0, 1, //顶点3颜色
              ]);
// 物体有漫反射、镜面反射,太阳光照在一个物体表面,物体表面与光线夹角位置不同的区域明暗程度不同
// WebGL中为了计算光线与物体表面入射角,首先要计算物体表面每个位置的法线方向,
// 没有法向量数据,点光源、平行光等带有方向性的光源不会起作用(物体无法参与光照计算)
// 两个三角形表面法线不同,即使光线方向相同,明暗依然不同,在分界位置将有棱角感。
// 顶点法向量数据和顶点位置数据、顶点颜色数据都是一一对应的。
const normals = new Float32Array([
    0, 0, 1, //顶点1法向量
    0, 0, 1, //顶点2法向量
    0, 0, 1, //顶点3法向量
])
// 创建顶点索引数组的时候,可以根据顶点的数量选择类型数组Uint8Array、Uint16Array、Uint32Array。
// 对于顶点索引而言选择整型类型数组,对于非索引的顶点数据,需要使用浮点类型数组Float32Array等
const indexes = new Uint16Array([// 用于解决重复顶点的问题,重复顶点不需要重复设置位置和法线数据,只需要用索引指向对应的数据即可
                    // 0对应第1个顶点位置数据、第1个顶点法向量数据
                    // 1对应第2个顶点位置数据、第2个顶点法向量数据
                    // 索引值3个为一组,表示一个三角形的3个顶点
                    0, 1, 2
                ])
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 );// 绑定顶点到空几何体

组Group

在这里插入图片描述

var tags = new THREE.Group();

组用于整合元素,比如要创建一个人,一个人作为一整个Group,Group下又分为几部分,头,身体,胳膊,腿。头作为一整个Group,包含有眼睛,耳朵,嘴巴等,胳膊作为一整个Goup,包含有上臂,下臂,手,手作为一个Group包含有5个手指等,Group套Group,就像H5的div套div,当旋转某个Group,如转动头部,Group下的子元素:眼睛耳朵嘴巴就作为一个整体,会整体旋转或移动。需要注意,如果单独移动子元素,父元素不会随之移动,当移动父元素,则所有子元素都会随之移动
某种意义上Group==Object3D。只是Group更加语义化,Object3D本身就是表示模型节点的意思。
threejs默认mesh也可以添加子对象,原因很简单,mesh和Group父类都是Object3D,本质上也可以认为都是Object3D。
Object3D封装了一个属性.visible,通过该属性可以隐藏或显示一个模型

mesh.visible =false;// 隐藏一个网格模型,visible的默认值是true
group.visible =false;// 隐藏一个包含多个模型的组对象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)

精灵用于创建标签,无论怎么旋转模型,Sprite始终面向屏幕,比如创建一个3D地图,地图上要标注很多地点,这些地点就可以使用Sprite来做,无论从什么角度查看3D地图,地点始终面向屏幕。也可以使用CSS2DRenderer,CSS2DObject来做。不过本质上Sprite属于3D元素,CSS2DObject是div标签

数学工具

向量:

三维向量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) {
}

鼠标拾取用于与3D模型的交互,将鼠标的点击坐标进行3D空间转化,从而检测到点击到了模型的哪些部位,从而进行一些UI的更新操作等

模型规范与案例讲解:

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>
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值