1 Star 0 Fork 2

吴建勇/web3DExample

forked from Admin/web3DExample 
加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
2年前
2年前
2年前
2年前
Loading...
README

web3DExample

参考链接

目录


3D方案

  • 技术调研

    以下网站实现全景图VR漫游,均基于Krpano:

    需给钱:pano2vr、playcanvas、得图云、airpano、720云、动景网、全景客、ivrpano、全景旅行者、光鱼全景、全景云、720think、三维家、视网、网展

    krpano核心是js/flash/html5/xml,支持webGL下的webVR

    其它实现方法:CSS3 3D,ThreeJS

  • 720制作方法

    1. 根据教程导图
    2. 在线制作全景图VR漫游
    3. 开通vip下载离线包,部署到自己的网站
    4. iframe嵌套,添加其它功能
  • WebGL3D引擎(three.js,babylon.js,playcanvas)

threejs

场景(scene)

场景能够让你在什么地方、摆放什么东西来交给three.js来渲染,这是你放置物体、灯光和摄像机的地方。

对象体系

所有三维场景中的东西都加到 scene 里来管理。

三维世界本来是黑的,有了 light 之后才能看到东西,有点光源、环境光等不同的光源。

三维世界中的物体,可以从不同角度去观察,改变位置就可以看到不同的风景,这就是相机 camera 的事情。

三维世界中的物体叫做 mesh,任何一个物体都有一个形状,比如圆柱、立方体等,也就是 geometry,然后还得有材质 material,比如金属材质可以反光、普通材质不能。材质可以指定颜色、还可以指定图片作为纹理 texture。

场景中的所有物体,会由渲染器 WebGLRenderer 渲染出来。

场景、物体、灯光、相机、渲染器,这就是 three.js 的核心概念。

每一个物体都可以设置位置 position、缩放 scale、旋转 rotation。

每一帧渲染的时候,改变物体的位置、颜色、旋转角度等就可以实现动画效果了。

容器

容纳着除渲染器以外的三维世界里的一切。

分组(Group)

Three.js 是通过场景 Scene 来管理所有的物体的,加到 Scene 的物体还可以分个组:

const scene = new THREE.Scene();

scene.add(xxx);

const group = new THREE.Group();
group.add(yyy);
group.add(zzz);

scene.add(group);

坐标系与辅助工具

场景的元素采用右手笛卡尔坐标系,x轴正方向向右,y轴正方向向上,z轴由屏幕从里向外

  • 世界坐标

一个应用程序可能包含成百上千个单独的对象,我们必须把他们放到一个公共的场景里,这个公共的场景就叫做世界坐标。

  • 相机默认在世界坐标的原点

  • 3D屏幕中的所有物体都可以在该坐标系系统下移动和旋转

  • 对于屏幕上所有的物体来说,这个坐标系是相同的,并且它不会改变

  • 用户默认的观察视角是沿着Z轴的负半轴方向

  • 世界坐标系是以屏幕中心为原点(0, 0, 0),且是始终不变的

  • 你的右边是x正轴,上面是y正轴,屏幕指向你的为z正轴

  • 窗口范围按此单位恰好是(-1,-1)到(1,1),即屏幕左下角坐标为(-1,-1),右上角坐标为(1,1)

  • 本地坐标

物体的本身的坐标,即物体中心点。

  • 屏幕坐标系

webGL的重要功能之一就是将三维的世界坐标经过变换、投影等计算,最终算出它在显示设备上对应的位置,这个位置就称为设备坐标。在屏幕、打印机等设备上的坐标是二维坐标。

  • 视点坐标系

是以视点(照相机)为原点,以视线的方向为Z+轴正方向的坐标系中的方向。webGL会将世界坐标先变换到视点坐标,然后进行裁剪,只有在视线范围(视见体)之内的场景才会进入下一阶段的计算。

  • AxesHelper三维坐标系

    • 可以在场景中添加辅助坐标系帮助开发
    • 可以使物体绕着本地坐标系旋转,而不绕世界坐标系旋转
    • 用于简单模拟3个坐标轴的对象(红色代表x轴,绿色代表y轴,蓝色代表z轴)
    var axeshelper = new THREE.AxesHelper(5);
    axesHelper.rotation.y -=0.01; //围绕坐标轴旋转
    scene.add(axeshelper)
    
    // 位置x、y、z都大于0,视线指向坐标原点
    camera.position.set(292, 223, 185);
    camera.lookAt(0, 0, 0);
    
  • CameraHelper相机视锥体的辅助对象

    const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    const helper = new THREE.CameraHelper(camera);
    scene.add(helper);
    
  • DirectionalLightHelper平行光的辅助对象

    const light = new THREE.DirectionalLight(0xFFFFFF);
    const helper = new THREE.DirectionalLightHelper(light, 5);
    scene.add(helper);
    
  • 网格辅助

    let gridHelper = new THREE.GridHelper(10, 1, "f0f0f0", "ffffff");//网格大小、步长、中线颜色、网格颜色
    gridHelper.position.x = 10;
    gridHelper.position.y = 2;
    gridHelper.position.z = 10;
    scene.add(gridHelper);
    
  • 极坐标格辅助对象

    const radius = 10;
    const sectors = 16;
    const rings = 8;
    const divisions = 64;
    
    const helper = new THREE.PolarGridHelper(radius, sectors, rings, divisions);
    scene.add(helper);
    
  • 其它辅助对象

    • HemisphereLightHelper:创建一个虚拟的球形网格 Mesh 的辅助对象来模拟 半球形光源 HemisphereLight.
    • PlaneHelper:用于模拟平面 Plane 的辅助对象.
    • PointLightHelper:创建一个虚拟的球形网格 Mesh 的辅助对象来模拟 点光源 PointLight.
    • SkeletonHelper:用来模拟骨骼 Skeleton 的辅助对象. 该辅助对象使用 LineBasicMaterial 材质.
    • SpotLightHelper:用于模拟聚光灯 SpotLight 的锥形辅助对象.

雾(Fog)

线性雾,雾的密度是随着距离线性增大的。

const scene = new THREE.Scene();
scene.fog = new THREE.Fog( 0xcccccc, 10, 15 );

指数雾,它可以在相机附近提供清晰的视野,且距离相机越远,雾的浓度随着指数增长越快。

const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2( 0xcccccc, 0.002 );

摄像机(camera)

在一个空间里可以看向任意方向,可以通过参数调节可视角度和可视距离。

透视相机PerspectiveCamera

符合物理世界近大远小真实情况

PerspectiveCamera( fov : Number, aspect : Number, near : Number, far : Number )
//构造函数参数
//fov:视场角,眼睛睁开的角度,即视角大小。如果设置为0,相当于闭上了眼睛,故什么也看不见;如果为180,看以认为视角很广阔。但是在180度时,物体会很小,因为物体在整个可视区域中的比例变小了。
//aspect:视场宽高比,窗口的纵横比,即宽度除以高度。这个值越大,表示宽度越大,那么看的就越宽;否则看的就越窄。(一般用 画布宽/画布 高即可)
//near:能看多近,眼睛距离近处的距离。不可以设置为负值。
//far:能看多远
//这几个参数决定了哪些scene里的三维顶点会被渲染/绘制出来

正交相机OrthographicCamera

远近大小是一样的

OrthographicCamera(left,right,top,bottom,near,far)
//left:左平面距离相机中心点的垂直距离。
//right:右平面距离相机中心点的垂直距离。
//top:顶平面距离相机中心点的垂直距离。
//bottom:底平面距离相机中心点的垂直距离。
//near:近平面距离相机中心点的垂直距离。
//far:远平面距离相机中心点的垂直距离。
var camera = new THREE.OrthographicCamera(width/-2,width/2,height/2,height/-2,1,1000);
//将浏览器窗口的高度和宽度作为了视景体的高度和宽度,相机正好在窗口的中心点上

立方体相机CubeCamera

为场景中的所要渲染的物体创建快照,创建反光效果

//CubeCamera(near:Number,far:Number,cubeResolution:Number)
//近 - 近裁剪距离。
//远 - 裁剪距离
//cubeResolution - 设置立方体边缘的长度。

//可以通过renderTarget对象获取生成的立方体纹理。
//创建一个获取环境贴图的cubeCamera
cubeCamera = new THREE.CubeCamera(0.1, 1000, 256);
scene.add(cubeCamera);

立体相机StereoCamera

双透视摄像机(立体相机)常被用于创建3D Anaglyph(3D立体影像) 或者Parallax Barrier(视差屏障)。

相机的位置、方向、看向

var camera = new THREE.PerspectiveCamera(45,width/height,1,1000);
//表示相机的位置
camera.position.x = 0;
camera.position.y = 0;
camera.position.z = 600;
//表示相机以哪个方向为上方,相当于真实相机的快门朝向
camera.up.x = 0;
camera.up.y = 1;
camera.up.z = 0;
//相机看向的坐标,即相机的中心镜头对准哪里
camera,lookAt({
    x:0,
    y:0,
    z:0
});

在球坐标中的相机转动

推导相机朝向方向
相机朝向A(x0,y0,z0),圆点O,AO垂直夹角(纬度lat)y,水平夹角(经度lon)x
x0=r在x轴上投影=r在xz平面投影在x轴上投影=rsin(y)cos(x)
y0=r在y轴上投影=rsin(y)
z0=r在z轴上投影=r在xz平面投影在z轴上投影=rsin(y)sin(x)
lat = Math.max(- 85, Math.min(85, lat));//纬度限制在[-85°,85°]
phi = THREE.Math.degToRad(90 - lat);//与y轴夹角,y轴正方向为90°
theta = THREE.Math.degToRad(lon);//与x轴夹角
//相机旋转
camera.target.x = scene_radius * Math.sin(phi) * Math.cos(theta);//r在x轴上投影=r在xz平面投影在x轴上投影=rsin(y)cos(x)
camera.target.y = scene_radius * Math.cos(phi);//r在y轴上投影=rsin(y)
camera.target.z = scene_radius * Math.sin(phi) * Math.sin(theta);//r在z轴上投影=r在xz平面投影在z轴上投影=rsin(y)sin(x)
camera.lookAt(camera.target);

渲染器(renderer)

将camera在scene里看到的内容渲染/绘制到画布上

  • 物体运动的两种方法

    1. 让相机在坐标系里面移动,物体不动。
    2. 物体在坐标系里面移动,摄像机不动。
  • 样例

    const width = window.innerWidth;
    const height = window.innerHeight;
    const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000);
    
    const scene = new THREE.Scene();
    const renderer = new THREE.WebGLRenderer();
    
    camera.position.set(0,0, 100);
    camera.lookAt(scene.position);
    
    renderer.setSize(width, height);
    document.body.appendChild(renderer.domElement)
    
    function render() {
        renderer.render(scene, camera);
        requestAnimationFrame(render);
    }
    

着色器(shader)

每一个图元都会先执行顶点着色器,然后再执行片元着色器。

shader程序就是在显卡中运行的,针对每一个顶点或者片元(像素),显卡将运行一次shader程序。每个顶点或者片元的shader程序在显卡中都是并行运行的,所以速度极快。

  • 分类

    • 顶点着色器 :就是对顶点进行操作。例如改变顶点的位置和顶点的大小。
    • 片元着色器 :通俗讲就是用来定义屏幕中显示的各个点的颜色。
  • 样例

    var gl = renderer.context;
    
    var glVertexShader = new THREE.WebGLShader( gl, gl.VERTEX_SHADER, vertexSourceCode );
    var glFragmentShader = new THREE.WebGLShader( gl, gl.FRAGMENT_SHADER, fragmentSourceCode );
    
    var program = gl.createProgram();
    
    gl.attachShader( program, glVertexShader );
    gl.attachShader( program, glFragmentShader );
    
    gl.linkProgram( program );
    

加载器(loader)

AnimationLoader

以JSON格式来加载 AnimationClips 的一个类。

// 初始化一个加载器
const loader = new THREE.AnimationLoader();

// 加载资源
loader.load(
	// 资源URL
	'animations/animation.js',

	// onLoad回调
	function ( animations ) {
		// animations时一个AnimationClips组数
	},

	// onProgress回调
	function ( xhr ) {
		console.log( (xhr.loaded / xhr.total * 100) + '% loaded' );
	},

	// onError回调
	function ( err ) {
		console.log( 'An error happened' );
	}
);

AudioLoader

用来加载 AudioBuffer的一个类。

// 初始化一个监听
const audioListener = new THREE.AudioListener();

// 把监听添加到camera
camera.add( audioListener );

// 初始化音频对象
const oceanAmbientSound = new THREE.Audio( audioListener );

// 添加一个音频对象到场景中
scene.add( oceanAmbientSound );

// 初始化一个加载器
const loader = new THREE.AudioLoader();

// 加载资源
loader.load(
	// 资源URL
	'audio/ambient_ocean.ogg',

	// onLoad回调
	function ( audioBuffer ) {
		// 给一个加载器对象设置音频对象的缓存
		oceanAmbientSound.setBuffer( audioBuffer );

		// 播放音频
		oceanAmbientSound.play();
	},

	// onProgress回调
	function ( xhr ) {
		console.log( (xhr.loaded / xhr.total * 100) + '% loaded' );
	},

	// onError回调
	function ( err ) {
		console.log( 'An error happened' );
	}
);

BufferGeometryLoader

用来加载BufferGeometry的加载器。

// 初始化一个加载器
const loader = new THREE.BufferGeometryLoader();

// 加载资源
loader.load(
	// 资源URL
	'models/json/pressure.json',

	// onLoad回调
	function ( geometry ) {
		const material = new THREE.MeshLambertMaterial( { color: 0xF5F5F5 } );
		const object = new THREE.Mesh( geometry, material );
		scene.add( object );
	},

	// onProgress回调
	function ( xhr ) {
		console.log( (xhr.loaded / xhr.total * 100) + '% loaded' );
	},

	// onError回调
	function ( err ) {
		console.log( 'An error happened' );
	}
);

Cache

缓存系统,要在所有使用FileLoader的加载器上启用缓存, 需设置THREE.Cache.enabled = true.

CompressedTextureLoader

基于块的纹理加载器 (dds, pvr, ...)的抽象类。

CompressedTextureLoader( manager : LoadingManager )

CubeTextureLoader

加载CubeTexture的一个类。

const scene = new THREE.Scene();
scene.background = new THREE.CubeTextureLoader()
	.setPath( 'textures/cubeMaps/' )
	.load( [
				'px.png',
				'nx.png',
				'py.png',
				'ny.png',
				'pz.png',
				'nz.png'
			] );

DataTextureLoader

用于加载二进制文件格式的(rgbe, hdr, ...)的抽象类。

DataTextureLoader( manager : LoadingManager )

FileLoader

使用XMLHttpRequest来加载资源的低级类,并由大多数加载器内部使用。 它也可以直接用于加载任何没有对应加载器的文件类型。

const loader = new THREE.FileLoader();

//加载一个文本文件,并把结果输出到控制台上
loader.load(
	// resource URL
	'example.txt',

	// onLoad回调
	function ( data ) {
		// output the text to the console
		console.log( data )
	},

	// onProgress回调
	function ( xhr ) {
		console.log( (xhr.loaded / xhr.total * 100) + '% loaded' );
	},

	// onError回调
	function ( err ) {
		console.error( 'An error happened' );
	}
);
//必须启用缓存THREE.Cache.enabled = true;

ImageBitmapLoader

一个把Image加载为ImageBitmap的加载器。

// 初始化一个加载器
const loader = new THREE.ImageBitmapLoader(); 
// set options if needed
loader.setOptions( { imageOrientation: 'flipY' } );
// 加载一个图片资源 
loader.load(
    // 资源的URL
    'textures/skyboxsun25degtest.png',
    // onLoad回调
    function ( imageBitmap ) {
        const texture = new THREE.CanvasTexture( imageBitmap );
        const material = new THREE.MeshBasicMaterial( { map: texture } );
    },
    // 目前暂不支持onProgress的回调 undefined,
    // onError回调
    function ( err ) {
        console.log( 'An error happened' );
    }
);

ImageLoader

用来加载一个Image的加载器。

// 初始化一个加载器
const loader = new THREE.ImageLoader();

// 加载一个图片资源
loader.load(
	// 资源URL
	'textures/skyboxsun25degtest.png',

	// onLoad回调
	function ( image ) {
		// use the image, e.g. draw part of it on a canvas
		const canvas = document.createElement( 'canvas' );
		const context = canvas.getContext( '2d' );
		context.drawImage( image, 100, 100 );
	},

	// 目前暂不支持onProgress的回调
	undefined,

	// onError回调
	function () {
		console.error( 'An error happened.' );
	}
);

MaterialLoader

以JSON格式来加载Material的加载器。

// 初始化一个加载器
const loader = new THREE.MaterialLoader();

// 加载资源
loader.load(
	// 资源URL
	'path/to/material.json',

	// onLoad回调
	function ( material ) {
		object.material = material;
	},

	// onProgress回调
	function ( xhr ) {
		console.log( (xhr.loaded / xhr.total * 100) + '% loaded' );
	},

	// onError回调
	function ( err ) {
		console.log( 'An error happened' );
	}
);

ObjectLoader

以JSON格式来加载物体和场景的加载器。

const loader = new THREE.ObjectLoader();

loader.load(
	// 资源的URL
	"models/json/example.json",

	// onLoad回调
	// Here the loaded data is assumed to be an object
	function ( obj ) {
		// Add the loaded object to the scene
		scene.add( obj );
	},

	// onProgress回调
	function ( xhr ) {
		console.log( (xhr.loaded / xhr.total * 100) + '% loaded' );
	},

	// onError回调
	function ( err ) {
		console.error( 'An error happened' );
	}
);


// 或者,解析先前加载的JSON结构
const object = loader.parse( a_json_object );

scene.add( object );

TextureLoader

加载texture的一个类。

const texture = new THREE.TextureLoader().load( 'textures/land_ocean_ice_cloud_2048.jpg' );

// 立即使用纹理进行材质创建
const material = new THREE.MeshBasicMaterial( { map: texture } );
// 初始化一个加载器
const loader = new THREE.TextureLoader();

// 加载一个资源
loader.load(
	// 资源URL
	'textures/land_ocean_ice_cloud_2048.jpg',

	// onLoad回调
	function ( texture ) {
		// in this example we create the material when the texture is loaded
		const material = new THREE.MeshBasicMaterial( {
			map: texture
		 } );
	},

	// 目前暂不支持onProgress的回调
	undefined,

	// onError回调
	function ( err ) {
		console.error( 'An error happened.' );
	}
);

文字加载器(FontLoader)

各种 Mesh 中比较特殊是文字,它用的是 TextGeometry,文字需要从一个 xxx.typeface.json 中加载。

而这种 json 文件可以用字体文件 ttf 来转换得到。用ttf 转 typeface.json 的这个网站来转:

http://gero3.github.io/facetype.js/

```js
import { FontLoader } from 'three/addons/loaders/FontLoader.js';
const fontLoader = new THREE.FontLoader();

fontLoader.load('./font/xxx.typeface.json', function (font) {
    var textGeometry = new THREE.TextGeometry('文字', 参数);
    const textMaterial = [
        new THREE.MeshBasicMaterial({color: '字体颜色'}),
        new THREE.MeshBasicMaterial({color: '侧面颜色'}),
    ];

    const text = new THREE.Mesh(textGeometry, textMaterial);
});
```

GLTF加载器(GLTFLoader)

glTF(gl传输格式)是一种开放格式的规范 (open format specification), 用于更高效地传输、加载3D内容。该类文件以JSON(.gltf)格式或二进制(.glb)格式提供, 外部文件存储贴图(.jpg、.png)和额外的二进制数据(.bin)。一个glTF组件可传输一个或多个场景, 包括网格、材质、贴图、蒙皮、骨架、变形目标、动画、灯光以及摄像机。

```js
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
// Instantiate a loader
const loader = new GLTFLoader();

// Optional: Provide a DRACOLoader instance to decode compressed mesh data
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath( '/examples/jsm/libs/draco/' );
loader.setDRACOLoader( dracoLoader );

// Load a glTF resource
loader.load(
    // resource URL
    'models/gltf/duck/duck.gltf',
    // called when the resource is loaded
    function ( gltf ) {

        scene.add( gltf.scene );

        gltf.animations; // Array<THREE.AnimationClip>
        gltf.scene; // THREE.Group
        gltf.scenes; // Array<THREE.Group>
        gltf.cameras; // Array<THREE.Camera>
        gltf.asset; // Object

    },
    // called while loading is progressing
    function ( xhr ) {

        console.log( ( xhr.loaded / xhr.total * 100 ) + '% loaded' );

    },
    // called when loading has errors
    function ( error ) {

        console.log( 'An error happened' );

    }
);
```

MMD加载器(MMDLoader)

MMDLoader从MMD资源(例如PMD、PMX、VMD和VPD文件)中创建Three.js物体(对象)。

```js
import { MMDLoader } from 'three/addons/loaders/MMDLoader.js';
// Instantiate a loader
const loader = new MMDLoader();

// Load a MMD model
loader.load(
    // path to PMD/PMX file
    'models/mmd/miku.pmd',
    // called when the resource is loaded
    function ( mesh ) {

        scene.add( mesh );

    },
    // called when loading is in progresses
    function ( xhr ) {

        console.log( ( xhr.loaded / xhr.total * 100 ) + '% loaded' );

    },
    // called when loading has errors
    function ( error ) {

        console.log( 'An error happened' );

    }
);
```

MTL加载器(MTLLoader)

材质模版库(MTL)或 .MTL 文件格式是 .OBJ 的配套文件格式, 用于描述一个或多个 .OBJ 文件中物体表面着色(材质)属性。

```js
import { MTLLoader } from 'three/addons/loaders/MTLLoader.js';
```

OBJ加载器(OBJLoader)

OBJ 文件格式是一种简单的数据格式, 这种格式以人类可读的形式来表示3D几何体,即每个顶点的位置、每个纹理坐标顶点的UV位置、顶点法线、 将使每个多边形定义为顶点列表的面以及纹理顶点。

```js
import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
// instantiate a loader
const loader = new OBJLoader();

// load a resource
loader.load(
    // resource URL
    'models/monster.obj',
    // called when resource is loaded
    function ( object ) {

        scene.add( object );

    },
    // called when loading is in progresses
    function ( xhr ) {

        console.log( ( xhr.loaded / xhr.total * 100 ) + '% loaded' );

    },
    // called when loading has errors
    function ( error ) {

        console.log( 'An error happened' );

    }
);
```

PCD加载器(PCDLoader)

加载PCD格式文件

```js
import { PCDLoader } from 'three/addons/loaders/PCDLoader.js';
// instantiate a loader
const loader = new PCDLoader();

// load a resource
loader.load(
    // resource URL
    'pointcloud.pcd',
    // called when the resource is loaded
    function ( points ) {

        scene.add( points );

    },
    // called when loading is in progresses
    function ( xhr ) {

        console.log( ( xhr.loaded / xhr.total * 100 ) + '% loaded' );

    },
    // called when loading has errors
    function ( error ) {

        console.log( 'An error happened' );

    }
);
```

SVG加载器(SVGLoader)

用于加载.svg资源的加载器,可伸缩向量图形是XML形式的矢量图形格式,用来描述二维矢量图形并支持交互和动画。

```js
import { SVGLoader } from 'three/addons/loaders/SVGLoader.js';
// instantiate a loader
const loader = new SVGLoader();

// load a SVG resource
loader.load(
    // resource URL
    'data/svgSample.svg',
    // called when the resource is loaded
    function ( data ) {

        const paths = data.paths;
        const group = new THREE.Group();

        for ( let i = 0; i < paths.length; i ++ ) {

            const path = paths[ i ];

            const material = new THREE.MeshBasicMaterial( {
                color: path.color,
                side: THREE.DoubleSide,
                depthWrite: false
            } );

            const shapes = SVGLoader.createShapes( path );

            for ( let j = 0; j < shapes.length; j ++ ) {

                const shape = shapes[ j ];
                const geometry = new THREE.ShapeGeometry( shape );
                const mesh = new THREE.Mesh( geometry, material );
                group.add( mesh );

            }

        }

        scene.add( group );

    },
    // called when loading is in progresses
    function ( xhr ) {

        console.log( ( xhr.loaded / xhr.total * 100 ) + '% loaded' );

    },
    // called when loading has errors
    function ( error ) {

        console.log( 'An error happened' );

    }
);
```

TGA加载器(TGALoader)

用于加载.tga资源的加载器。TGA是光栅图形,图形文件格式。

```js
import { TGALoader } from 'three/addons/loaders/TGALoader.js';
// instantiate a loader
const loader = new TGALoader();

// load a resource
const texture = loader.load(
    // resource URL
    'textures/crate_grey8.tga'
    // called when loading is completed
    function ( texture ) {

        console.log( 'Texture is loaded' );

    },
    // called when the loading is in progresses
    function ( xhr ) {

        console.log( ( xhr.loaded / xhr.total * 100 ) + '% loaded' );

    },
    // called when the loading fails
    function ( error ) {

        console.log( 'An error happened' );

    }
);

const material = new THREE.MeshPhongMaterial( {
    color: 0xffffff,
    map: texture
} );
```

控制器(controls)

轨道控制器(OrbitControls)

Orbit controls(轨道控制器)可以使得相机围绕目标进行轨道运动。

支持手动的旋转,这个直接使用 Three.js 的轨道控制器 OrbitControls 就行。

const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

const scene = new THREE.Scene();

const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 10000 );

const controls = new OrbitControls( camera, renderer.domElement );

//controls.update() must be called after any manual changes to the camera's transform
camera.position.set( 0, 20, 100 );
controls.update();

function animate() {

    requestAnimationFrame( animate );

    // required if controls.enableDamping or controls.autoRotate are set to true
    controls.update();

    renderer.render( scene, camera );

}

参数是相机,因为这种视野的改变就是通过改变相机位置和朝向来实现的。

拖放控制器(DragControls)

提供一个拖放交互。

import { DragControls } from 'three/addons/controls/DragControls.js';
const controls = new DragControls( objects, camera, renderer.domElement );

// add event listener to highlight dragged objects
controls.addEventListener( 'dragstart', function ( event ) {
    event.object.material.emissive.set( 0xaaaaaa );
} );

controls.addEventListener( 'dragend', function ( event ) {
    event.object.material.emissive.set( 0x000000 );
} );

第一人称控制器(FirstPersonControls)

第一人称控制器(FirstPersonControls),就是玩游戏时那种交互,通过 W、S、A、D 键控制前后左右,通过鼠标控制方向。

import { FirstPersonControls } from 'three/addons/controls/FirstPersonControls.js';
const controls = new THREE.FirstPersonControls(camera);
controls.lookSpeed = 0.05;//转换方向的速度
controls.movementSpeed = 100;//移动的速度
controls.lookVertical = false;//禁止了纵向的转动

飞行控制器(FlyControls)

FlyControls 启用了一种类似于数字内容创建工具(例如Blender)中飞行模式的导航方式。 你可以在3D空间中任意变换摄像机,并且无任何限制(例如,专注于一个特定的目标)。

import { FlyControls } from 'three/addons/controls/FlyControls.js';

指针锁定控制器(PointerLockControls)

该类的实现是基于Pointer Lock API的。 对于第一人称3D游戏来说, PointerLockControls 是一个非常完美的选择。

import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
const controls = new PointerLockControls( camera, document.body );

controls.maxPolarAngle = Math.PI * 0.5
controls.minPolarAngle = Math.PI * 0.5

// add event listener to show/hide a UI (e.g. the game's menu)
controls.addEventListener( 'lock', function () {
    menu.style.display = 'none';
});

controls.addEventListener( 'unlock', function () {
    menu.style.display = 'block';
});

document.addEventListener('mouseup', () => {
  if (!this.controls.isLocked) {
    this.controls.lock();  // 执行该方法后,鼠标将无法控制,转而控制摄像机的镜头,实现第一人称的操作
  }
});

function updateControls() {
    const time = performance.now();
    // 指针控制器锁定时计算方向和速度
    if (controls.isLocked === true) {

        const delta = (time - prevTime) / 1000;
        
        // 计算每次更新时的速度
        velocity.x -= velocity.x * 10.0 * delta;
        velocity.z -= velocity.z * 10.0 * delta;
        
        // 计算z轴方向数值 1向前,0站立,-1向后
        direction.z = Number(moveForward) - Number(moveBackward);
        
        // 将方向归一化
        direction.normalize(); // this ensures consistent movements in all directions
        
        // 如果按下向前或者向后,计算移动速度
        if (moveForward || moveBackward) velocity.z -= direction.z * 40.0 * delta;
        
        // 角色前后移动方向修改,z如果>0向前移动反之向后移动,如果为0站立
        controls.moveForward(velocity.z * delta);

    }

    prevTime = time;

}

轨迹球控制器(TrackballControls)

TrackballControls 与 OrbitControls 相类似。然而,它不能恒定保持摄像机的up向量。 这意味着,如果摄像机绕过“北极”和“南极”,则不会翻转以保持“右侧朝上”。

import { TrackballControls } from 'three/addons/controls/TrackballControls.js';

变换控制器(TransformControls)

该类可提供一种类似于在数字内容创建工具(例如Blender)中对模型进行交互的方式,来在3D空间中变换物体。 和其他控制器不同的是,变换控制器不倾向于对场景摄像机的变换进行改变。

import { TransformControls } from 'three/addons/controls/TransformControls.js';

几何体(geometry)

3D世界里的所有物体都是点组成面,面组成几何体

var point1 = new THREE.Vector3(2,3,4);
//第二种方法
var point1 = new THREE.Vector3();
point1.set(1,2,3);

线

var geometry = new THREE.BufferGeometry();  //声明一个几何体
var material = new THREE.LineBasicMaterial({vertexColors:true});  //定义线条的材质
//定义两种颜色,分别表示线条两个端点的颜色
var color1 = new THREE.Color(0x444444),color2 = new THREE.Color(0xFF0000);

//线的材质可以由两点的颜色决定
//定义两个顶点的位置,并放在几何体重
var p1 = new THREE.Vector3(-100,0,100);
var p2 = new THREE.Vector3(100,0,-100);
geometry.vertices.push(p1,p2);
//geometry.vertices.push(p2);
geometry.colors.push(color1,color2); //为物体添加颜色

//定义一条线
var line = new THREE.Line(geometry,material,THREE.LineSegments);
scene.add(line);

面(Sprite)

Sprite 是精灵的意思,在 Three.js 中,它就是一个永远面向相机的二维平面。

function addMesh(){
    let texture = new THREE.TextureLoader().load('./sprite.png'); //读取精灵图(其实就是一张png,jpg图片)

    //创建精灵材质
    let spriteMaterial = new THREE.SpriteMaterial({
        map:texture
    });

    //创建精灵
    let sprite = new THREE.Sprite(spriteMaterial);

    // sprite.scale.setScalar(0.1); // scale仅有x和y对sprite生效,scale.z对sprite无效
    // sprite.position.z += 5;
    
    //将精灵添加到场景
    scene.add(sprite);
}

PlaneGeometry(平面几何体)

var camera,scene,renderer;
var mesh;
init();
animate();

function init(){
    renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth,window.innerHeight);
    document.body.appendChild(renderer.domElement);

    camera = new THREE.PerspectiveCamera(70,window.innerHeight/window.innerHeight,1,1000);
    camera.position.z = 400;
    scene = new THREE.Scene

    //x轴宽,y轴高,x轴分段数,y轴分段数
    var geometry = new THREE.PlaneGeometry(500,300,1,1);
    geometry.vertices[0].uv = new THREE.Vector2(0,0);
    geometry.vertices[1].uv = new THREE.Vector2(2,0);
    geometry.vertices[2].uv = new THREE.Vector2(2,2);
    geometry.vertices[3].uv = new THREE.Vector2(0,2);

    var texture = THREE.ImageUtils.loadTexture("index2.jpg",null,function(t){

    });

    var material = new THREE.MeshBasicMaterial({map:texture});
    var mesh = new THREE.Mesh(geometry,material);
    scene.add(mesh);

    window.addEventListener('resize',onwindowResize,false);
}

function animate(){
    requestAnimationFrame(animate);
    renderer.render(scene,camera);
}

CircleGeometry(圆形几何体)

//radius — 圆的半径, 默认值 = 50.
//segments — 分割面数量 (三角形), 最低值 = 3, 默认值 = 8.
//thetaStart — 第一个分割面的开始角度, 默认值 = 0 (3点钟方向).
//thetaLength — 圆形扇形的圆心角通常称为θ。默认为2 * Pi,这形成了一个完整的圆
CircleGeometry(radius, segments, thetaStart, thetaLength)

RingGeometry(环形几何体)

//innerRadius — 内半径默认值为0, 但是当值设为0的时候并不能正常工作.
//outerRadius — 外半径默认值为50.
//thetaSegments — 分割面数量. 更高的值意味着更加的圆滑. 最小值为3. 默认值为8.
//phiSegments — 最小值为1. 默认值为8.
//thetaStart — 第一个分割面的开始角度. 默认值为0.
//thetaLength — 圆形扇形的圆心角通常称为θ. 默认值为Math.PI * 2.
RingGeometry(innerRadius, outerRadius, thetaSegments, phiSegments, thetaStart, thetaLength)

ShapeGeometry(形状几何体)

var rectLength = 120, rectWidth = 40;

var rectShape = new THREE.Shape();
rectShape.moveTo( 0,0 );
rectShape.lineTo( 0, rectWidth );
rectShape.lineTo( rectLength, rectWidth );
rectShape.lineTo( rectLength, 0 );
rectShape.lineTo( 0, 0 );

var rectGeom = new THREE.ShapeGeometry( rectShape );
var rectMesh = new THREE.Mesh( rectGeom, new THREE.MeshBasicMaterial( { color: 0xff0000 } ) ) ;

scene.add( rectMesh );

BoxGeometry(立方几何体)

//width — X轴上的面的宽度.
//height — Y轴上的面的高度.
//depth — Z轴上的面的深度.
//widthSegments — 可选参数. 沿宽度面的分割面数量. 默认值为1.
//heightSegments — 可选参数. 沿高度面的分割面数量. 默认值为1.
//depthSegments — 可选参数. 沿深度面的分割面数量. 默认值为1.
BoxGeometry(width, height, depth, widthSegments, heightSegments, depthSegments)

SphereGeometry(球几何体)

//radius — 球体半径. 默认值为50.
//widthSegments — 水平分割面的数量. 最小值为3, 默认值为8.
//heightSegments — 垂直分割面的数量. 最小值为2, 默认值为6.
//phiStart — 指定水平起始角度. 默认值为0.
//phiLength — 指定水平扫描角度大小. 默认值为 Math.PI * 2.
//thetaStart — 指定垂直起始角度. 默认值为0.
//thetaLength — 指定垂直扫描角度大小. 默认值为Math.PI.
SphereGeometry(radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength)

CylinderGeometry(圆柱几何体)

//radiusTop — 圆柱体顶端半径. 默认值为20.
//radiusBottom — 圆柱体底端半径. 默认值为20.
//height — 圆柱体高度. 默认值为100.
//radiusSegments — 围绕圆柱体周长的分割面数量. 默认值为8.
//heightSegments — 沿圆柱体高度的分割面数量. 默认值为1.
//openEnded — 指示圆柱体两端是打开还是覆盖的布尔值. 默认值为false, 意思是覆盖.
//thetaStart — 第一个分割面的开始角度, 默认值 = 0 (3点钟方向).
//thetaLength — 圆形扇形的圆心角通常称为θ。默认为2 * Pi,这形成了一个完整的圆柱体.
CylinderGeometry(radiusTop, radiusBottom, height, radiusSegments, heightSegments, openEnded, thetaStart, thetaLength)

ConeGeometry(圆锥几何体)

//radius — 锥底半径. 默认值为20.
//height — 锥体高度. 默认值为100.
//radiusSegments — 围绕圆锥周长的分割面数量. 默认值为8.
//heightSegments — 沿圆锥高度的分割面数量. 默认值为1.
//openEnded — 指示锥底是打开还是覆盖的布尔值. 默认值为false, 意思是覆盖.
//thetaStart — 第一个分割面的开始角度, 默认值 = 0 (3点钟方向).
//thetaLength — 圆形扇形的圆心角通常称为θ。默认为2 * Pi,这形成了一个完整的锥体.
ConeGeometry(radius, height, radiusSegments, heightSegments, openEnded, thetaStart, thetaLength)

TorusGeometry(圆环几何体)

//radius — 半径, 默认值为100.
//tube — 管道直径. 默认值为40.
//radialSegments — 默认值为8
//tubularSegments — 默认值为6.
//arc — 圆心角. 默认值为Math.PI * 2.
TorusGeometry(radius, tube, radialSegments, tubularSegments, arc)

TetrahedronGeometry(四面几何体,三棱锥)

//radius — 四面体半径. 默认值为1.
//detail — 默认值为0. 设置为大于0的值将添加顶点使之不再是一个四面体.
TetrahedronGeometry(radius, detail)

OctahedronGeometry(八面几何体,双面三棱锥)

//radius — 八面体半径. 默认值为1.
//detail — 默认值为0. 如果此值设为大于0则不再是八面体.
OctahedronGeometry(radius, detail)

DodecahedronGeometry(十二面几何体,正五边形十二面体)

//radius — 十二面体的半径. 默认值为1.
//detail — 默认值为0. 设置为大于0的值将添加顶点使之不再是一个十二面体.
DodecahedronGeometry(radius, detail)

IcosahedronGeometry(二十面几何体,正三边形十二面体)

//radius — 默认值为1.
//detail — 默认值为0. 设置为大于0的值将添加顶点使之不再是一个二十面体. 当值大于1时它就成了一个球体.
IcosahedronGeometry(radius, detail)

PolyhedronGeometry(多面几何体)

var verticesOfCube = [
    -1,-1,-1,    1,-1,-1,    1, 1,-1,    -1, 1,-1,
    -1,-1, 1,    1,-1, 1,    1, 1, 1,    -1, 1, 1,
];

var indicesOfFaces = [
    2,1,0,    0,3,2,
    0,4,7,    7,3,0,
    0,1,5,    5,4,0,
    1,2,6,    6,5,1,
    2,3,7,    7,6,2,
    4,5,6,    6,7,4
];
//vertices — Array 以 [1,1,1, -1,-1,-1, ... ] 这种形式出现的点的数组
//faces — Array 以 [0,1,2, 2,3,0, ... ] 这种形式出现的构成各个面的指数数组
//radius — Float - 最终形状的半径
//detail — Integer - 把几何模型细分成多少层. 层越多形状越光滑.
//PolyhedronGeometry(vertices, faces, radius, detail)
var geometry = new THREE.PolyhedronGeometry( verticesOfCube, indicesOfFaces, 6, 2 );

TorusKnotGeometry(圆环扭结几何体)

//radius — 半径, 默认值为100.
//tube — 管道直径. 默认值为40.
//tubularSegments — 默认值为64.
//radialSegments — 默认值为8.
//p — 这个值决定了几何体绕旋转对称轴绕了多少圈. 默认值为2.
//q — 这个值决定了几何体绕环面的圆绕了多少圈. 默认值为3.
TorusKnotGeometry(radius, tube, tubularSegments, radialSegments, p, q)

TubeGeometry(管道几何体)

var CustomSinCurve = THREE.Curve.create(
    function ( scale ) { //custom curve constructor
        this.scale = (scale === undefined) ? 1 : scale;
    },

    function ( t ) { //getPoint: t is between 0-1
        var tx = t * 3 - 1.5,
            ty = Math.sin( 2 * Math.PI * t ),
            tz = 0;

        return new THREE.Vector3(tx, ty, tz).multiplyScalar(this.scale);
    }
);

var path = new CustomSinCurve( 10 );

var geometry = new THREE.TubeGeometry(
    path,  //path 从 Curve 基本类继承而来的路径
    20,    //segments 组成管道的分割面数量, 默认值为64
    2,     //radius 管道半径, 默认值为1
    8,     //radiusSegments 组成截面的分割面数量, 默认值为8
    false  //closed 管道是开放的还是闭合的, 默认值为false
);

ExtrudeGeometry(挤压几何体)

将一个2D图形拉伸为一个3D几何体

//shapes — 形状或形状数组.
//options — 包括下面这些参数的对象.
//  curveSegments — int. 曲线上点的个数
//  steps — int. 用于细分拉伸的样条段数量
//  amount — int. 拉伸形状的深度
//  bevelEnabled — bool. 打开斜面
//  bevelThickness — float. 在原来的形状里面弄多深的斜面
//  bevelSize — float. 斜面离形状轮廓的距离
//  bevelSegments — int. 斜面层的数量
//  extrudePath — THREE.CurvePath. 沿3D样条路径拉伸形状. (创建帧(如果帧没有定义))
//  frames — THREE.TubeGeometry.FrenetFrames. 包含切线、法线、副法线的数组
//  material — int. 前面和后面的材质索引号
//  extrudeMaterial — int. 拉伸或斜化面的材质索引号
//  UVGenerator — Object. 提供UV生成器各功能的对象
ExtrudeGeometry(shapes, options)

LatheGeometry(车削几何体,带洞灯罩)

var points = [];
for ( var i = 0; i < 10; i ++ ) {
points.push( new THREE.Vector2( Math.sin( i * 0.2 ) * 10 + 5, ( i - 5 ) * 2 ) );
}
//points — Vector2s数组. X轴上每个点都必须大于0.
//segments — 生成圆周段的数目. 默认值为12.
//phiStart — 起始角度的弧度值. 默认值为0.
//phiLength — 弧度范围在0到2PI间的,2PI是闭合车床, 小于2PI的是部分。默认值为2PI.
//LatheGeometry(points, segments, phiStart, phiLength)
var geometry = new THREE.LatheGeometry( points );
var material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );
var lathe = new THREE.Mesh( geometry, material );
scene.add( lathe );

Geometry(自定义几何体)

通过添加属性值得到相应几何体

//点:this.vertices = [];
//颜色:this.colors = [];
//面:this.faces = []
var geometry = new THREE.Geometry();
geometry.vertices.push(
new THREE.Vector3(-100,100,0),
    new THREE.Vector3(100,-100,0),
    new THREE.Vector3(100.-100,0)
);

ParametricGeometry(参数化几何体)

//func — 一个函数,接收介于0到1之间的 u 和 v 值,并返回一个 Vector3
//slices — 用于参数化函数的切片数量
//stacks — 用于参数化函数的堆栈数量
ParametricGeometry(func, slices, stacks)

TextGeometry(文本几何体)

//text — 要显示的文字
//parameters — 包含下面这些参数的对象
//  font — THREE. 字体.
//  size — Float. 大小.
//  height — Float. 文字厚度. 默认值为50.
//  curveSegments — Integer. 曲线上点的数量. 默认值为12.
//  bevelEnabled — Boolean. 是否打开斜面. 默认值为False.
//  bevelThickness — Float. 文本斜面的深度. 默认值为10.
//  bevelSize — Float. 斜面离轮廓的距离. 默认值为8.
TextGeometry(text, parameters)

ConvexGeometry(凸包几何体)

ConvexGeometry 可被用于为传入的一组点生成凸包。 该任务的平均时间复杂度被认为是O(nlog(n))。

import { ConvexGeometry } from 'three/addons/geometries/ConvexGeometry.js';
const geometry = new ConvexGeometry( points );
const material = new THREE.MeshBasicMaterial( {color: 0x00ff00} );
const mesh = new THREE.Mesh( geometry, material );
scene.add( mesh );

DecalGeometry(贴花几何体)

DecalGeometry 可被用于创建贴花网格物体,以达到不同的目的,例如:为模型增加独特的细节、进行动态的视觉环境改变或覆盖接缝。

import { DecalGeometry } from 'three/addons/geometries/DecalGeometry.js';
const geometry =  new DecalGeometry( mesh, position, orientation, size );
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
const mesh = new THREE.Mesh( geometry, material );
scene.add( mesh );

ParametricGeometry(参数化缓冲几何体)

生成由参数表示其表面的几何体。

import { ParametricGeometry } from 'three/addons/geometries/ParametricGeometry.js';
const geometry = new THREE.ParametricGeometry( THREE.ParametricGeometries.klein, 25, 25 );
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
const klein = new THREE.Mesh( geometry, material );
scene.add( klein );

灯光(light)与阴影(LightShadow)

3d引擎在没有手动创建光的情况下会默认有个环境光,不然你什么都看不到。常见的灯光有以下几种类型:

AmbientLight(环境光,没有方向全局打亮,不会产生明暗)

不能将 THREE.AmbientLight 作为场景中唯一的光源,因为它会将场景中的所有物体渲染为相同的颜色,而不管是什么形状。

在使用其他光源(如 THREE.SpotLight 或者 THREE.DirectionLight)的同时使用它,目的是弱化阴影或给场景添加一些额外的颜色。

var ambientLight = new THREE.AmbientLight(0x523318);
scene.add(ambientLight);

DirectionLight(平行光,无衰减,参考日光来理解)

//THREE.DirectionalLight(hex,intensity)
//hex:光的颜色,用16进制表示
//intensity:光线的强度,默认为1。因为RGB的三个值均在0-255之间,不能反映出光照的强度变化,光照越强,物体表面就更明亮。它的取值范围是0-1.如果为0,表示光线基本没有什么作用,那么物体就会显示会黑色。
//平行光的方向:方向由位置和原点(0,0,0)来决定,方向光只与方向有关,与离物体的远近无关。
var light = new THREE.DirectionalLight(0xff0000,1);
light.position.set(0,0,1);
scene.add(light);

DirectionalLightShadow这是用于在DirectionalLights内部计算阴影

PointLight(点光源,参考灯泡来理解)

//PointLight(color,intensity,distance)
var pointLight = new THREE.PointLight("#ccffcc");
pointLight.position.set(0,10,10);
scene.add(pointLight);
//color:光源的颜色
//distance:光源照射的距离。默认值为 0,意味着光的强度不会随着距离的增加而减少。
//intensity:光源照射的强度。默认值为 1。
//position:光源在场景中的位置。
//visible:设为 ture(默认值),光源就会打开。设为 false,光源就会关闭。

PointLightShadow该类在内部由PointLights所使用,以用于计算阴影。

SpotLight(聚光灯,有衰减,参考舞台聚光灯)

THREE.SpotLight(hex,intensity,distance,angle,exponent)
//hex:聚光灯发出的颜色,十六进制。
//intensity:光源的强度。默认是1.0,如果为0.5,则强度是一半,意思是颜色会淡一些。和点光源一样。
//distance:光线的强度,从最大值衰减到0需要的距离。默认为0,表示光不衰减,如果非0,则表示从光源的位置到Distance的距离,光都在线性衰减。到离光源距离distance时,光源强度为0
//angle:聚光灯着色的角度,用弧度作为单位,这个角度是和光源的方向形成的角度。
//exponent:光源模型中,衰减的一个参数,越大衰减越快

SpotLightShadow这在SpotLights内部用于计算阴影。

HemisphereLight(半球光,不能投射阴影)

光源直接放置于场景之上,光照颜色从天空光线颜色渐变到地面光线颜色。

// HemisphereLight( skyColor : Integer, groundColor : Integer, intensity : Float )
// skyColor -(可选)一个表示颜色的 Color 的实例、字符串或数字,默认为一个白色(0xffffff)的 Color 对象。
// groundColor -(可选)一个表示颜色的 Color 的实例、字符串或数字,默认为一个白色(0xffffff)的 Color 对象。
// intensity -(可选)光照强度。默认值为 1。
const light = new THREE.HemisphereLight( 0xffffbb, 0x080820, 1 );
scene.add( light );

RectAreaLight(平面光光源,模拟明亮的窗户或者条状灯光)

// RectAreaLight( color : Color, intensity : Float, width : Float, height : Float )
// color -(可选)一个表示颜色的 Color 的实例、字符串或数字,默认为一个白色(0xffffff)的 Color 对象。
// intensity -(可选)光源强度/亮度 。默认值为 1。
// width -(可选)光源宽度。默认值为 10。
// height -(可选)光源高度。默认值为 10。
const width = 10;
const height = 10;
const intensity = 1;
const rectLight = new THREE.RectAreaLight( 0xffffff, intensity,  width, height );
rectLight.position.set( 5, 5, 0 );
rectLight.lookAt( 0, 0, 0 );
scene.add( rectLight )

rectLightHelper = new RectAreaLightHelper( rectLight );
scene.add( rectLightHelper );

纹理贴图(texture)

纹理可以看作是图片,或者贴图。3d世界的纹理由图片组成。

  • Texture:创建一个纹理贴图,将其应用到一个表面,或者作为反射/折射贴图。
    • CanvasTexture:从Canvas元素中创建纹理贴图。
    • CompressedTexture:基于被压缩的数据,创建一个纹理贴图,例如从一个DDS文件中。
    • CompressedArrayTexture:基于被压缩的数据,创建一个二维纹理贴图,例如从一个DDS文件中。
    • CubeTexture:创建一个由6张图片所组成的纹理对象。
    • DataArrayTexture:直接从原始数据、宽度、高度和深度创建纹理数组。这种类型的纹理只能与 WebGL 2 渲染上下文一起使用。
    • Data3DTexture:创建一个三维的纹理贴图。这种纹理贴图只能被用于WebGL 2渲染环境中。
    • DataTexture:从原始数据(raw data)、宽(width)、高(height)来直接创建一个纹理贴图。
    • DepthTexture:深度纹理
    • FramebufferTexture:帧缓冲纹理
    • VideoTexture:创建一个使用视频来作为贴图的纹理对象。
//image:这是一个图片的类型,基本上由 ImageUtils来加载
    //var image = THREE.ImageUtils.loadTexture(url);
    //url不能加载本地图片
    //加载的图片的大小必须是2的次方
//mapping:是一个THREE.UVMapping()类型,表示的是纹理坐标。
//wrapS:表示x轴的纹理的回环方式,就是当纹理的宽度小于需要贴图的平面的宽度时,平面剩下的部分应该以何种方式贴图的问题。
//wrapT:表示y轴的纹理回环方式。mapFilter和minFilter表示过滤的方式,这是OpenGL的基本概念。不设置时会取默认值。
//format:表示加载图片的格式,这个参数可以取值THREE.RGBAFormat,RGBFormat等。
    //THREE.RGBAFormat表示每个像素点要使用四个分量表示,分别是红、绿、蓝、透明表示。
    //RGBFormat表示不适用透明,也就是说纹理不会又透明的效果。
//type:表示存储纹理额的内存的每一个字节的格式,是有符号还是没有符号;是整形还是浮点型。默认是无符号型。
//anisotropy:各向异性过滤。使用各向异性过滤能够使纹理的效果更好,但是会消耗更多的内存、CPU、GPU时间。
THREE.Texture (image,mapping,wrapS,wrapT,magFilter,minFilter,format,type,anisotropy)
//纹理贴图
const cakeTexture1 = new THREE.TextureLoader().load('img/cake1.png');
const cakeMaterail1 = new THREE.MeshBasicMaterial({map: cakeTexture1});
//场景贴图
let urls = [
    './img/home.left.jpg',
    './img/home.right.jpg',
    './img/home.top.jpg',
    './img/home.bottom.jpg',
    './img/home.front.jpg',
    './img/home.back.jpg'
];
let cubeTexture = new THREE.CubeTextureLoader().load(urls);
scene.background = cubeTexture;
  • 纹理的坐标

    (0,0)在左下,向右上递增,用一幅图来做纹理的时候,要逆时针指定4角坐标

  • 重复纹理的方式(回环)

    • 简单重复:THREE.RepeatWrapping;纹理将重复无限远。
    • 边缘拉伸:THREE.ClampToEdgeWrappinng,这个是默认设置。即纹理事务最后一个像素延伸到网格的边缘。
    • 镜像重复:THREE.MirroredRepeatWrapping,将纹理重复到无穷大,并在每次重复时都进行镜像。

想象一下你手里有一个立方体,你用一张A4纸包裹上立方体的所有面,并在上面画画。你画的内容就是贴图。

  1. 普通贴图(_col):material.map,替代颜色

  2. 法线贴图(_nor):material.normalMap,让细节程度较低的表面生成高细节程度的精确光照方向和反射效果

  3. 环境光遮蔽贴图(_occ):material.aoMap,用来描绘物体和物体相交或靠近的时候遮挡周围漫反射光线的效果

  4. 环境反射贴图: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;
    }
}

材质(material)

延续贴图里的想象,你用白卡纸画画,还是用油纸画画,呈现出来的质感是不同

  1. MeshBasicMaterial(基础材质,不受光照影响)
  2. MeshStandardMaterial(PBR标准材质)
  3. MeshPhongMaterial(反光材质,适用于陶瓷,烤漆类质感)
  4. MeshToonMaterial(卡通材质,俗称三渲二)
  5. MeshStandardMaterial(PBR标准材质模拟金属反射)
  6. MeshMatcapMaterial(不对灯光作出反应,会投射阴影到一个接受阴影的物体上,但不会产生自身阴影或是接受阴影)
  7. ShadowMaterial(可以接收阴影,但在其他方面完全透明)
  • 样例

    • 透明的玻璃

    天窗和前挡风玻璃的透明度以及基底颜色是不同的

    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;
    }
    
    • 玻璃的反射
    child.material.envMap=allTexture["skymap"].texture;
    //环境反射贴图envMap的映射方式,这里用的是一个叫等量矩形投影的映射方法
    child.material.envMap.mapping = THREE.EquirectangularReflectionMapping;
    //环境反射贴图的强度
    child.material.envMapIntensity=1;
    
    • 车身漆面质感

    使用MeshStandardMaterial材质,通过调节metalness,roughness的值来调节金属的质感

    child.material = new THREE.MeshStandardMaterial();
                        
    child.material.color=new THREE.Color(0x70631B);
    child.material.metalness = 0.44;
    child.material.roughness = 0;
    

3d模型的文件格式

  1. OBJ格式

    老牌通用3d模型文件,不包含贴图,材质,动画等信息。

  2. GLTF格式(图形语言传输格式)

    由OpenGL官方维护团队推出的现代3d模型通用格式,可以包含几何体、材质、动画及场景、摄影机等信息,并且文件量还小。有3D模型界的JPEG之称。

    GLTF(Graphics Language Transmission Format)是一种标准的3D模型文件格式,它以JSON的形式存储3D模型信息,例如模型的层次结构、材质、动画、纹理等。

    模型中依赖的静态资源,比如图片,可以通过外部URI的方式来引入,也可以转成base64直接插入在GLTF文件中。

    它包含两种形式的后缀,分别是.gltf(JSON/ASCII)和.glb(Binary)。.gltf是以JSON的形式存储信息。.glb则是.gltf的扩展格式,它以二进制的形式存储信息,因此导出的模型体积也更小一些。如果我们不需要通过JSON对.gltf模型进行直接修改,建议使用.glb模型,它更小、加载更快。

    //<script src="js/GLTFLoader.js"></script>
    //放到之前添加立方体的代码处
    const loader = new THREE.GLTFLoader();
    
    //加载一个.gltf格式的3d模型文件
    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){});
    

信息点

物体定位:需通过坐标器手动计算

Raycaster( origin, direction, near, far ) 

//origin — 射线的起点向量。
//direction — 射线的方向向量,应该归一标准化。
//near — 所有返回的结果应该比 near 远。Near不能为负,默认值为0。
//far — 所有返回的结果应该比 far 近。Far 不能小于 near,默认值为无穷大。

推导过程

mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
    
推导过程:
设A点为点击点(x1,y1),x1=e.clintX, y1=e.clientY,原点在左上角,右x正半轴,下y正半轴
设A点在世界坐标中的坐标值为B(x2,y2),原点在屏幕中心,右x正半轴,上y正半轴
A需要通过翻折平移等方法,求出相同位置不同坐标系对应坐标
先将坐标方向对齐:A的屏幕(第一象限)实际在B的第四象限,x不变,y翻折(x1,-y1)
然后x和y均需平移半个屏幕
x2' = x1 - innerWidth/2
y2' = innerHeight/2 - y1
又由于在世界坐标的范围是[-1,1],要得到正确的B值我们必须要将坐标标准化
x2 = (x1 -innerWidth/2)/(innerwidth/2) = (x1/innerWidth)*2-1
同理得 y2 = -(y1/innerHeight)*2 +1

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

碰撞检测

碰撞检测是一个非常经典的问题。虽然现在我们都有物理引擎提供的便捷接口,很多时候不需要我们手动实现,但是了解其中的算法思想有助于拓展我们的思维。而且很多问题其实都可以转化为碰撞检测问题,如:视锥体裁剪、LOD(视野范围与显示对象的碰撞)、RTS游戏中单位自动攻击警戒范围内的敌方单位(警戒范围与敌方单位的碰撞)、光线追踪等。

  • Raycaster

    1. 收集障碍物

      模型加载完成后,遍历所有的child,如果child是一个物体(mesh),则把它加入到障碍物队列(colliders)中。

      const colliders = []
      
      loader.load('path/to/gallery.glb',
      gltf => {
          gltf.scene.traverse(child => {
              // 收集障碍物
              if(isMesh(child)) {
              colliders.push(child) 
              }
          })
      } 
      })
      
    2. 检测碰撞

      碰撞检测逻辑基于THREE.Raycaster来实现,racaster可以理解为一个射线,当射线穿过了某个物体,我们就认为射线和物体相交了。

      我们让射线的方向和player的朝向保持一致,并且在移动过程中不断判断射线前方/后面是否有相交的物体,如果有相交的物体,且和射线顶点距离distance < 2.5则认为遇到了障碍物,不能再继续前进。

      function updatePlayer(dt) {
          const pos = player.position.clone()
          pos.y -= 1.5 // 降低高度,用于计算collision
          const dir = new THREE.Vector3()
      
          // 获取当前player的朝向
          player.getWorldDirection(dir)
          dir.negate()
          // 如果是向后退,需要对朝向取反
          if (move.forward < 0) dir.negate()
      
          // 利用Raycaster判断player是否和colliders有碰撞行为
          const raycaster = new THREE.Raycaster(pos, dir)
          let blocked = false
      
          if (colliders.length > 0) {
              const intersect = raycaster.intersectObjects(colliders)
              if (intersect.length > 0) {
                  // 如果相交距离<2.5,表示前方或后面有障碍物
                  if (intersect[0].distance < 2.5) {
                      blocked = true
                  }
              }
          }
          // 如果遇到障碍物,则停滞移动
      
          if (!blocked) {
              // 调整镜头前进 or 后退
              if (move.forward !== 0) {
                  player.translateZ(move.forward > 0 ? -dt * speed : dt * speed * 0.5)
              }
          }
      
          // 调整镜头朝向
          if (move.turn !== 0) {
              player.rotateY(move.turn * 1.2 * dt)
          }
      }
      
  • Octree-八叉树

    可以发现在暴力法中做了太多多余的检查,如果能提前过滤掉一些明显不可能发生碰撞的方块就好了。 思路:将空间按网格分块,只对有可能发生碰撞的网格内的方块进行碰撞检测。具体做法就开启一个网格的数组,然后根据方块的坐标计算出应该属于哪些格子,最后只需要对每个格子内的方块进行碰撞检测即可。

    • 四叉树

      四叉树是一种类似二叉树的数据结构,树中的子节点数只能是4或0(叶子节点)。我们将用树中的每个节点对应到空间中的一片区域,并包含这个区域中的对象。

      因为不可能无限制的划分下去,一般我们会设置两个阈值来控制一个节点是不是应该继续划分:最大对象数和最大划分层级。当一个节点代表的区域中包含的对象数超过最大对象数,且当前层级小于最大划分层级时,我们才对当前节点进行划分,并将当前节点包含的对象分配到子节点中去。这样就能将对象分布密集的区域划分的更细,实现灵活的空间划分。

      利用四叉树将空间划分完毕后,我们就能根据树结构快速查询可能与检测区域发生碰撞的候选项了。具体做法就是从根节点开始检查当前节点代表的区域是否与待检测节点相交,是的话将当前节点包含的对象添加到候选项列表中,然后递归检测子节点即可。

      四叉树

    • 八叉树

      八叉树(Octree)是一种用于描述三维空间的树状数据结构。八叉树的每个节点表示一个正方体的体积元素,每个节点有八个子节点,将八个子节点所表示的体积元素加在一起就等于父节点的体积。八叉树是四叉树在三维空间上的扩展,二维上我们有四个象限,而三维上,我们有8个卦限。八叉树主要用于空间划分和最近邻搜索。

      八叉树

    • Octree模块

      import { Octree } from 'three/examples/jsm/math/Octree';
      import { Capsule } from 'three/examples/jsm/math/Capsule';//在Octree中提供了Capsule对象碰撞的方法,让我们可以直接使用来更容易的实现碰撞检测。
      
      this.octree = new Octree();
      
      //通过fromGraphNode为需要的场景构建节点,通过构建好的节点我们就可以实现碰撞等操作
      const model = this.resource.models.find(item => item.userData.name === 'modular_dungeon') as GLTF;  // 这是我加载的一个模型,不限于任何方式,只需要获取加载好的模型即可
      this.octree.fromGraphNode(model.scene);  // 通过Octree对象来构建节点
      this.scene.add(model.scene);  // 将模型添加到场景中
      
      this.capsule = new Capsule();
      //通过capsuleIntersect方法来捕获Capsule胶囊体与所构建了八叉树节点的场景是否进行了碰撞
      const result = this.octree.capsuleIntersect(this.capsule);
      //result属性
      //depth: 碰撞的深度,可以理解为物体和场景中相机的比例
      //normal:碰撞的法线向量,可以理解为碰撞的方向
      
      //当Capsule对象与场景物体碰撞后,将depth与normal法线向量相乘,得到一个新的数值。
      //这个数值就是我们需要将Capsule对象偏移的值,偏移该值后Capsule对象也就不再与场景物体相碰撞了。
      if (result) {
          const { normal, depth } = result;
          this.capsule.translate(normal.multiplyScalar(depth));
      }
      
      //尽管,我们可能会在不断的移动逻辑中通过translate修改Capsule的位置,又通过capsuleIntersect检测来修复Capsule的位置,但这一切都只是一个数学上的运算。
      //所以,我们需要将Capsule对象的信息同步到场景中的物体上,可以是一个简单的几何体、也可以是一个模型、当然也可以是拍摄的相机。
      

性能监视器Stats

  • 概念

    • FPS:表示上一秒的帧数,值越大越好,一般为60左右。点击下图会变成绿色的。
    • MS:表示渲染一帧需要的毫秒数,这个数字越小越好。再次点击可回到FPS视图中。
  • stats

    var stats = new Stats();
    stats.setMode(1) //参数为0时,FPS界面。参数为1时,MS界面。参数为2时,内存界面。其它为自定义界面
    //将Stats的界面放在左上角
    stats.domElement.style.position = 'absolute';
    stats.domElement.style.left = '0px';
    stats.domElement.style.top = '0px';
    document.body.appendChild(stats.domElement);
    setInterval(function(){
        //在要测试的代码前面调用begin函数,在代码执行完毕后调用end函数,这样就能统计出这段代码执行的平均帧数了。
        stats.begin();
        //每一帧代码stats.update()
        stats.end();
    },1000/60);
    
    <!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>让物体动起来</title>
        <script src="three.js"></script>
        <script src="stats.js"></script>
    </head>
    <body>
        <div id='app'></div>
        <script>
            //创建场景
            var scene = new THREE.Scene();
            //创建相机
            var camera = new THREE.PerspectiveCamera(105,window.innerWidth/window.innerHeight,1,1000);
            camera.position.x = 0;
            camera.position.y = 0;
            camera.position.z = 40;
            //创建渲染器
            render = new THREE.WebGLRenderer({
                antialias:true
            });
            //设置画布大小
            render.setSize(window.innerWidth,window.innerHeight);
    
            var app = document.getElementById("app");
            app.appendChild(render.domElement);
    
            //创造一个立方体,点模型
            var geometry = new THREE.CylinderGeometry(10,20,15,5);//参数:顶面半径,地面半径,高度,分成几边体
            //创造一个立方体,网格模型
            var material3 = new THREE.MeshBasicMaterial({
                color:0x00ff00
            });
            var meshs = new THREE.Mesh(geometry,material3);
            //创建物体的边框线
            var geoedg = new THREE.EdgesGeometry(geometry,10);
            var edgcol = new THREE.LineBasicMaterial({color:0xffd700});
            var geoline = new THREE.LineSegments(geoedg,edgcol);
    
            meshs.add(geoline);
            scene.add(meshs);
    
            //渲染
            render.render(scene,camera);
    
            //性能监视器
            var stats = new Stats();
            stats.setMode(0);
            stats.domElement.style.position = 'absolute';
            stats.domElement.style.left = '30px';
            stats.domElement.style.top = '10px';
            app.appendChild(stats.domElement);
    
            //产生动画
            function animate(){
                //相机移动
                camera.position.y -=0.05;
                if(camera.position.y < -10){
                    camera.position.z +=0.05;
                }
    
                render.render(scene,camera);
    
                //物体移动
                //meshs.position.y +=0.05;
                //if(meshs.position.y>10){
                    //meshs.position.z -=0.05;
                //}
                //render.render(scene,camera);
                //window.requestAnimationFrame(animate);
                stats.update();
            }
            setInterval(animate,10);
    
        </script>
    </body>
    </html>
    

平台

模型:sketchfab 编辑器:threejs editor

优化

  • 在移动端网页里流畅运行,最多不能超过10万面

  • 纹理烘焙(Texture Baking)

    光影效果放到模型里,而非代码实时计算。

    页面中有10+个光源,每个光源都在实时投射阴影(尤其是点光源十分消耗资源,引起卡顿)。但实际,场景中的光源和物体位置都没有发生改变,这意味着我们不需要计算实时阴影,只需要固定的阴影。

    这点可以通过纹理烘焙来实现。并且在移动端,经过纹理烘焙的光影效果实际上要优于设备计算的实时光影效果。

  • 优化模型大小

    • 优先使用.glb而非.gltf格式。.glb是二进制格式,它比.gltf的JSON格式小25% - 30%左右。
    • 将纹理(Texture)和模型分离,并行加载。23M的模型中,其实只有2.3M为模型大小,其余都为纹理贴图。将模型和纹理分开后,可以极大减少模型的加载速度。
    • 使用Draco、gltfpack等工具或一些online compressor来压缩模型(Blender在导出gltf模型时,就带有基于Draco的压缩选项)。
    • 压缩纹理(Texture)。

样例

球形3D漫游

官方示例效果

  1. 思路

    • 第一步:构建一个空间直角坐标系 :Three中称之为场景(Scene)
    • 第二步:在坐标系中,绘制几何体: Three中的几何体有很多种,包括BoxGeometry(立方体),SphereGeometry(球体)等等
    • 第三步:选择一个观察点,并确定观察方向等:Three中称之为相机(Camera)
    • 第四步:将观察到的场景渲染到屏幕上的指定区域 :Three中使用Renderer完成这一工作(相当于拍照)
  2. 素材

球体全景所需的图片素材:宽是高的两倍,数值是2的整数倍最好,建议图片宽高为2048px*1024px

  1. 理论基础
经度:lon,取值范围:[0,360],纬度:lat,取值范围:[-90,90];

经纬度转换三维坐标

X = R * cos(lat)* sin(lon)
Y = R * sin(lat)
Z = R * cos(lat)*cos(lon)

ThreeJS中默认的坐标系是右手坐标系,X轴为左右,Y轴为上下,Z轴为前后。
  1. 具体步骤
  • 第一步:创建一个场景(Scene)
  • 第二步:创建一个球体,并将全景图片贴到球体的内表面,放入场景中
  • 第三步:创建一个透视投影相机将camera拉到球体的中心,相机观看球体内表面
  • 第四步:通过修改经纬度来,改变相机观察的点
  1. 样例代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>手把手教你制作酷炫Web全景</title>
    <meta name="viewport" id="viewport" content="width=device-width,initial-scale=1,minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
</head>
<body>
    <div id="wrap" style="position: absolute;z-index: 0;top: 0;bottom: 0;left: 0;right: 0;width: 100%;height: 100%;overflow: hidden;">
    </div>
    <script src="https://cdn.bootcdn.net/ajax/libs/three.js/r128/three.js"></script>
    <script>
        const width = window.innerWidth, height = window.innerHeight // 屏幕宽高
        const radius = 50 // 球体半径

        // 第一步:创建场景
        const scene = new THREE.Scene()

        // 第二步:绘制一个球体
        const geometry = new THREE.SphereBufferGeometry(radius, 32, 32)
        geometry.scale(-1, 1, 1) // 球面反转,由外表面改成内表面贴图
        const material = new THREE.MeshBasicMaterial({
            map: new THREE.TextureLoader().load('./img/1.jpeg') // 上面的全景图片
        })
        const mesh = new THREE.Mesh(geometry, material)
        scene.add(mesh)

        // 第三步:创建相机,并确定相机位置
        const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 100)
        camera.position.x = 0  // 确定相机位置移到球心
        camera.position.y = 0
        camera.position.z = 0

        camera.target = new THREE.Vector3(radius, 0, 0) // 设置一个对焦点
        

        // 第四步:拍照并绘制到canvas
        const renderer = new THREE.WebGLRenderer()
        renderer.setPixelRatio(window.devicePixelRatio)
        renderer.setSize(width, height) // 设置照片大小

        document.querySelector('#wrap').appendChild(renderer.domElement) // 绘制到canvas
        renderer.render(scene, camera)

        let lat = 0, lon = 0

        function render() {
            lon += 0.003 // 每帧加一个偏移量
            // 改变相机的对焦点,计算公式参考:2.2.2章节
            camera.target.x = radius * Math.cos(lat) * Math.cos(lon);
            camera.target.y = radius * Math.sin(lat);
            camera.target.z = radius * Math.cos(lat) * Math.sin(lon)
            camera.lookAt(camera.target) // 对焦

            renderer.render(scene, camera)
            requestAnimationFrame(render)
        }
        render()
    </script>
</body>

</html>
  1. 手指在屏幕滑动过程
  • touchstart:记录滑动起始的位置(startX,startY, startTime)
  • touchmove:记录当前位置(curX,curY)相减上一次位置的值,乘以factor,计算出(lon,lat),【触摸跟随】
  • touchend:记录endTime,计算本次滑动过程中的平均速度,然后,每帧减去减速度d,直至速度为0或者touchstart事件被触发 【触摸结束触发惯性动画】
distanceX = clientX1 - clientX2   // X轴方向
distanceY = clientY1 - clientY2   // Y轴方向

// 其中R为球体半径,根据弧长公式:
lon = distanX / R
lat = distanY / R
// 增加touch事件监听
let lastX, lastY       // 上次屏幕位置
let curX, curY         // 当前屏幕位置
const factor = 1 / 10  // 灵敏系数

const $wrap = document.querySelector('#wrap')
// 触摸开始
$wrap.addEventListener('touchstart', function (evt) {
    const obj = evt.targetTouches[0] // 选择第一个触摸点
    startX = lastX = obj.clientX
    startY = lastY = obj.clientY
})

// 触摸中
$wrap.addEventListener('touchmove', function (evt) {
    evt.preventDefault()
    const obj = evt.targetTouches[0]
    curX = obj.clientX
    curY = obj.clientY

    // 参考:弧长公式
    lon -= ((curX - lastX) / radius) * factor // factor为了全景旋转平稳,乘以一个灵敏系数
    lat += ((curY - lastY) / radius) * factor

    lastX = curX
    lastY = curY
})
  1. 手势缩放
const camera = new THREE.PerspectiveCamera( fov , aspect , near , fear )
near:取默认值:0.1即可
fear:只要大于球体半径就可,取值为:球体半径R
aspect:在全景的场景已经确定了,照片的长宽比:屏幕宽度 / 屏幕高度
fov:视场,缩放是通过修改它的值来完成全景图片的缩放;
// 其中,(clientX1,clientY1)和(clientX2,clientY2)为双指在屏幕的当前位置

// 计算距离,简化运输不用平方计算
const distance = Math.abs(clientX1 - clientX2) + Math.abc(clientY1 - clientY)
// 计算缩放比
const scale = distance / lastDiance 
// 计算新的视角
fov = camera.fov / scale

// 视角范围取值
camera.fov = Math.min(90, Math.max(fov,60)) // 90 > fov > 60 ,从参数说明中选取

// 视角需要主动更新
camera.updateProjectionMatrix()

立方体3D漫游

官方示例效果

  1. 思路

    • 创建黑色场景
    • 创建六面体/球体并贴图
    • 视角(相机)移到空间
    • 添加信息点和点击热点事件
  2. 实现

    • 创建黑色场景

      var scene, camera, renderer;
      
      function initThree(){
          //场景
          scene = new THREE.Scene();
          //镜头
          camera = new THREE.PerspectiveCamera(90, document.body.clientWidth / document.body.clientHeight, 0.1, 100);
          camera.position.set(0, 0, 0.01);
          //渲染器
          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);
          
          //一会儿在这里添加3D物体
      
          loop();
      }
      
      //帧同步重绘
      function loop() {
          requestAnimationFrame(loop);
          renderer.render(scene, camera);
      }
      
      window.onload = initThree;
      
    • 使用立方体(box)实现

      var materials = [];
      //根据左右上下前后的顺序构建六个面的材质集
      var texture_left = new THREE.TextureLoader().load( './images/scene_left.jpeg' );
      materials.push( new THREE.MeshBasicMaterial( { map: texture_left} ) );
      
      var texture_right = new THREE.TextureLoader().load( './images/scene_right.jpeg' );
      materials.push( new THREE.MeshBasicMaterial( { map: texture_right} ) );
      
      var texture_top = new THREE.TextureLoader().load( './images/scene_top.jpeg' );
      materials.push( new THREE.MeshBasicMaterial( { map: texture_top} ) );
      
      var texture_bottom = new THREE.TextureLoader().load( './images/scene_bottom.jpeg' );
      materials.push( new THREE.MeshBasicMaterial( { map: texture_bottom} ) );
      
      var texture_front = new THREE.TextureLoader().load( './images/scene_front.jpeg' );
      materials.push( new THREE.MeshBasicMaterial( { map: texture_front} ) );
      
      var texture_back = new THREE.TextureLoader().load( './images/scene_back.jpeg' );
      materials.push( new THREE.MeshBasicMaterial( { map: texture_back} ) );
      
      var box = new THREE.Mesh( new THREE.BoxGeometry( 1, 1, 1 ), materials );
      scene.add(box);
      
    • 使用球体(sphere)实现

      var sphereGeometry = new THREE.SphereGeometry(/*半径*/1, /*垂直节点数量*/50, /*水平节点数量*/50);//节点数量越大,需要计算的三角形就越多,影响性能
      
      var sphere = new THREE.Mesh(sphereGeometry);
      sphere.material.wireframe  = true;//用线框模式大家可以看得清楚是个球体而不是圆形
      scene.add(sphere);
      
      var texture = new THREE.TextureLoader().load('./images/scene.jpeg');
      var sphereMaterial = new THREE.MeshBasicMaterial({map: texture});
      
      var sphere = new THREE.Mesh(sphereGeometry,sphereMaterial);
      // sphere.material.wireframe  = true;
      
    • 视角(相机)移到立方体

      box.geometry.scale( 1, 1, -1 );
      
    • 视角(相机)移到球体

      var sphereGeometry = new THREE.SphereGeometry(/*半径*/1, 50, 50);
      sphereGeometry.scale(1, 1, -1);
      
    • 添加信息点

      点的数组

      var hotPoints=[
          {
              position:{
                  x:0,
                  y:0,
                  z:-0.2
              },
              detail:{
                  "title":"信息点1"
              }
          },
          {
              position:{
                  x:-0.2,
                  y:-0.05,
                  z:0.2
              },
              detail:{
                  "title":"信息点2"
              }
          }
      ];
      

      遍历这个数组,并将信息点的指示图添加到3D场景中

      var pointTexture = new THREE.TextureLoader().load('images/hot.png');
      var material = new THREE.SpriteMaterial( { map: pointTexture} );
      
      for(var i=0;i<hotPoints.length;i++){
          var sprite = new THREE.Sprite( material );
          sprite.scale.set( 0.1, 0.1, 0.1 );
          sprite.position.set( hotPoints[i].position.x, hotPoints[i].position.y, hotPoints[i].position.z );
      
      scene.add( sprite );
      }
      
    • 点击热点事件

      sprite.detail = hotPoints[i].detail;
      poiObjects.push(sprite);
      

      通过射线检测(raycast),就像是镜头中心向鼠标所点击的方向发射出一颗子弹,去检查这个子弹最终会打中哪些物体

      document.querySelector("#container").addEventListener("click",function(event){
          event.preventDefault();
      
          var raycaster = new THREE.Raycaster();
          var mouse = new THREE.Vector2();
      
          mouse.x = ( event.clientX / document.body.clientWidth ) * 2 - 1;
          mouse.y = - ( event.clientY / document.body.clientHeight ) * 2 + 1;
      
          raycaster.setFromCamera( mouse, camera );
      
          var intersects = raycaster.intersectObjects( poiObjects );
          if(intersects.length>0){
              alert("点击了热点"+intersects[0].object.detail.title);
          }
      });
      

两个小球可视化A、B两点位置

const A = new THREE.Vector3(0, 30, 0);//A点
const B = new THREE.Vector3(80, 0, 0);//B点

// 绿色小球可视化A点位置
const AMesh = createSphereMesh(0x00ff00,2);
AMesh.position.copy(A);
// 红色小球可视化B点位置
const BMesh = createSphereMesh(0xff0000,2);
BMesh.position.copy(B);

const group = new THREE.Group();
group.add(AMesh,BMesh);

function createSphereMesh(color,R) {
    const geometry = new THREE.SphereGeometry(R);
    const material = new THREE.MeshLambertMaterial({
        color: color,
    });
    const mesh = new THREE.Mesh(geometry, material);
    return mesh;
}

生成一个A指向B的箭头

// 绘制一个从A指向B的箭头
const AB = B.clone().sub(A);
const L = AB.length();//AB长度
const dir = AB.clone().normalize();//单位向量表示AB方向

// 生成箭头从A指向B
const arrowHelper = new THREE.ArrowHelper(dir, A, L)
group.add(arrowHelper);

箭头可视化一个立方体的法线方向

const geometry = new THREE.BoxGeometry(50, 50, 50);
const material = new THREE.MeshLambertMaterial({
    color: 0x00ffff,
});
const mesh = new THREE.Mesh(geometry, material);

const p = mesh.geometry.attributes.position;
const n = mesh.geometry.attributes.normal;
const count = p.count;//顶点数量
for (let i = 0; i < count; i++) {
    // 顶点位置O
    const O = new THREE.Vector3(p.getX(i), p.getY(i), p.getZ(i));
    // 顶点位置O对应的顶点法线
    const dir = new THREE.Vector3(n.getX(i), n.getY(i), n.getZ(i));
    // 箭头可视化顶点法线
    const arrowHelper = new THREE.ArrowHelper(dir, O, 20);
    mesh.add(arrowHelper);
}

更加轻量的3D引擎(css3d)

好处除了库很小以外,还是div+css来搭建三维场景的。但这个库的作者几乎不维护,遇到问题必须得自己想办法解决,比如使用在电脑上会看到明显的面片边缘,但是在手机上浏览的话表现还是相当完美的

使用skybox实现

window.onload=initCSS3D;

function initCSS3D(){
    var s = new C3D.Stage();
    s.size(window.innerWidth, window.innerHeight).update();
    document.getElementById('container').appendChild(s.el);

    var box = new C3D.Skybox();
    box.size(954).position(0, 0, 0).material({
        front: {image: "images/scene_front.jpeg"},
        back: {image: "images/scene_back.jpeg"},
        left: {image: "images/scene_right.jpeg"},
        right: {image: "images/scene_left.jpeg"},
        up: {image: "images/scene_top.jpeg"},
        down: {image: "images/scene_bottom.jpeg"},

    }).update();
    s.addChild(box);

    function loop() {
        angleX += (curMouseX - lastMouseX + lastAngleX - angleX) * 0.3;
        angleY += (curMouseY - lastMouseY + lastAngleY - angleY) * 0.3;

        s.camera.rotation(angleY, -angleX, 0).updateT();
        requestAnimationFrame(loop);
    }

    loop();

    var lastMouseX = 0;
    var lastMouseY = 0;
    var curMouseX = 0;
    var curMouseY = 0;
    var lastAngleX = 0;
    var lastAngleY = 0;
    var angleX = 0;
    var angleY = 0;

    document.addEventListener("mousedown", mouseDownHandler);
    document.addEventListener("mouseup", mouseUpHandler);

    function mouseDownHandler(evt) {
        lastMouseX = curMouseX = evt.pageX;
        lastMouseY = curMouseY = evt.pageY;
        lastAngleX = angleX;
        lastAngleY = angleY;

        document.addEventListener("mousemove", mouseMoveHandler);
    }

    function mouseMoveHandler(evt) {
        curMouseX = evt.pageX;
        curMouseY = evt.pageY;
    }

    function mouseUpHandler(evt) {
        curMouseX = evt.pageX;
        curMouseY = evt.pageY;

        document.removeEventListener("mousemove", mouseMoveHandler);
    }
}

添加信息点

var hotPoints=[
    {
        position:{
            x:0,
            y:0,
            z:-476
        },
        detail:{
            "title":"信息点1"
        }
    },
    {
        position:{
            x:0,
            y:0,
            z:476
        },
        detail:{
            "title":"信息点2"
        }
    }
];
function initPoints(){
    var poiObjects = [];
    for(var i=0;i<hotPoints.length;i++){
        var _p = new C3D.Plane();

        _p.size(207, 162).position(hotPoints[i].position.x,hotPoints[i].position.y,hotPoints[i].position.z).material({
            image: "images/hot.png",
            repeat: 'no-repeat',
            bothsides: true,//注意这个两面贴图的属性
        }).update();
        s.addChild(_p);

        _p.el.detail = hotPoints[i].detail;

        _p.on("click",function(e){
            console.log(e.target.detail.title);
        })
    }
}

bothsides属性为true时,背面的信息点图片是反的。需根据其与相机的夹角重置一下信息点的旋转角度。

var r = Math.atan2(hotPoints[i].position.z-0,0-0) * 180 / Math.PI+90;
_p.size(207, 162).position(hotPoints[i].position.x,hotPoints[i].position.y,hotPoints[i].position.z).material({
            image: "images/hot.png",
            repeat: 'no-repeat',
            bothsides: false,
        }).update();

WebGPU

WebGPU开发环境

谷歌浏览器从Chrome 113 Beta测试版开始默认支持WebGPU。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>http://www.webgl3d.cn/</title>
</head>

<body>
    <script type="module">
        if(navigator.gpu){
            console.log('你的浏览器支持WebGPU。');
        }else{
            console.log('你的浏览器不支持WebGPU,请更换新版本浏览器。');
        }
    </script>
</body>

</html>

GPU设备对象

GPU就是图形处理器,再具体点说,就是你电脑上的显卡,如果为了追求更好的性能,一般会在电脑上安装独立显卡。

创建GPU设备对象device非常简单,执行navigator.gpu.requestAdapter()和adapter.requestDevice()两步操作即可完成。

.requestAdapter()和.requestDevice()都是异步函数,函数前需要加上es6语法的关键字await。

// 浏览器请求GPU适配器
const adapter = await navigator.gpu.requestAdapter();
// 获取GPU设备对象,通过GPU设备对象device的WebGPU API可以控制GPU渲染过程
const device = await adapter.requestDevice();
console.log('适配器adapter',adapter);
console.log('GPU设备对象device',device);
device.createRenderPipeline()//创建渲染管线
device.createComputePipeline()//创建计算管线
device.createShaderModule()//创建着色器模块
device.createCommandEncoder()//创建命令对象(绘制和计算命令)
device.createBuffer()//创建缓冲区对象
device.feature
device.queue

顶点缓冲区、渲染管线

  • WebGPU坐标系

    WebGPU坐标系在Canvas画布上的坐标原点是Canvas画布的中间位置,x轴水平向右,y轴竖直向上,z轴垂直与Canvas画布,朝向屏幕内。

    WebGPU中顶点坐标的表示值采用的是相对值,比如x和y的坐标范围都是[-1,1],z坐标的范围是[0,1]。

  • Float32Array表示顶点坐标

    const vertexArray = new Float32Array([
        // 三角形三个顶点坐标的x、y、z值
        0.0, 0.0, 0.0,//顶点1坐标
        1.0, 0.0, 0.0,//顶点2坐标
        0.0, 1.0, 0.0,//顶点3坐标
    ]);
    
  • 创建顶点缓冲区.createBuffer()

    当device.createBuffer()执行的时候,会在你的电脑显卡GPU的内存(显存)中开辟一片存储空间,用来存存储顶点数据,你可以把这个开辟的存储空间,称为顶点缓冲区。

    const vertexBuffer = device.createBuffer();
    

    设置存储空间的size属性,表示存储空间的大小size。

    const vertexBuffer = device.createBuffer({
        size: vertexArray.byteLength,//数据字节长度
    });
    //类型化数组Float32Array一个数字元素,占用存储空间4字节,9个浮点数,数据字节长度9*4
    console.log('类型化数组数据字节长度',vertexArray.byteLength);
    

    缓冲区用途定义usage

    const vertexBuffer = device.createBuffer({
        size: vertexArray.byteLength,//顶点数据的字节长度
        //usage设置该缓冲区的用途(作为顶点缓冲区|可以写入顶点数据)
        usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
    });
    

    顶点数据写入顶点缓冲区

    //把vertexArray里面的顶点数据写入到vertexBuffer对应的GPU显存缓冲区中
    //参数2的0表示从vertexArray的数据开头读取数据。
    device.queue.writeBuffer(vertexBuffer, 0, vertexArray)
    
  • .createRenderPipeline()创建渲染管线

    const pipeline = device.createRenderPipeline({
        layout: 'auto',
        vertex: {
            // 顶点着色器
            module: device.createShaderModule({ code: vertex }),
            entryPoint: "main"
        },
        fragment: {
            // 片元着色器
            module: device.createShaderModule({ code: fragment }),
            entryPoint: "main",
        },
        primitive: {
            topology: "triangle-list",//三角形绘制顶点数据
        }
    });
    

    vertex.buffers配置顶点缓冲区,顶点缓冲区负责渲染管线提供顶点数据,所以所以需要通过渲染管线参数的vertex.buffers属性配置,渲染管线如何获取顶点缓冲区中的顶点数据。

    const pipeline = device.createRenderPipeline({
        vertex: {//顶点相关配置
            buffers: [// 顶点所有的缓冲区模块设置
                {//其中一个顶点缓冲区设置
                    arrayStride: 3*4,//一个顶点数据占用的字节长度,该缓冲区一个顶点包含xyz三个分量,每个数字是4字节浮点数,3*4字节长度
                    attributes: [{// 顶点缓冲区属性
                        shaderLocation: 0,//GPU显存上顶点缓冲区标记存储位置
                        format: "float32x3",//格式:loat32x3表示一个顶点数据包含3个32位浮点数
                        offset: 0//arrayStride表示每组顶点数据间隔字节数,offset表示读取改组的偏差字节数,没特殊需要一般设置0
                    }]
                }
            ]
        },
    });
    

着色器语言WGSL

WGSL语言是专门给WebGPU定制的着色器语言,就像WebGL OpenGL中使用的GLSL着色器语言。

WGSL英文文档 (opens new window):https://www.w3.org/TR/WGSL/

WebGPU引擎Orillusion团队翻译 (opens new window):https://www.orillusion.com/zh/wgsl.html

顶点着色器

把渲染管线想象为工厂的一条流水线,顶点着色器想象为流水线上一个的工位。

顶点着色器

GPU渲染管线上提供的顶点着色器单元的功能就是计算顶点,所谓计算顶点,简单点说,就是对顶点坐标x、y、z的值进行平移、旋转、缩放等等各种操作。

顶点着色器

顶点着色器

glMatrix生成顶点着色器缩放矩阵

// 传递着色器对应uniform数据
const mat4Array = glMatrix.mat4.create();
//缩放变换
glMatrix.mat4.scale(mat4Array, mat4Array, [0.5, 0.5, 1]);
// 在GPU显存上创建一个uniform数据缓冲区
const mat4Buffer = device.createBuffer({
    size: mat4Array.byteLength,
    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
});
// mat4Array里面矩阵数据写入uniform缓冲区mat4Buffer
device.queue.writeBuffer(mat4Buffer, 0, mat4Array);

glMatrix生成顶点着色器平移矩阵

// 传递着色器对应uniform数据
const mat4Array = glMatrix.mat4.create();
//生成平移变换矩阵
glMatrix.mat4.translate(mat4Array, mat4Array, [-1, -1, 0]);
// 在GPU显存上创建一个uniform数据缓冲区
const mat4Buffer = device.createBuffer({
    size: mat4Array.byteLength,
    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
});
// mat4Array里面矩阵数据写入uniform缓冲区mat4Buffer
device.queue.writeBuffer(mat4Buffer, 0, mat4Array);

模型矩阵(先平移、后缩放)

// 传递着色器对应uniform数据
const modelMatrix = glMatrix.mat4.create();
//后发生缩放变换,先乘
glMatrix.mat4.scale(modelMatrix, modelMatrix, [0.5, 0.5, 1]);
//先发生平移变换,后乘
glMatrix.mat4.translate(modelMatrix, modelMatrix, [-1, -1, 0]);
// 在GPU显存上创建一个uniform数据缓冲区
const modelMatrixBuffer = device.createBuffer({
   size: modelMatrix.byteLength,
   usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
});
device.queue.writeBuffer(modelMatrixBuffer, 0, modelMatrix);

模型矩阵(先缩放、后平移)

// 传递着色器对应uniform数据
const modelMatrix = glMatrix.mat4.create();
//后发生平移变换,先乘
glMatrix.mat4.translate(modelMatrix, modelMatrix, [-1, -1, 0]);
//先发生缩放变换,后乘
glMatrix.mat4.scale(modelMatrix, modelMatrix, [0.5, 0.5, 1])
// 在GPU显存上创建一个uniform数据缓冲区
const modelMatrixBuffer = device.createBuffer({
   size: modelMatrix.byteLength,
   usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
});
device.queue.writeBuffer(modelMatrixBuffer, 0, modelMatrix);

primitive.topology(图元装配)

经过顶点着色器处理过的顶点数据,会进入图元装配环节,简单说就是如何通过顶点数据生成几何图形,比如三个点绘制一个三角形,两点可以绘制一条线段...

通过渲染管线参数的primitive.topology属性可以设置WebGPU如何绘制顶点数据

//triangle-list表示三个点为一组绘制一个三角形。
const pipeline = device.createRenderPipeline({
    primitive: {
        topology: "triangle-list",//绘制三角形
    }
});
//line-strip表示把多个顶点首位相接连接(不闭合),三个坐标点可以绘制两条直线段。
const pipeline = device.createRenderPipeline({
    primitive: {
        topology: "line-strip",//多个定点依次连线
    }
});
//point-list表示每个顶点坐标对应位置渲染一个小点
const pipeline = device.createRenderPipeline({
    primitive: {
        topology: "point-list",
    }
});

WebGPU光栅化、片元着色器

光栅化,就是生成几何图形对应的片元,你可以把片元类比为图像上一个个像素理解,比如绘制绘制一个三角形,光栅化,相当于在三角形返回内,生成一个一个密集排列的片元(像素)。

经过光栅化处理得到的片元,你可以认为是一个没有任何颜色的片元(像素),需要通过渲染管线上片元着色器上色,片元着色器单元就像流水线上一个喷漆的工位一样,给物体设置外观颜色。

给片元着色器传递颜色数据

// 给片元着色器传递一个颜色数据
const colorArray = new Float32Array([0.0,1.0,0.0]);//绿色
// 在GPU显存上创建一个uniform数据缓冲区
const colorBuffer = device.createBuffer({
    size: colorArray.byteLength,
    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
});
// colorArray里面颜色数据写入uniform缓冲区colorBuffer
device.queue.writeBuffer(colorBuffer, 0, colorArray);
// 设置uniform数据的绑定组
// 学习createBindGroup的参数,可以类比渲染管线中shaderLocation学习
const bindGroup = device.createBindGroup({
    // .getBindGroupLayout(0)参数0对应shader中@group(0)代码的参数0
    layout: pipeline.getBindGroupLayout(0),//绑定组,标记为0
    // 一个组里面可以包含多个uniform数据
    entries: [//每个元素可以用来设置一个uniform数据
        {
            //binding的值对应@binding(0)的参数,保持一致,比如都是0
            binding: 0,//标记组里面的uniform数据
            resource: { buffer: mat4Buffer }
        },
        {
            //binding的值对应@binding(1)的参数,保持一致,比如都是1
            binding: 1,//标记组里面的uniform数据
            resource: { buffer: colorBuffer }
        }
    ]
});
// uniform关键字辅助var声明一个三维向量变量color表示片元颜色
//@binding(1)的参数对应webgpu代码.binding的值,保持一致,比如都是1
@group(0) @binding(1) var<uniform> color:vec3<f32>;
@fragment
fn main() -> @location(0) vec4<f32> {
    // return vec4<f32>(1.0, 0.0, 0.0, 1.0);
    return vec4<f32>(color, 1.0);
}

渲染命令

通过WebGPU渲染管线各个功能处理后,会得到图形的片元数据,或者说像素数据,这些像素数据,会存储到显卡内存颜色缓冲区中。

你可以类比顶点缓冲区和理解颜色缓冲区,顶点缓冲区的功能是存储顶点数据,颜色缓冲区的功能是存储渲染管线输出的像素数据。

颜色缓冲区和顶点缓冲区类似,可以创建,不过有一个比较特殊,就是canvas画布对应一个默认的颜色缓冲区,可以直接使用。

如果你希望webgpu绘制的图形,呈现在canvas画布上,就要把绘制的结果输出到canvas画布对应的颜色缓冲区中。

//首先通过GPU设备对象的方法.createCommandEncoder()创建一个命令编码器对象。
// 创建GPU命令编码器对象
const commandEncoder = device.createCommandEncoder();
//通过命令对象的方法.beginRenderPass()可以创建一个渲染通道对象renderPass。
const renderPass = commandEncoder.beginRenderPass({
    // 给渲染通道指定颜色缓冲区,配置指定的缓冲区
    colorAttachments:[{
        // 指向用于Canvas画布的纹理视图对象(Canvas对应的颜色缓冲区)
        // 该渲染通道renderPass输出的像素数据会存储到Canvas画布对应的颜色缓冲区(纹理视图对象)
        view: context.getCurrentTexture().createView(),  
        storeOp: 'store',//像素数据写入颜色缓冲区
        loadOp: 'clear',
        clearValue: { r: 0.5, g: 0.5, b: 0.5, a: 1.0 }, //背景颜色
    }]
});
// const pipeline = device.createRenderPipeline()
// 设置该渲染通道控制渲染管线
renderPass.setPipeline(pipeline);
//顶点缓冲区数据和渲染管线shaderLocation: 0表示存储位置关联起来
renderPass.setVertexBuffer(0, vertexBuffer);
// renderPass.setPipeline(pipeline);
// 绘制命令.draw()绘制顶点数据
renderPass.draw(3);
// 渲染通道结束命令.end()
renderPass.end();
// const commandEncoder = device.createCommandEncoder();
// 命令编码器.finish()创建命令缓冲区(生成GPU指令存入缓冲区)
const commandBuffer = commandEncoder.finish();
// const commandEncoder = device.createCommandEncoder();
const commandBuffer = commandEncoder.finish();
// 命令编码器缓冲区中命令传入GPU设备对象的命令队列.queue
device.queue.submit([commandBuffer]);
// 命令编码器
const commandEncoder = device.createCommandEncoder();
// 渲染通道
const renderPass = commandEncoder.beginRenderPass({
    colorAttachments: [{
        view: context.getCurrentTexture().createView(),
        storeOp: 'store',
        loadOp: 'clear',
    }]
});
renderPass.setPipeline(pipeline);
// 顶点缓冲区数据和渲染管线shaderLocation: 0表示存储位置关联起来
renderPass.setVertexBuffer(0, vertexBuffer);
// 把绑定组里面的uniform数据传递给着色器中uniform变量
renderPass.setBindGroup(0, bindGroup);
renderPass.draw(6);// 绘制顶点数据
renderPass.end();
const commandBuffer = commandEncoder.finish();
device.queue.submit([commandBuffer]);

动画循环render()重复渲染

//渲染循环
function render() {
    // 命令编码器
    const commandEncoder = device.createCommandEncoder();
    // 渲染通道
    const renderPass = commandEncoder.beginRenderPass({
        colorAttachments: [{
            view: context.getCurrentTexture().createView(),
            storeOp: 'store',
            
            loadOp: 'clear',
        }]
    });
    renderPass.setPipeline(pipeline);
    // 顶点缓冲区数据和渲染管线shaderLocation: 0表示存储位置关联起来
    renderPass.setVertexBuffer(0, vertexBuffer);
    // 把绑定组里面的uniform数据传递给着色器中uniform变量
    renderPass.setBindGroup(0, bindGroup);
    renderPass.draw(6);// 绘制顶点数据
    renderPass.end();
    const commandBuffer = commandEncoder.finish();
    device.queue.submit([commandBuffer]);
    requestAnimationFrame(render);
}
render()

WebGPU渲染循环更新uniform矩阵

//渲染循环
let angle = 0.0;//初始旋转角度
function render() {
    angle += 0.05;//每次渲染角度增加
    const modelMatrix = glMatrix.mat4.create();
    // 每次渲染,生成新的旋转矩阵
    glMatrix.mat4.rotateZ(modelMatrix, modelMatrix,angle);
    //模型矩阵modelMatrix重新写入uniform数据的缓冲区中
    device.queue.writeBuffer(modelMatrixBuffer, 0, modelMatrix)

    // 命令编码器
    const commandEncoder = device.createCommandEncoder();
    // 渲染通道
    const renderPass = commandEncoder.beginRenderPass({
        colorAttachments: [{
            view: context.getCurrentTexture().createView(),
            storeOp: 'store',
            loadOp: 'clear',
        }]
    });
    renderPass.setPipeline(pipeline);
    // 顶点缓冲区数据和渲染管线shaderLocation: 0表示存储位置关联起来
    renderPass.setVertexBuffer(0, vertexBuffer);
    // 把绑定组里面的uniform数据传递给着色器中uniform变量
    renderPass.setBindGroup(0, bindGroup);
    renderPass.draw(6);// 绘制顶点数据
    renderPass.end();
    const commandBuffer = commandEncoder.finish();
    device.queue.submit([commandBuffer]);

    requestAnimationFrame(render);
}
render()

模型和库

数学库

  • Box2:创建一个介于最小和最大值二维端点之间的盒子
  • Box3:创建一个介于最小和最大值三维端点之间的盒子
  • Color:表示一个颜色
  • Cylindrical:圆柱坐标
  • Euler:欧拉角描述一个旋转变换,通过指定轴顺序和其各个轴向上的指定旋转角度来旋转一个物体。对 Euler 实例进行遍历将按相应的顺序生成它的分量 (x, y, z, order)。
  • Frustum:视锥体Frustums 用于确定相机视野内的东西。 它有助于加速渲染过程——位于摄像机视锥体外的物体可以安全地排除在渲染之外。
  • Line3:用起点和终点表示的三维线段。
  • MathUtils:具有多个数学实用函数的对象。
  • Matrix3:表示3X3矩阵
  • Matrix4:表示4X4矩阵
  • Plane:在三维空间中无限延伸的二维平面,平面方程用单位长度的法向量和常数表示
  • Quaternion:四元数在three.js中用于表示 rotation (旋转)。对 Quaternion 实例进行遍历将按相应的顺序生成它的分量 (x, y, z, w)。quaternion.setFromAxisAngle( new THREE.Vector3( 0, 1, 0 ), Math.PI / 2 );
  • Ray:射线由一个原点向一个确定的方向发射。它被Raycaster(光线投射)所使用, 以用于辅助raycasting。 光线投射用于在各个物体之间进行拾取(当鼠标经过三维空间中的物体/对象时进行拾取)。
  • Sphere:一个球由球心和半径所定义。Sphere( center : Vector3, radius : Float )
  • Spherical:一个点的spherical coordinates(球坐标)。Spherical( radius : Float, phi : Float, theta : Float )
  • SphericalHarmonics3:表示三次球面谐波(SH)。光照探测器使用此类来编码光照信息。
  • Triangle:一个三角形由三个表示其三个角的Vector3所定义。
  • Vector2:二维向量
  • Vector3:三维向量
  • Vector4:四维向量
  • CubicInterpolant:三次插值CubicInterpolant( 位置数组, 样本数组, 样本数量, 用于存储插值结果的缓冲区 )
  • DiscreteInterpolant:离散插值DiscreteInterpolant( 位置数组, 样本数组, 样本数量, 用于存储插值结果的缓冲区 )
  • LinearInterpolant:线性插值LinearInterpolant( 位置数组, 样本数组, 样本数量, 用于存储插值结果的缓冲区 )
  • QuaternionLinearInterpolant:四元数线性插值QuaternionLinearInterpolant( 位置数组, 样本数组, 样本数量, 用于存储插值结果的缓冲区 )

工具库

  • BufferGeometryUtils:一个包含 BufferGeometry 实例的实用方法的类。(three/addons/utils/BufferGeometryUtils.js)
  • SceneUtils:一个用于操控场景的实用类。(three/addons/utils/SceneUtils.js)
  • SkeletonUtils:用于操控 Skeleton、 SkinnedMesh、和 Bone 的实用方法。(three/addons/utils/SkeletonUtils.js)
  • LoaderUtils:具有多个实用的加载器函数功能的对象

tween.js补间动画库

tween.js是一个补间动画库,我们可以用来做规定时间内的对象属性的数值变化,而且起始值到最终值的变化是自动变化的

var position = { x: 100, y: 0 }
// 首先为位置创建一个补间(tween),然后告诉 tween 我们想要在1000毫秒内以动画的形式移动 x 的位置
var tween = new TWEEN.Tween(position).to({ x: 200 }, 1000).start();

function animate() {
    requestAnimationFrame(animate);
    // [...]
    TWEEN.update();
    // [...]
}

animate();

tween.onUpdate(function(object) {
    console.log(object.x);
});

gl-matrix数学计算库

  • gl-matrix github

  • gl-matrix官网文档

  • 引入gl-matrix.js库

    npm install gl-matrix -S
    import * as glMatix from 'gl-matrix'
    
    <script  src="./gl-matrix-master/dist/gl-matrix.js"></script>
    
  • glMatrix.mat4.fromValues()创建4x4矩阵

    通过.fromValues()方法创建矩阵的时候,输入矩阵参数的顺序是按照矩阵列的顺序,一列一列输入到.fromValues()方法的参数。

    // 创建一个平移矩阵(沿着x、y、z轴分别平移1、2、3)
    //1   0   0    1
    //0   1   0    2
    //0   0   1    3
    //0   0   0    1
    //把矩阵按照列依次写入作为参数
    const mat4T = glMatrix.mat4.fromValues(1,0,0,0,  0,1,0,0,  0,0,1,0,  1,2,3,1);
    
  • glMatrix.mat4.create()创建单位矩阵

    const mat4 = glMatrix.mat4.create();//单位矩阵
    console.log('mat4',mat4);
    
  • 生成平移矩阵

    const mat4 = glMatrix.mat4.create();//单位矩阵,辅助创建平移矩阵
    // 创建一个平移矩阵(沿着x平移2)
    const mat4T = glMatrix.mat4.create();
    glMatrix.mat4.translate(mat4T,mat4,[2,0,0]);
    console.log('mat4T',mat4T);
    
  • 生成缩放矩阵

    const mat4 = glMatrix.mat4.create();
    // 创建一个缩放矩阵(x缩放10) 
    const mat4S = glMatrix.mat4.create();
    glMatrix.mat4.scale(mat4S,mat4,[10,1,1]);
    console.log('mat4S',mat4S)
    
  • 生成旋转矩阵

    const mat4 = glMatrix.mat4.create();
    // 生成一个旋转矩阵(绕z轴旋转45度) 
    const mat4X = glMatrix.mat4.create();
    glMatrix.mat4.rotateX(mat4X,mat4,Math.PI/4);
    console.log('mat4X',mat4X);
    
    const mat4 = glMatrix.mat4.create();
    // 生成一个旋转矩阵(绕z轴旋转45度) 
    const mat4Y = glMatrix.mat4.create();
    glMatrix.mat4.rotateY(mat4Y,mat4,Math.PI/4);
    console.log('mat4Y',mat4Y);
    
    const mat4 = glMatrix.mat4.create();
    // 生成一个旋转矩阵(绕z轴旋转45度) 
    const mat4Z = glMatrix.mat4.create();
    glMatrix.mat4.rotateZ(mat4Z,mat4,Math.PI/4);
    console.log('mat4Z',mat4Z)
    
  • 矩阵乘法运算.multiply(),生成模型矩阵

    // 创建一个平移矩阵(沿着x平移2)
    const mat4T = glMatrix.mat4.create();
    glMatrix.mat4.translate(mat4T,mat4,[2,0,0]);
    // 创建一个缩放矩阵(x缩放10) 
    const mat4S = glMatrix.mat4.create();
    glMatrix.mat4.scale(mat4S,mat4,[10,1,1]);
    // 矩阵乘法运算.multiply()
    const modelMatrix = glMatrix.mat4.create();//模型矩阵
    glMatrix.mat4.multiply(modelMatrix,modelMatrix,mat4S);//后缩放
    glMatrix.mat4.multiply(modelMatrix,modelMatrix,mat4T);//先平移
    console.log('modelMatrix',modelMatrix);
    
    //简化写法生成模型矩阵
    const modelMatrix = glMatrix.mat4.create();
    //后发生缩放变换,先乘
    glMatrix.mat4.scale(modelMatrix, modelMatrix, [10, 1, 1]);
    //先发生平移变换,后乘
    glMatrix.mat4.translate(modelMatrix, modelMatrix, [2, 0, 0]);
    console.log('modelMatrix', modelMatrix);
    
  • 三维向量vec3

    //p1表示一个顶点的坐标
    const p1 = glMatrix.vec3.fromValues(2, 0, 0);
    const p2 = glMatrix.vec3.create();//默认(0,0,0)
    console.log('p2',p2);
    
  • vec3进行进行矩阵变换.transformMat4()

    // 顶点先平移、后缩放
    const modelMatrix = glMatrix.mat4.create();
    glMatrix.mat4.scale(modelMatrix, modelMatrix, [10, 1, 1]);
    glMatrix.mat4.translate(modelMatrix, modelMatrix, [2, 0, 0])
    //p1表示一个顶点的坐标
    const p1 = glMatrix.vec3.fromValues(2, 0, 0);
    const p2 = glMatrix.vec3.create();//默认(0,0,0)
    console.log('p2',p2);
    //p1矩阵变换,变换后结果存储在p2
    glMatrix.vec3.transformMat4(p2, p1, modelMatrix);
    console.log('p2', p2);//Float32Array(3) [40, 0, 0]
    

数学

3D坐标系与三角函数

右手坐标系

Three.js默认坐标系一个默认y轴向上,x轴水平向右,z轴垂直Canvas画布向外。

坐标系角度值

以XOY平面为例,以x轴正半轴为起点,作为角度的0度,逆时针旋转一圈是360度

const x = R * Math.cos(angle);
const y = R * Math.sin(angle);

3D空间中位置坐标

Vector3对象具有属性.x、.y、.z三个属性,这意味着你可以用Vector3对象表示3D空间中的位置坐标x、y、z。

const v3 = new THREE.Vector3(30,30,0);
console.log('v3',v3);

hreejs本身就会给mesh.position一个默认值THREE.Vector3(0,0,0),这就是说你可以不用给mesh.position赋值Vector3对象,你可以直接访问mesh.position,获取或设置Vector3的.x、.y、.z属性。

console.log('mesh.position',mesh.position);
mesh.position.y = 80;// 设置网格模型y坐标
mesh.position.set(80,2,10);// 设置模型xyz坐标

向量

位移量

人在3D空间中的坐标A点是(30,30,0),此人运动到B点,从A到B的位移变化量可以用一个向量Vector3表示,已知AB在x轴上投影长度是100,y方向投影长度是50,这个变化可以用三维向量THREE.Vector3(100,50,0)

const A = new THREE.Vector3(30, 30, 0);// 人起点A
// walk表示运动的位移量用向量
const walk = new THREE.Vector3(100, 50, 0);
const B = new THREE.Vector3();// 人运动结束点B
// 计算结束点xyz坐标
//B.x = A.x + walk.x;
//B.y = A.y + walk.y;
//B.z = A.z + walk.z;
// addVectors的含义就是参数中两个向量xyz三个分量分别相加
//B.addVectors(A,walk);
//console.log('B',B);
B = A.clone().add(walk);//不希望A被改变,使用clone

速度

一个人的运动速度大小是√2,方向是x和y正半轴的角平分线,那么人的速度可以用向量THREE.Vector3(1, 1, 0)表示。

以速度v运动50秒,计算运动位移变化量。

// 向量v表示人速度,大小√2米每秒,方向是x、y正半轴的角平分线
const v = new THREE.Vector3(1, 1, 0);

// xyz三个方向上速度分别和时间相乘,得到三个方向上位移
//const walk = new THREE.Vector3(v.x * 50, v.y * 50, v.z * 50);

// `.multiplyScalar(50)`表示向量x、y、z三个分量和参数分别相乘
const walk = v.clone().multiplyScalar(50);

//假设人起点坐标A(30, 30, 0),以速度v运动50秒,计算运动结束位置。
const A = new THREE.Vector3(30, 30, 0);// 人起点A
const B = A.clone().add(walk);

长度length

const A = new THREE.Vector3(30, 30, 0);// 人起点A
const B = new THREE.Vector3(130,80,0);// 人运动结束点B

const AB = B.clone().sub(A);//B.sub(A);表示B的xyz三个属性分别减去A的xyz三个属性,然后结果赋值给B自身的xyz属性
console.log('AB',AB);

//length函数用于计算两点之间距离,相当于以下计算
// 3D空间,A和B两点之间的距离
const L = Math.sqrt(Math.pow(B.x-A.x,2) + Math.pow(B.y-A.y,2) + Math.pow(B.z-A.z,2));

归一化normalize

单位向量是向量长度.length()为1的向量。

向量归一化,就是等比例缩放向量的xyz三个分量,缩放到向量长度.length()为1。

const AB = new THREE.Vector3(100, 50, 0);
AB.normalize(); //向量归一化

//自己写代码实现归一化。
const dir = new THREE.Vector3(1, 1, 0);
const L = dir.length();
// 归一化:三个分量分别除以向量长度
dir.x = dir.x / L;
dir.y = dir.y / L;
dir.z = dir.z / L;
//Vector3(√2/2, √2/2, 0)   Vector3(0.707, 0.707, 0)
console.log('dir',dir);

物体沿着直线AB平移:单位向量表示平移方向,用向量表示平移过程

//直线上两点坐标A和B
const A = new THREE.Vector3(-50,0,-50);
const B = new THREE.Vector3(100,0,100);
const AB = B.clone().sub(A);//AB向量
AB.normalize();//AB归一化表示直线AB的方向
const T = AB.clone().multiplyScalar(100);

位移:相机沿着视线方向运动

  • 单位向量表示相机视线方向

    相机目标观察点,也就是lookAt参数,和相机位置相减,获得一个沿着相机视线方向的向量,然后归一化,就可以获取一个表示相机视线方向的单位向量。

    camera.position.set(202, 123, 125);
    camera.lookAt(0, 0, 0);
    // 相机目标观察点和相机位置相减,获得一个沿着相机视线方向的向量
    const dir = new THREE.Vector3(0 - 202,0 - 123,0 - 125);
    // 归一化,获取一个表示相机视线方向的单位向量。
    dir.normalize();
    console.log('相机方向',dir);
    console.log('单位向量',dir.length());
    
  • 获取相机视线方向

    通过相机对象的.getWorldDirection()方法,可以快速获取一个沿着相机视线方向的单位向量,不需要自己写代码计算视线方向了

    const dir = new THREE.Vector3();
    // 获取相机的视线方向
    camera.getWorldDirection(dir);
    console.log('相机方向',dir);
    console.log('单位向量',dir.length());
    
  • 相机沿着视线方向平移

    // dis向量表示相机沿着相机视线方向平移200的位移量
    const dis = dir.clone().multiplyScalar(200);
    // 相机沿着视线方向平移
    camera.position.add(dis);
    
  • 相机沿着视线移动动画(tweenjs库辅助)

    import TWEEN from '@tweenjs/tween.js';
    const dir = new THREE.Vector3();
    camera.getWorldDirection(dir);// 获取相机的视线方向
    // dis表示相机沿着相机视线方向平移200
    const dis = dir.clone().multiplyScalar(200);
    // 相机动画:平移前坐标——>平移后坐标
    new TWEEN.Tween(camera.position).to(camera.position.clone().add(dis), 3000).start()
    function render() {
        TWEEN.update();
        renderer.render(scene, camera);
        requestAnimationFrame(render);
    }
    render();
    
  • GUI沿着相机视线方向拖动相机平移

    // 从threejs扩展库引入gui.js
    import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
    const pos0 = camera.position.clone();//记录相机初始位置
    const gui = new GUI();
    // L:相机沿着视线移动距离,从0~200
    gui.add({L: 0}, 'L', 0, 200).onChange(function(v){
        const dis = dir.clone().multiplyScalar(v);//相机沿着视线偏移长度v
        const newPos = pos0.clone().add(dis);//相机初始位置+相机偏移向量    
        camera.position.copy(newPos);//新的位置赋值给相机位置
    });
    

匀速运动

  • 速度 x 时间计算位移

    const v = new THREE.Vector3(10, 0, 10);//物体运动速度
    const clock = new THREE.Clock();//时钟对象
    let t = 0;
    const pos0 = mesh.position.clone();//物体初始位置
    // 渲染循环
    function render() {
        const spt = clock.getDelta();//两帧渲染时间间隔(秒)
        t += spt;
        // 在t时间内,以速度v运动的位移量
        const dis = v.clone().multiplyScalar(t);
        // 网格模型初始位置加上t时间段内运动的位移量
        const newPos = pos0.clone().add(dis);
        mesh.position.copy(newPos);
        renderer.render(scene, camera);
        requestAnimationFrame(render);
    }
    render();
    
  • 速度 x 间隔时间,然后累加计算位移

    const v = new THREE.Vector3(10,0,10);//物体运动速度
    const clock = new THREE.Clock();//时钟对象
    // 渲染循环
    function render() {
        const spt = clock.getDelta();//两帧渲染时间间隔(秒)
        // 在spt时间内,以速度v运动的位移量
        const dis = v.clone().multiplyScalar(spt);
        // 网格模型当前的位置加上spt时间段内运动的位移量
        mesh.position.add(dis);
        renderer.render(scene, camera);
        requestAnimationFrame(render);
    }
    render();
    

重力加速度:物体下落动画

物理加速度位移公式x = vt + 1/2gt^2计算位置

const v = new THREE.Vector3(30, 0, 0);//物体运动速度
const clock = new THREE.Clock();//时钟对象
let t = 0;
const g = new THREE.Vector3(0, -9.8, 0);
const pos0 = mesh.position.clone();
// 渲染循环
function render() {
    if (mesh.position.y > 0) {
        const spt = clock.getDelta();//两帧渲染时间间隔(秒)
        t += spt;
        // 在t时间内,以速度v运动的位移量
        const dis = v.clone().multiplyScalar(t).add(g.clone().multiplyScalar(0.5 * t * t));
        // 网格模型当前的位置加上spt时间段内运动的位移量
        const newPos = pos0.clone().add(dis);
        mesh.position.copy(newPos);
    }
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}
render();
const v = new THREE.Vector3(30, 0, 0);//物体初始速度
const clock = new THREE.Clock();//时钟对象
const g = new THREE.Vector3(0, -9.8, 0);
// 渲染循环
function render() {
    if (mesh.position.y > 0) {
        const spt = clock.getDelta();//两帧渲染时间间隔(秒)
        //spV:重力加速度在时间spt内对速度的改变
        const spV = g.clone().multiplyScalar(spt);
        v.add(spV);//v = v + spV  更新当前速度
        // 在spt时间内,以速度v运动的位移量
        const dis = v.clone().multiplyScalar(spt);
        // 网格模型当前的位置加上spt时间段内运动的位移量
        mesh.position.add(dis);
    }
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}
render();

斜向上抛出去物体(更改初速度)

//物体初始速度
const v = new THREE.Vector3(30, 20, 0);

点乘dot

a.dot(b)的几何含义是向量a在向量b上投影长度与向量b相乘,或者说向量a长度 * 向量b长度 * cos(ab夹角)。

const a = new THREE.Vector3(10, 10, 0);
const b = new THREE.Vector3(20, 0, 0);
// dot几何含义:向量a长度 * 向量b长度 * cos(ab夹角)
const dot = a.dot(b);
console.log('点乘结果',dot);//判断结果是不是200

单位向量点乘含义(计算向量夹角余弦值):两个向量的夹角是θ,两个向量的单位向量进行点乘.dot(),返回的结果就是夹角θ的余弦值cos(θ)

const a = new THREE.Vector3(10, 10, 0);
const b = new THREE.Vector3(20, 0, 0);
// a、b向量归一化后点乘
const cos =  a.normalize().dot(b.normalize());
console.log('向量夹角余弦值',cos);
// 弧度转角度
const angle = THREE.MathUtils.radToDeg(cos);
console.log('向量夹角角度值',angle);

点乘:判断物体在人前或人后

// a向量:人的正前方沿着z轴负半轴
const a = new THREE.Vector3(0,0,-1);

person.position.set(0,0,2);//人位置
mesh.position.set(2,0,-3);//物体位置

//物体坐标减去人坐标,创建一个人指向物体的向量
const b = mesh.position.clone().sub(person.position);

//向量b与向量a的夹角处于0~180度之间。
// 0 ~ 90度:物体在人的前方,人指向物体的向量b与人正前方向量a夹角处于0 ~ 90之间,余弦值大于0
// 90 ~ 180度:物体在人的后方,人指向物体的向量b与人正前方向量a夹角处于90 ~ 180之间,余弦值小于0
const dot = a.dot(b);//向量a和b点乘
if (dot > 0) {
    console.log('物体在人前面');
} else if (dot < 0) {
    console.log('物体在人后面');
}

点乘:判断是否在扇形内

判断一个点(物体)是否在人前方扇形范围内(半径R、扇形角度angle)。

// 已知数据
person.position.set(0, 0, 2);//人位置
mesh.position.set(2, 0, -3);//物体位置
// a向量:人的正前方沿着z轴负半轴
const a = new THREE.Vector3(0, 0, -1);
// 扇形范围
const R = 20;//人前方扇形半径
const angle = 60;//人前方扇形角度

//先通过人与物体距离排除,物体不在扇形范围内情况
// 物体坐标减去人坐标,创建一个人指向物体的向量
const b = mesh.position.clone().sub(person.position);
const L = b.length();//物体与人的距离

//比较距离L与扇形半径的关系,排除物体不在扇形范围内的情况。
if (L < R) {//物体与人的距离在半径R以内
    console.log('物体与人距离小于扇形半径');
}else{
    console.log('不在扇形区域内');
}

//比较夹角余弦值大小判断物体是否在扇形内
b.normalize();//归一化
const cos = a.dot(b);//向量a和b夹角余弦值
//在扇形范围内,从人所在位置,向外沿着扇形半径方向绘制向量c,你会发现向量c与向量a最大夹角就是扇形角度一半。
// 角度转弧度
const rad = THREE.MathUtils.degToRad(angle);
// 扇形角度一半的余弦值
const rangeCos = Math.cos(rad / 2)
//比较向量a、b夹角余弦值cos和扇形角度一半的余弦值rangeCos大小,判断物体是否在扇形内。
if (L < R) {
    if (cos > rangeCos) {//物体在人前方扇形里面
        console.log('cos',cos);
        console.log('人在半径为R,角度为angle的扇形区域内');
    }else{
        console.log('不在扇形区域内');
    }
}else{
    console.log('不在扇形区域内');
}

点乘:判断平行向量方向异同

const a = new THREE.Vector3(10, 10, 0);
const b = new THREE.Vector3(20, 0, 0);
// a、b向量归一化后点乘
const cos =  a.normalize().dot(b.normalize());
console.log('向量夹角余弦值',cos);

//注意判断的前提是,两个向量是平行的,余弦值要么1,要么-1
if(cos>0.5){
    console.log('方向相同');
}else{
    console.log('方向相反');
}

叉乘cross

向量a叉乘向量b,得到一个新的向量c,向量c垂直于向量a和b构成的平面,或者说向量c同时垂直于向量a、向量b。

向量a和b的夹角是θ,a和b叉乘结果是c,c的长度c.length()是a长度a.length()乘b长度b.length()乘夹角θ的正弦值sin(θ)

叉乘不满足交换律

叉乘方向(右手螺旋定则判断),假设向量a和向量b在水平地上,那么向量c,要么竖直向上,要么竖直向下。如果想具体判定向量c的朝向,最简单的方式,就是用箭头ArrowHelper可视化c,一看便知。偏理论的方式就是通过右手螺旋定则,判断叉乘结果c的方向,没有threejs箭头简单直观,如果你不想掌握,也没关系,写代码时候,用ArrowHelper类辅助判断。

你先把向量c想象成一根筷子,尝试用手去握住它。具体过程就是,把右手手掌展平,四指并拢,大拇指与四指垂直,假设向量a和b处于水平平面上,向量c就是竖直方向,让大拇指沿着c,大拇指朝上还是朝下,随便先选个方向,让四指沿着向量a的方向,去开始握住向量c,这时候如果四指旋转的方向靠近向量b,那么说明大拇指的指向方向是向量c的方向,否则反之。

//.cross()和.crossVectors()都是向量对象的叉乘计算方法,功能一样,只是使用的细节有些不同,向量对象叉乘的结果仍然是向量对象。
const c = new THREE.Vector3();
c.crossVectors(a,b);
c = a.clone().cross(b);
c.length() = a.length()*b.length()*sin(θ);

叉乘:判断人左右

//在XOZ平面上,随机选择了两个坐标,作为人和物体的位置,选择一个方向作为人的正前方。
person.position.set(0, 0, 2);//人位置
// a向量:假设人的正前方沿着z轴负半轴
const a = new THREE.Vector3(0, 0, -5);

// 箭头可视化向量a
const arrowA = new THREE.ArrowHelper(a.clone().normalize(), person.position, a.length(),0xff0000);
model.add(arrowA);

//物体位置选择了两种情况,一个在人的左侧(左前方),一个在人的右侧(右前方)。
mesh.position.set(2, 0, -3);//物体位置在人右边     
mesh.position.set(-2, 0, -3);//物体位置在人左边 

//创建人指向物体的向量b
//物体两个位置对应的两个向量b,分别位于向量a左右两侧。
const b = mesh.position.clone().sub(person.position);
const arrowB = new THREE.ArrowHelper(b.clone().normalize(), person.position, b.length(),0x00ff00);
model.add(arrowB);

const c = a.clone().cross(b);
c.normalize();
// 可视化向量c方向
const arrowC = new THREE.ArrowHelper(c, person.position, 2.5 ,0x0000ff);

//当向量b在向量a右侧(物体在人右侧)时,向量c竖直向下,当向量b在向量a左侧(物体在人左侧)时,向量c竖直向上。
//向量旋转不超过180度情况下,向量b在向量a右侧,说明向量a顺时针旋转与b重合,向量c竖直向下,当向量b在向量a左侧时,说明向量a逆时针旋转与b重合,向量c竖直向上。
// 根据向量c方向,判断物体在人的左侧还是右侧。
if(c.y < 0){
    console.log('物体在人右侧');
}else if(c.y > 0){
    console.log('物体在人左侧');
}

叉乘:计算三角形法线

思路非常简单,可以把通过三角形的三个顶点构建两个向量,两个向量叉乘,就会得到一个垂直三角形的向量c。不过注意一点,如果两个向量,随意构建,实际计算结果向量c虽然都垂直a和b但是方向可能有两种情况。所以,三个顶点构建两个向量,按照三角形顶点的顺序,构建1指向2的向量,2指向3的向量,这样可以向量叉乘结果可以反应三角形三个点位置顺序关系。

// 已知三角形三个顶点的坐标,计算三角形法线方向
const p1 = new THREE.Vector3(0, 0, 0);
const p2 = new THREE.Vector3(50, 0, 0);
const p3 = new THREE.Vector3(0, 100, 0);

// 三个顶点构建两个向量,按照三角形顶点的顺序,构建1指向2的向量,2指向3的向量
const a = p2.clone().sub(p1);
const b = p3.clone().sub(p2);

const c = a.clone().cross(b);
c.normalize();//向量c归一化表示三角形法线方向

// 可视化向量a和b叉乘结果:向量c
const arrow = new THREE.ArrowHelper(c, p3, 50, 0xff0000);
mesh.add(arrow);

叉乘:计算三角形面积

// c.crossVectors(a,b);
// c.length() = a.length()*b.length()*sin(θ)
// S = 0.5*a.length()*b.length()*sin(θ)

// 三角形两条边构建两个向量
const a = p2.clone().sub(p1);
const b = p3.clone().sub(p1);
// 两个向量叉乘结果c的几何含义:a.length()*b.length()*sin(θ)
const c = a.clone().cross(b);

// 三角形面积计算
const S = 0.5*c.length();

console.log('S',S);

网格模型Mesh其实就一个一个三角形拼接构成,这意味着,我们可以通过计算Mesh所有三角形面积,然后累加,就可以获取模型的表面积。

  • Geometry有顶点索引数据

    //三角形面积计算
    function AreaOfTriangle(p1, p2, p3) {
        // 三角形两条边构建两个向量
        const a = p2.clone().sub(p1);
        const b = p3.clone().sub(p1);
        // 两个向量叉乘结果c的几何含义:a.length()*b.length()*sin(θ)
        const c = a.clone().cross(b);
        // 三角形面积计算
        const S = 0.5 * c.length();
        return S
    }
    //获取模型对象所有的三角形,分别计算某个三角形对应的面积,然后所有三角形面积累加,就可以获取模型的表面积。
    const pos = geometry.attributes.position;
    const index = geometry.index;
    console.log('geometry',geometry);
    let S = 0;//表示物体表面积
    for (var i = 0; i < index.count; i += 3) {
        // 获取当前三角形对应三个顶点的索引
        const i1 = index.getX(i);
        const i2 = index.getX(i + 1);
        const i3 = index.getX(i + 2);
    
        //获取三个顶点的坐标 
        const p1 = new THREE.Vector3(pos.getX(i1), pos.getY(i1), pos.getZ(i1));
        const p2 = new THREE.Vector3(pos.getX(i2), pos.getY(i2), pos.getZ(i2));
        const p3 = new THREE.Vector3(pos.getX(i3), pos.getY(i3), pos.getZ(i3));
        S += AreaOfTriangle(p1, p2, p3); 
    }
    console.log('S',S);
    
  • Geometry没有顶点索引数据:直接从顶点位置属性获取每间隔三个点作为一个三角形数据。

    const pos = geometry.attributes.position;
    let S = 0;//表示物体表面积
    for (let i = 0; i < pos.count; i += 3) {
        const p1 = new THREE.Vector3(pos.getX(i), pos.getY(i), pos.getZ(i));
        const p2 = new THREE.Vector3(pos.getX(i + 1), pos.getY(i + 1), pos.getZ(i + 1));
        const p3 = new THREE.Vector3(pos.getX(i + 2), pos.getY(i + 2), pos.getZ(i + 2));
        S += AreaOfTriangle(p1, p2, p3);//所有三角形面积累加
    }
    console.log('S', S);
    //三角形面积计算
    function AreaOfTriangle(p1, p2, p3) {
        // 三角形两条边构建两个向量
        const a = p2.clone().sub(p1);
        const b = p3.clone().sub(p1);
        // 两个向量叉乘结果c的几何含义:a.length()*b.length()*sin(θ)
        const c = a.clone().cross(b);
        // 三角形面积计算
        const S = 0.5 * c.length();
        return S;
    }
    

点乘叉乘综合:判断两个点是否在线段同一侧

//为了方便查看几何关系,可以尝试可视化表示
// 小球可视化四个坐标点
const group = new THREE.Group();
const AMesh = createSphereMesh(0xffff00,2);
AMesh.position.copy(A);
const BMesh = createSphereMesh(0xffff00,2);
BMesh.position.copy(B);
const p1Mesh = createSphereMesh(0xff0000,2);
p1Mesh.position.copy(p1);
const p2Mesh = createSphereMesh(0xff0000,2);
p2Mesh.position.copy(p2);
group.add(AMesh,BMesh,p1Mesh,p2Mesh);

function createSphereMesh(color,R) {
    const geometry = new THREE.SphereGeometry(R);
    const material = new THREE.MeshLambertMaterial({
        color: color,
    });
    const mesh = new THREE.Mesh(geometry, material);
    return mesh;
}
// Line可视化线段AB
const geometry = new THREE.BufferGeometry(); 
const vertices = new Float32Array([
    A.x, A.y, A.z, 
    B.x, B.y, B.z, 
]);
geometry.attributes.position = new THREE.BufferAttribute(vertices, 3);
const material = new THREE.LineBasicMaterial({
    color: 0xffff00, 
});
const line = new THREE.LineLoop(geometry, material); 
group.add(line);

//p1分别向线段AB两点创建两条向量a1、b1
//p2分别向线段AB两点创建两条向量a2、b2
//你会发现,p1、p2同侧时候,a1转向b1与a2转向b2方向一致,如果是异侧,方向不一致。
//换句话说,a1叉乘b1得到向量c1,与a2叉乘b2得到向量c2,如果p1、p2同侧,那么c1和c2方向一样,否则方向不同。

// p1分别向线段AB两点创建两条向量a1、b1
const a1 = A.clone().sub(p1);
const b1 = B.clone().sub(p1);
// p2分别向线段AB两点创建两条向量a2、b2
const a2 = A.clone().sub(p2);
const b2 = B.clone().sub(p2);
// 通过c1、c2方向是否相同来推断两点是否位于线段同一侧
const c1 = a1.clone().cross(b1);
const c2 = a2.clone().cross(b2);

//箭头可视化所有向量辅助判断
group.add(new THREE.ArrowHelper(a1.clone().normalize(), p1, a1.length(),0xff0000))
group.add(new THREE.ArrowHelper(b1.clone().normalize(), p1, b1.length(),0x00ff00))
group.add(new THREE.ArrowHelper(a2.clone().normalize(), p2, a2.length(),0xff0000))
group.add(new THREE.ArrowHelper(b2.clone().normalize(), p2, b2.length(),0x00ff00))
group.add(new THREE.ArrowHelper(c1.clone().normalize(), p1, 50, 0x0000ff))
group.add(new THREE.ArrowHelper(c2.clone().normalize(), p2, 50, 0x0000ff))

// 向量c1与c2夹角余弦值:用来推断向量c1与c2方向是否相同
const cos =  c1.normalize().dot(c2.normalize());
if(cos>0.5){//方向相同时候,余弦值1>0.5
    console.log('方向相同,两点在线段同侧');
}else{//方向相反时候,余弦值-1<0.5
    console.log('方向相反,两点在线段异侧');
}

点乘叉乘综合:点到直线的距离

// 已知条件
// 直线经过两点坐标A、B
const A = new THREE.Vector3(0, 0, 0);
const B = new THREE.Vector3(100, 0, 0);
// 直线外一点p
const p = new THREE.Vector3(50, 0, 30);

// ApB构建一个三角形,其中两条边构建向量a、向量b
const a = A.clone().sub(p);
const b = B.clone().sub(p);
const c = a.clone().cross(b);
const S = 0.5*c.length();//叉乘结果长度一半是三角形ApB的面积

//计算三角形ApB底边AB的长度
const AB = B.clone().sub(A);
const width = AB.length();//AB两点距离

//计算三角形高度(点到直线的距离)
//叉乘结果长度一半是三角形ApB的面积
const S = 0.5*c.length();
//AB两点距离
const width = AB.length();
const H = S / width * 2;//三角形高度,也就是点到直线的距离
console.log('点到直线的距离',H);

空间姿态角度

欧拉角Euler

欧拉角Euler是用来表述物体空间姿态角度的一种数学工具,Three.js也提供了相关的类Euler。

  • 欧拉对象Euler

    Euler(x,y,z,order),参数xyz分别表示绕xyz轴旋转的角度值,角度单位是弧度。参数order表示旋转顺序,默认值XYZ,也可以设置为YXZ、YZX等值

    // 创建一个欧拉对象,表示绕着xyz轴分别旋转45度,0度,90度
    var Euler = new THREE.Euler( Math.PI/4,0, Math.PI/2);
    
  • 创建一个欧拉角表示特定旋转角度

    //创建一个欧拉角对象,表示绕x轴旋转60度
    const Euler = new THREE.Euler();
    Euler.x = Math.PI / 3;
    Euler.y = Math.PI / 3;//绕y轴旋转60度
    Euler.z = Math.PI / 3;//绕z轴旋转60度
    
  • 欧拉角改变物体姿态角度(.rotation属性)

    threejs模型对象都有一个角度属性.rotation,.rotation的值其实就是欧拉角对象Euler。你可以改变.rotation对应欧拉角x、y或z属性值,查看物体姿态角度变化。

    // 物体fly绕x轴旋转60度
    fly.rotation.x = Math.PI / 3;
    const Euler = new THREE.Euler();
    Euler.x = Math.PI / 3;
    // 复制欧拉角的值,赋值给物体的.rotation属性
    fly.rotation.copy(Euler);
    
  • 物体旋转顺序.order

    物体先后绕x、y、z轴旋转,旋转的顺序不同,物体的姿态角度也可能不同。

    欧拉角对象的.order属性是用来定义旋转顺序的,也就是说你同时设置欧拉对象的x、y、z三个属性,在旋转的时候,先绕哪个轴,后绕那个轴旋转。

    下面两段代码,欧拉角xyz属性是一样的,区别是.order表示的旋转顺序不同,你可以对比不同旋转顺序,物体旋转后姿态角度是否相同。

    const Euler = new THREE.Euler();
    Euler.x = Math.PI / 3;
    Euler.y = Math.PI / 3;
    //先绕X轴旋转,在绕Y、Z轴旋转
    Euler.order = 'XYZ';
    fly.rotation.copy(Euler);
    
    const Euler = new THREE.Euler();
    Euler.x = Math.PI / 3;
    Euler.y = Math.PI / 3;
    //先绕Y轴旋转,在绕X、Z轴旋转
    Euler.order = 'YXZ';
    fly.rotation.copy(Euler);
    
    // 直接修改.rotation.order,和上面代码一样功能。
    fly.rotation.order = 'YXZ';
    fly.rotation.x = Math.PI / 3;
    fly.rotation.y = Math.PI / 3;
    

四元数Quaternion

四元数Quaternion和欧拉角Euler一样,可以用来计算或表示物体在3D空间中的旋转姿态角度。

Three.js对四元数的数学细节和算法进行了封装,提供了一个四元数相关的类,平时写一些姿态角度的代码,可以使用Quaternion辅助。本节课,咱们就结合具体的threejs代码科普这个抽象的四元数概念,有了具体代码辅助,这样更容易使用四元数表示物体的姿态角度。

  • 实例化Quaternion

    const quaternion = new THREE.Quaternion();
    
  • 四元数方法.setFromAxisAngle()

    .setFromAxisAngle()是四元数的一个方法,可以用来辅助生成表示特定旋转的四元数。

    .setFromAxisAngle(axis, angle)生成的四元数表示绕axis旋转,旋转角度是angle。

    .setFromAxisAngle()可以生成一个四元数,绕任意轴,旋转任意角度,并不局限于x、y、z轴。

    const quaternion = new THREE.Quaternion();
    // 旋转轴new THREE.Vector3(0,0,1)
    // 旋转角度Math.PI/2
    // 绕z轴旋转90度
    quaternion.setFromAxisAngle(new THREE.Vector3(0,0,1),Math.PI/2);
    
  • 四元数旋转A点坐标

    threejs三维向量Vector3具有一个方法.applyQuaternion(quaternion),该方法的功能就是通过参数quaternion对Vector3进行旋转,比如Vector3表示A点的xyz坐标,执行A.applyQuaternion(quaternion),相当于通过quaternion表示的四元数旋转A。

    // A表示3D空间一个点的位置坐标
    const A = new THREE.Vector3(30, 0, 0);
    // 黄色小球可视化坐标点A 
    const Amesh = createSphereMesh(0xffff00,2);
    Amesh.position.copy(A);
    group.add(Amesh);
    // 创建小球mesh
    function createSphereMesh(color,R) {
        const geometry = new THREE.SphereGeometry(R);
        const material = new THREE.MeshLambertMaterial({
            color: color,
        });
        const mesh = new THREE.Mesh(geometry, material);
        return mesh;
    }
    
    const quaternion = new THREE.Quaternion();
    // 绕z轴旋转90度
    quaternion.setFromAxisAngle(new THREE.Vector3(0,0,1),Math.PI/2);
    // 通过四元数旋转A点:把A点绕z轴旋转90度生成一个新的坐标点B
    const B = A.clone().applyQuaternion(quaternion);
    console.log('B',B);//查看旋转后坐标
    
    // 红色小球可视化坐标点B 
    const Bmesh = createSphereMesh(0xff0000,2);
    Bmesh.position.copy(B);
    group.add(Bmesh);
    

    three.js模型对象的角度.rotation和四元数.quaternion属性都是用来表示物体姿态角度的,只是表达形式不同而已,.rotation和.quaternion两个属性的值,一个改变,另一个也会同步改变。

    const quaternion = new THREE.Quaternion();
    quaternion.setFromAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI / 2);
    fly.quaternion.copy(quaternion);
    // 四元数属性改变后,查看角度属性(欧拉角)变化
    // .quaternion改变,.rotation同步改变
    console.log('角度属性',fly.rotation.z);
    
  • 四元数乘法.multiply()

    对象的一个旋转可以用一个四元数表示,两次连续旋转可以理解为两次旋转对应的四元数对象进行乘法运算。

    // 在物体原来姿态基础上,进行旋转
    const q1 = new THREE.Quaternion();
    q1.setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.PI / 2);
    fly.quaternion.multiply(q1);
    // 在物体上次旋转基础上,进行旋转
    const q2 = new THREE.Quaternion();
    q2.setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2);
    fly.quaternion.multiply(q2);
    // 在物体上次旋转基础上,进行旋转
    const q3 = new THREE.Quaternion();
    q3.setFromAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI / 2);
    fly.quaternion.multiply(q3);
    

    四元数乘法不满足交换律

    const q1 = new THREE.Quaternion();
    q1.setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.PI / 2);
    const q2 = new THREE.Quaternion();
    q2.setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2);
    const newQ= q1.clone().multiply(q2);
    fly.quaternion.multiply(newQ);
    
    // 先变换q2,后变换q1,和上面代码效果不一样,
    // q2.clone().multiply(q1)与q1.clone().multiply(q2)表示的旋转过程顺序不同
    const q1 = new THREE.Quaternion();
    q1.setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.PI / 2);
    const q2 = new THREE.Quaternion();
    q2.setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2);
    const newQ= q2.clone().multiply(q1);
    fly.quaternion.multiply(newQ);
    

    A.multiply(B)表示A乘以B,结果赋值给A,在A的基础上旋转B。

    A.copy(B)表示用B的值替换A的值,A表示的旋转会被B替换。

    //可以先通过欧拉角改变物体的姿态,先物体一个初始的角度状态。
    //改变物体欧拉角,四元数属性也会同步改变
    fly.rotation.x = Math.PI/2;
    //创建一个四元数表示一个旋转过程。
    
    const quaternion = new THREE.Quaternion();
    quaternion.setFromAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI / 2);
    //执行fly.quaternion.copy(quaternion),参数quaternion表示的旋转会完全覆盖已有的旋转fly.quaternion。无论物体原来的姿态角度是什么样,都会被参数quaternion表示新的姿态角度覆盖。
    
    //quaternion表示旋转角度复制给物体.quaternion
    fly.quaternion.copy(quaternion);
    .quaternion.multiply(quaternion)表示在自身已有旋转的基础上,增加参数quaternion表示的旋转。
    
    fly.quaternion.multiply(quaternion);
    
  • 欧拉、四元数和矩阵转化

    欧拉对象、四元数对象和旋转矩阵可以相关转化,都可以表示旋转变换。

    Matrix4.makeRotationFromQuaternion(q)方法可以把四元数转化对应的矩阵对象。

    quaternion.setFromEuler(Euler)通过欧拉对象设置四元数对象

    Euler.setFromQuaternion(quaternion)四元数转化为欧拉对象

  • Object3D

    Object3D对象角度属性.rotation的值是欧拉对象Euler,四元数属性.quaternion的值是四元数对象Quaternion。

    执行Object3D对象旋转方法,会同时改变对象的角度属性和四元数属性。四元数属性和位置.position、缩放属性.scale一样会转化为对象的本地矩阵属性.matrix,本地矩阵属性值包含了旋转矩阵、缩放矩阵、平移矩阵。

    // 一个网格模型对象,基类是Object3D
    var mesh = new THREE.Mesh()
    // 绕z轴旋转
    mesh.rotateZ(Math.PI)
    
    console.log('查看角度属性rotation',mesh.rotation);
    console.log('查看四元数属性quaternion',mesh.quaternion);
    

    .rotateOnAxis(axis, angle)表示绕绕着任意方向某个轴axis旋转一定角度angle,绕X、Y和Z轴旋转对应的方法分别是rotateX()、rotateY()和rotateZ(),绕着XYZ特定轴旋转的方法是基于.rotateOnAxis()方法实现的。

    // Object3D.js源码
    rotateOnAxis: function () {
        var q1 = new Quaternion();
        // 旋转轴axis,旋转角度angle
        return function rotateOnAxis( axis, angle ) {
            // 通过旋转轴和旋转角度设置四元数的xyzw分量
            q1.setFromAxisAngle( axis, angle );
            // Object3D对象的四元数属性和四元数q1相乘
            this.quaternion.multiply( q1 );
            return this;
        };
    }()
    

四元数:表示两个向量旋转

3D空间中有两个向量,一个向量向另外一个向量旋转,这个过程可以用一个四元数表示。

已知飞行原来的飞行方向是a表示的方向,需要把飞机飞行方向旋转到向量b表示的方向。

const model = new THREE.Group();
loader.load("../飞机.glb", function (gltf) {
    const fly = gltf.scene
    model.add(fly);
    fly.position.set(10, 10, 0);//相对世界坐标系坐标原点偏移
    const axesHelper = new THREE.AxesHelper(10);
    fly.add(axesHelper);//用一个坐标轴可视化模型的局部坐标系(本地坐标系)

    const a = new THREE.Vector3(0, 0, -1);//飞机初始姿态飞行方向
    // 飞机姿态绕自身坐标原点旋转到b指向的方向
    const b = new THREE.Vector3(-1, -1, -1).normalize();

})

//箭头可视化飞机旋转前后的方向
// 可视化飞机方向
const a = new THREE.Vector3(0, 0, -1);//飞机初始姿态飞行方向
const O = fly.position.clone();//飞机位置坐标箭头起点
model.add(new THREE.ArrowHelper(a, O, 30, 0xff0000));
// 飞机姿态绕自身坐标原点旋转到b指向的方向
const b = new THREE.Vector3(-1, -1, -1).normalize();
model.add(new THREE.ArrowHelper(b, O, 30, 0x00ff00));

//.setFromUnitVectors(a, b)生成四元数旋转飞机
//四元数Quaternion的方法.setFromUnitVectors(a, b)可以通过两个向量参数a和b,创建一个四元数,表示从向量a表示的方向旋转到向量b表示的方向。(参数a, b是单位向量)
//飞机初始姿态飞行方向a
const a = new THREE.Vector3(0, 0, -1);
// 飞机姿态绕自身坐标原点旋转到b指向的方向
const b = new THREE.Vector3(-1, -1, -1).normalize();
// a旋转到b构成的四元数
const quaternion = new THREE.Quaternion();
//注意两个参数的顺序
quaternion.setFromUnitVectors(a, b);
// quaternion表示的是变化过程,在原来基础上乘以quaternion即可
fly.quaternion.multiply(quaternion);

矩阵

平移、旋转、缩放矩阵

矩阵乘法运算规则

平移矩阵

缩放矩阵

绕Z轴旋转γ角度

绕X轴旋转α角度

绕Y轴旋转β角度

  • 快速生成平移、旋转、缩放矩阵

    使用threejs平移矩阵、旋转矩阵、缩放矩阵,可以不用自己直接设置.elements的值。threejs提供了一些更为简单的方法,辅助创建各种几何变换矩阵。

    • 平移矩阵.makeTranslation(Tx,Ty,Tz)
    • 缩放矩阵.makeScale(Sx,Sy,Sz)
    • 绕x轴的旋转矩阵.makeRotationX(angleX)
    • 绕y轴的旋转矩阵.makeRotationY(angleY)
    • 绕z轴的旋转矩阵.makeRotationZ(angleZ)
    const mat4 = new THREE.Matrix4();
    // 生成平移矩阵(沿着x轴平移50)
    mat4.makeTranslation(50,0,0);
    // 结果和.elements=[1,0,0,0,...... 50, 0, 0, 1]一样
    console.log('查看矩阵的值',mat4.elements);
    //生成绕z轴旋转90度的矩阵
    mat4.makeRotationZ(Math.PI/2);
    

模型矩阵

模型矩阵就是平移矩阵、旋转矩阵、缩放矩阵的统称,或者说模型矩阵是平移、缩放、旋转矩阵相乘得到的复合矩阵。

假设一个顶点原始坐标(2,0,0)。

先平移2、后缩放10:如果先沿着x轴平移2,变为(4,0,0),再x轴方向缩放10倍,最终坐标是(40,0,0)。

先缩放10、后平移2:如果先x轴方向缩放10倍,变为(20,0,0),再沿着x轴平移2,最终坐标是(22,0,0)。

你可以发现上面同样的平移和缩放,顺序不同,变换后的顶点坐标也不相同。

先平移、后缩放

先缩放、后平移

矩阵乘法顺序一般不满足交换律,R.clone().multiply(T)和T.clone().multiply(R)表示的结果不同,也就是R * T和T * R计算结果不同。

// R * T * p:先平移、后旋转
const modelMatrix = R.clone().multiply(T);
p.applyMatrix4(modelMatrix);
// T * R * p:先旋转、后平移
const modelMatrix = R.clone().multiply(T);
p.applyMatrix4(modelMatrix);

单位矩阵

单位矩阵就是对角线上都为1,其它为0的矩阵。

单位矩阵乘其它矩阵,或者其它矩阵成单位矩阵,新矩阵都和其它矩阵一样,不受范围矩阵影响,单位矩阵有点类似自然数加减乘除的1。

单位矩阵

单位矩阵

模型本地矩阵、世界矩阵

模型对象的父类Object3D,你可以看到本地矩阵.matrix和世界矩阵.matrixWorld两个属性。

当你改变模型位置.position、缩放.scale或角度.rotation(.quaternion)任何一个属性的时候,都会影响.matrix的值。.matrix就是本质上就是旋转矩阵、缩放矩阵、平移矩阵的复合矩阵。

// 不执行renderer.render(scene, camera);情况下测试
mesh.position.set(2,3,4);
mesh.updateMatrix();//更新矩阵,.matrix会变化
console.log('本地矩阵',mesh.matrix);
mesh.scale.set(6,6,6);
mesh.updateMatrix();
console.log('本地矩阵',mesh.matrix);
mesh.position.set(2,3,4);
mesh.scale.set(6,6,6);
mesh.updateMatrix();
console.log('本地矩阵',mesh.matrix);

本地矩阵

位置属性.position表示一个模型相对父对象的偏移,或者说相对本地坐标系的位置。

通过.getWorldPosition()获取的世界坐标,是模型相对世界坐标系的坐标,也就是该对象及其父对象所有.position属性值的累加。

console.log('本地坐标',mesh.position);
const worldPosition = new THREE.Vector3();
mesh.getWorldPosition(worldPosition)
console.log('世界坐标',worldPosition);

世界矩阵

当你改变.position、.scale等属性,不执行.updateMatrixWorld()更新矩阵矩阵,在.render之后查看本地矩阵和世界矩阵的值,你会发现发生了变化。这说明three.js默认情况下,在执行.render()的时候,会自动获取.position、.scale等属性的值,更新模型的本地矩阵、世界矩阵属性。

const scene = new THREE.Scene();
mesh.position.set(2,3,4);
const group = new THREE.Group();
group.position.set(2,3,4);
group.add(mesh);
scene.add(group);

// render渲染时候,会获取模型`.position`等属性更新计算模型矩阵值
renderer.render(scene, camera);

console.log('本地矩阵',mesh.matrix);
console.log('世界矩阵',mesh.matrixWorld);

空文件

简介

three.js Opanorama.js canvas css3d FBXLoader.js GLTFLoader.js OrbitControls.js Detector.js stats.js 展开 收起
取消

发行版

暂无发行版

贡献者 (3)

全部

近期动态

不能加载更多了
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
JavaScript
1
https://gitee.com/wu_jianyong/web3DExample.git
git@gitee.com:wu_jianyong/web3DExample.git
wu_jianyong
web3DExample
web3DExample
master

搜索帮助