Three 之 three.js (webgl)透视视角和正交视角,以及透视转正交的视角切换

Three 之 three.js (webgl)透视视角和正交视角,以及透视转正交的视角切换

目录

Three 之 three.js (webgl)透视视角和正交视角,以及透视转正交的视角切换

一、简单介绍

二、实现原理

三、正投影和透视投影简单解释

 四、透视相机(PerspectiveCamera)

五、正交相机(OrthographicCamera)

六、threejs中透视投影相机转换正交投影相机基本原理与代码实现

七、案例实现

八、案例代码下载

扫描二维码关注公众号,回复: 14133828 查看本文章

一、简单介绍

Three js 开发的一些知识整理,方便后期遇到类似的问题,能够及时查阅使用。

本节介绍, three.js (webgl)透视视角和正交视角,并且实现简单把当前透视角转为正交视角,然后在切换回来的透视视角的原理案例,如果有不足之处,欢迎指出,或者你有更好的方法,欢迎留言。
 

二、实现原理

1、场景构建三要素,scene、camera 和 renderer

2、其中 camera ,会根据需要先创建一个透视camera,然后根据转换,切换到正交camera

3、然后通过 renderer 中 透视camera 或者 正交camera 视角渲染,从而实现透视视角转为正交视角 view 的渲染

三、正投影和透视投影简单解释

下面对正投影相机和透视投影相机的投影算法进行简单介绍,对于初学者你有一个印象就可以,如果想深入了解,可以学习图形学或者阅读threejs官方源码src目录下文件OrthographicCamera.jsPerspectiveCamera.js

生活中的物体都是三维的,但是人的眼睛只能看到正面,不能看到被遮挡的背面,三维几何体在人眼睛中的效果就像一张相机拍摄的二维照片,你看到的是一个2D的投影图。 空间几何体转化为一个二维图的过程就是投影,不同的投影方式意味着投影尺寸不同的算法。

  • 对于正投影而言,一条直线放置的角度不同,投影在投影面上面的长短不同;
  • 对于透视投影而言,投影的结果除了与几何体的角度有关,还和距离相关, 人的眼睛观察世界就是透视投影,比如你观察一条铁路距离越远你会感到两条轨道之间的宽度越小。

无论正投影还是透视投影,three.js都对相关的投影算法进行了封装, 大家只需要根据不同的应用场景自行选择不同的投影方式。使用OrthographicCamera相机对象的时候,three.js会按照正投影算法自动计算几何体的投影结果; 使用PerspectiveCamera相机对象的时候,three.js会按照透视投影算法自动计算几何体的投影结果。

 四、透视相机(PerspectiveCamera)

这一投影模式被用来模拟人眼所看到的景象,它是3D场景的渲染中使用得最普遍的投影模式。

1、构造 透视相机

 new THREE.PerspectiveCamera(60, width / height, 1, 1000)

/**
 * 透视投影相机设置
 */
var width = window.innerWidth; //窗口宽度
var height = window.innerHeight; //窗口高度
/**透视投影相机对象*/
var camera = new THREE.PerspectiveCamera(60, width / height, 1, 1000);
camera.position.set(200, 300, 200); //设置相机位置
camera.lookAt(scene.position); //设置相机方向(指向的场景对象)
参数 含义 默认值
fov fov表示视场,所谓视场就是能够看到的角度范围,人的眼睛大约能够看到180度的视场,视角大小设置要根据具体应用,一般游戏会设置60~90度 45
aspect aspect表示渲染窗口的长宽比,如果一个网页上只有一个全屏的canvas画布且画布上只有一个窗口,那么aspect的值就是网页窗口客户区的宽高比 window.innerWidth/window.innerHeight
near near属性表示的是从距离相机多远的位置开始渲染,一般情况会设置一个很小的值。 0.1
far far属性表示的是距离相机多远的位置截止渲染,如果设置的值偏小,会有部分场景看不到 1000

2、属性

共有属性请参见其基类 Camera 。
请注意,在大多数属性发生改变之后,你将需要调用.updateProjectionMatrix来使得这些改变生效。

.aspect : Float

摄像机视锥体的长宽比,通常是使用画布的宽/画布的高。默认值是1(正方形画布)。

.far : Float

摄像机的远端面,默认值是2000

该值必须大于near plane(摄像机视锥体近端面)的值。

.filmGauge : Float

胶片尺寸,其默认值为35(毫米)。 这个参数不会影响摄像机的投影矩阵,除非.filmOffset被设置为了一个非零的值。

.filmOffset : Float

水平偏离中心偏移量,和.filmGauge单位相同。默认值为0

.focus : Float

用于立体视觉和景深效果的物体的距离。 这个参数不会影响摄像机的投影矩阵,除非使用了StereoCamera。 默认值是10

.fov : Float

摄像机视锥体垂直视野角度,从视图的底部到顶部,以角度来表示。默认值是50

.isPerspectiveCamera : Boolean

Read-only flag to check if a given object is of type PerspectiveCamera.

.near : Float

摄像机的近端面,默认值是0.1

其有效值范围是0到当前摄像机far plane(远端面)的值之间。 请注意,和OrthographicCamera不同,0对于PerspectiveCamera的近端面来说不是一个有效值。

.view : Object

Frustum window specification or null. 这个值使用.setViewOffset方法来进行设置,使用.clearViewOffset方法来进行清除。

.zoom : number

获取或者设置摄像机的缩放倍数,其默认值为1

3、方法

共有方法请参见其基类Camera。

.clearViewOffset () : undefined

清除任何由.setViewOffset设置的偏移量。

.getEffectiveFOV () : Float

结合.zoom(缩放倍数),以角度返回当前垂直视野角度。

.getFilmHeight () : Float

返回当前胶片上图像的高,如果.aspect小于或等于1(肖像格式、纵向构图),则结果等于.filmGauge。

.getFilmWidth () : Float

返回当前胶片上图像的宽,如果.aspect大于或等于1(景观格式、横向构图),则结果等于.filmGauge。

.getFocalLength () : Float

返回当前.fov(视野角度)相对于.filmGauge(胶片尺寸)的焦距。

.setFocalLength ( focalLength : Float ) : undefined

通过相对于当前.filmGauge的焦距,设置FOV。

默认情况下,焦距是为35mm(全画幅)摄像机而指定的。

.setViewOffset ( fullWidth : Float, fullHeight : Float, x : Float, y : Float, width : Float, height : Float ) : undefined

fullWidth — 多视图的全宽设置
fullHeight — 多视图的全高设置
x — 副摄像机的水平偏移
y — 副摄像机的垂直偏移
width — 副摄像机的宽度
height — 副摄像机的高度

在较大的viewing frustum(视锥体)中设置偏移量,对于多窗口或者多显示器的设置是很有用的。

五、正交相机(OrthographicCamera)

这种投影模式下,无论物体距离相机距离远或者近,在最终渲染的图片中物体的大小都保持不变。
这对于渲染2D场景或者UI元素是非常有用的。

1、构造 正交相机

OrthographicCamera( left : Number, right : Number, top : Number, bottom : Number, near : Number, far : Number )

**
 * 正投影相机设置
 */
var width = window.innerWidth; //窗口宽度
var height = window.innerHeight; //窗口高度
var k = width / height; //窗口宽高比
var s = 150; //三维场景显示范围控制系数,系数越大,显示的范围越大
//创建相机对象
var camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
camera.position.set(200, 300, 200); //设置相机位置
camera.lookAt(scene.position); //设置相机方向(指向的场景对象)
参数(属性) 含义
left 渲染空间的左边界
right 渲染空间的右边界
top 渲染空间的上边界
bottom 渲染空间的下边界
near near属性表示的是从距离相机多远的位置开始渲染,一般情况会设置一个很小的值。 默认值0.1
far far属性表示的是距离相机多远的位置截止渲染,如果设置的值偏小小,会有部分场景看不到。 默认值1000

2、属性

共有属性请参见其基类Camera。
请注意,在大多数属性发生改变之后,你将需要调用.updateProjectionMatrix来使得这些改变生效。

.bottom : Float

摄像机视锥体下侧面。

.far : Float

摄像机视锥体远端面,其默认值为2000

该值必须大于near plane(摄像机视锥体近端面)的值。

.isOrthographicCamera : Boolean

Read-only flag to check if a given object is of type OrthographicCamera.

.left : Float

摄像机视锥体左侧面。

.near : Float

摄像机视锥体近端面。其默认值为0.1.

其值的有效范围介于0和far(摄像机视锥体远端面)之间。
请注意,和PerspectiveCamera不同,0对于OrthographicCamera的近端面来说是一个有效值。

.right : Float

摄像机视锥体右侧面。

.top : Float

摄像机视锥体上侧面。

.view : Object

这个值是由setViewOffset来设置的,其默认值为null

.zoom : number

获取或者设置摄像机的缩放倍数,其默认值为1

3、方法

共有方法请参见其基类Camera。

.setViewOffset ( fullWidth : Float, fullHeight : Float, x : Float, y : Float, width : Float, height : Float ) : undefined

fullWidth — 多视图的全宽设置
fullHeight — 多视图的全高设置
x — 副摄像机的水平偏移
y — 副摄像机的垂直偏移
width — 副摄像机的宽度
height — 副摄像机的高度

在较大的viewing frustum(视锥体)中设置偏移量,对于多窗口或者多显示器的设置是很有用的。 对于如何使用它,请查看PerspectiveCamera中的示例。

.clearViewOffset () : undefined

清除任何由.setViewOffset设置的偏移量。

.updateProjectionMatrix () : undefined

更新摄像机投影矩阵。在任何参数被改变以后必须被调用。

.toJSON (meta : Object) : Object

meta -- 包含有元数据的对象,例如对象后代中的纹理或图像
将摄像机转换为 three.js JSON Object/Scene format(three.js JSON 物体/场景格式)。

三维场景中坐标值不在三维空间中的网格模型不会被渲染出来,会被剪裁掉,比如你把上面代码中far参数的值从1000更改为420,你会发现长方体的一部分无法显示。

注意:左右边界的距离与上下边界的距离比值与画布的渲染窗口的宽高比例要一致,否则三维模型的显示效果会被单方向不等比例拉伸

构造函数OrthographicCamera的参数( left,right,top,bottom,near,far)本质上是对WebGL投影矩阵的封装,宽度width、高度height越大,三维模型顶点的位置坐标就会越大,超出可视区域的网格模型就会被剪裁掉, 不会再显示在屏幕上,大家还可以看到参数leftright、参数topbottom互为相反数,这样做的目的是lookAt指向的对象能够显示在canvas画布的中间位置。

六、threejs中透视投影相机转换正交投影相机基本原理与代码实现

这里,假设已知透视投影相机,threejs中透视投影相机转换正交投影相机中,最重要的是根据已知透视相机的属性计算所需构造正交投影的量,即左右上下远近平面,远近平面已知(既可以使用 透视相机的远近平面),这是,就剩下计算左右和上下平面。

可以简单认为,所需的左右上下平面就是远平面和相机fov构成的矩形,如下图所示。

计算方法是得到当前透视相机到远平面的深度 depth,然后根据 fov 角度计算矩形的 top,在根据 透视相机的宽高比得到 right,再根据 top 和 bottom,right 和 left 成正反关系,最后得到所需正交相机的构造所需参数。

            //1.计算透视相机到场景 scene 的深度距离 depth
            let target = scene.position.clone();;
            let camPos = perCamera.position.clone();
            let depth = camPos.sub(target).length();

            //2.得到透视相机的宽高比和垂直可视角度
            let aspect = perCamera.aspect;
            let fov = perCamera.fov;

            //3.根据上述变量计算正交投影相机的视口矩形
            let top_ortho = depth  * Math.atan( (Math.PI/180)*(fov)/2);
            let right_ortho = top_ortho * aspect;
            let bottom_ortho = - top_ortho;
            let left_ortho = - right_ortho;


            //4.最后创建正交投影相机
            let near = perCamera.near;
            let far = perCamera.far;
            orthCamera = new THREE.OrthographicCamera(
                    left_ortho , right_ortho ,
                    top_ortho , bottom_ortho ,
                    near, far
            );

七、案例实现

1、引入 Three js 相关 js 文件

<script type="importmap">
			{
				"imports": {
					"three": "./js/three.module.js"
				}
			}
</script>
<script type="module">

    import * as THREE from 'three';

    import { OrbitControls } from './js/OrbitControls.js';
    import { RGBELoader } from './js/RGBELoader.js';
    import { GUI } from './js/lil-gui.module.min.js'

2、初始化 scene、renderer 、 Lights ,并且加 3D 物体

    function initScene()
    {

        // 场景,背景色
        scene = new THREE.Scene();
        scene.background = new THREE.Color( 0xcccccc );
        // 这是关键,包含了光照信息的环境 hdr 图,(因为没有添加灯光,少了他,场景就是黑色的)
        scene.environment = new RGBELoader().load( './src/venice_sunset_1k.hdr' );
        scene.environment.mapping = THREE.EquirectangularReflectionMapping;
    }

    function initRenderer()
    {
        const container = document.getElementById( 'app' );

        // 渲染器
        renderer = new THREE.WebGLRenderer( { antialias: true } );
        renderer.setPixelRatio( window.devicePixelRatio );
        renderer.setSize( window.innerWidth, window.innerHeight );
        renderer.outputEncoding = THREE.sRGBEncoding;
        renderer.toneMapping = THREE.ACESFilmicToneMapping;
        renderer.toneMappingExposure = 0.85;
        container.appendChild( renderer.domElement );
    }


    function addLights(){
        scene.add(new THREE.AmbientLight(0xffffff, 1));

        const hemiLight = new THREE.HemisphereLight(0xffffff, 0x000000, 1);
        hemiLight.position.set(0, 100, 0);
        scene.add(hemiLight);

    }


    function addObject3Ds(){
        const material1 =  new THREE.MeshStandardMaterial( {
            color: 0xff0000, metalness: 0.0, roughness: 0.5
        } );
        const mesh1 = new THREE.Mesh(new THREE.BoxGeometry(10,5,5),material1);
        mesh1.castShadow =true;
        mesh1.receiveShadow =true;
        scene.add(mesh1);

        const material2 = new THREE.MeshStandardMaterial({color:0xff00cc,metalness:0.0,roughness:0.1});
        const mesh2 = new THREE.Mesh(new THREE.BoxGeometry(5,5,5),material2);
        mesh2.position.set(0,5,2);
        scene.add(mesh2);

        const material3 = new THREE.MeshStandardMaterial({color:0xffcc00,metalness:0.1,roughness:0.1});
        const mesh3 = new THREE.Mesh(new THREE.SphereGeometry(5,30),material3);
        mesh3.position.set(0,5,-2);
        scene.add(mesh3);

    }

3、初始化相机camera,默认创建透视相机,把透视相机作为当前相机,在切换相机的时候创建正交相机作为当前相机

    function initCamera(){
        curCamera = createPerCamera();
    }

    function createPerCamera(){

        if(perCamera == null){
            perCamera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 200);
            perCamera.position.set (-10,4, 25);
        }

        return perCamera;
    }

    function createPerCameraToOrthCamera(perCamera){

        if(perCamera == null){
            console.error(' perCamera is null');
            return null;
        }

        if(orthCamera == null){
            //1.计算透视相机到场景 scene 的深度距离 depth
            let target = scene.position.clone();;
            let camPos = perCamera.position.clone();
            let depth = camPos.sub(target).length();

            //2.得到透视相机的宽高比和垂直可视角度
            let aspect = perCamera.aspect;
            let fov = perCamera.fov;

            //3.根据上述变量计算正交投影相机的视口矩形
            let top_ortho = depth  * Math.atan( (Math.PI/180)*(fov)/2);
            let right_ortho = top_ortho * aspect;
            let bottom_ortho = - top_ortho;
            let left_ortho = - right_ortho;


            //4.最后创建正交投影相机
            let near = perCamera.near;
            let far = perCamera.far;
            orthCamera = new THREE.OrthographicCamera(
                    left_ortho , right_ortho ,
                    top_ortho , bottom_ortho ,
                    near, far
            );
        }
        return orthCamera;
    }

4、创建 orbitControls 相机控制器

注意 :在 透视相机的时候才能漫游观察,正交相机是不能漫游观察

    function initOrbitControls(){
        const  controls = new OrbitControls(curCamera, renderer.domElement);
        controls.minDistance = 5;
        controls.maxDistance = 50;
        controls.enablePan = false; // 禁止 移动操作
    }

5、添加GUI 操作面板,进行透视和正交相机的切换

    function addGUI(){
        const param = {
            "透视转正交":orthCameraView,
            "透视转正交俯视":orthCameraLookDownView,
            "正交转透视":orthCameraBackPerCamera,

        }

        const gui = new GUI();
        gui.add(param,"透视转正交");
        gui.add(param,"透视转正交俯视");
        gui.add(param,"正交转透视");
    }

6、效果预览

7、完整代码

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width,initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
    <title>PersCameraToOrthCamera</title>
    <style type="text/css">
        * {
            margin: 0px;
            padding: 0px;
        }
    </style>
</head>
<body>
<div id="app">

</div>

<script type="importmap">
			{
				"imports": {
					"three": "./js/three.module.js"
				}
			}
</script>
<script type="module">

    import * as THREE from 'three';

    import { OrbitControls } from './js/OrbitControls.js';
    import { RGBELoader } from './js/RGBELoader.js';
    import { GUI } from './js/lil-gui.module.min.js'

    // 当前的相机,透视相机,正交相机
    let curCamera,perCamera,orthCamera
    let scene, renderer;

    init();
    animate();

    function init(){
        initScene();
        initRenderer();
        initCamera();
        addLights();
        initOrbitControls();
        addObject3Ds();
        addGUI();
    }

    function initScene()
    {

        // 场景,背景色
        scene = new THREE.Scene();
        scene.background = new THREE.Color( 0xcccccc );
        // 这是关键,包含了光照信息的环境 hdr 图,(因为没有添加灯光,少了他,场景就是黑色的)
        scene.environment = new RGBELoader().load( './src/venice_sunset_1k.hdr' );
        scene.environment.mapping = THREE.EquirectangularReflectionMapping;
    }

    function initRenderer()
    {
        const container = document.getElementById( 'app' );

        // 渲染器
        renderer = new THREE.WebGLRenderer( { antialias: true } );
        renderer.setPixelRatio( window.devicePixelRatio );
        renderer.setSize( window.innerWidth, window.innerHeight );
        renderer.outputEncoding = THREE.sRGBEncoding;
        renderer.toneMapping = THREE.ACESFilmicToneMapping;
        renderer.toneMappingExposure = 0.85;
        container.appendChild( renderer.domElement );
    }

    function initCamera(){
        curCamera = createPerCamera();
    }

    function createPerCamera(){

        if(perCamera == null){
            perCamera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 200);
            perCamera.position.set (-10,4, 25);
        }

        return perCamera;
    }

    function createPerCameraToOrthCamera(perCamera){

        if(perCamera == null){
            console.error(' perCamera is null');
            return null;
        }

        if(orthCamera == null){
            //1.计算透视相机到场景 scene 的深度距离 depth
            let target = scene.position.clone();;
            let camPos = perCamera.position.clone();
            let depth = camPos.sub(target).length();

            //2.得到透视相机的宽高比和垂直可视角度
            let aspect = perCamera.aspect;
            let fov = perCamera.fov;

            //3.根据上述变量计算正交投影相机的视口矩形
            let top_ortho = depth  * Math.atan( (Math.PI/180)*(fov)/2);
            let right_ortho = top_ortho * aspect;
            let bottom_ortho = - top_ortho;
            let left_ortho = - right_ortho;


            //4.最后创建正交投影相机
            let near = perCamera.near;
            let far = perCamera.far;
            orthCamera = new THREE.OrthographicCamera(
                    left_ortho , right_ortho ,
                    top_ortho , bottom_ortho ,
                    near, far
            );
        }
        return orthCamera;
    }

    function addLights(){
        scene.add(new THREE.AmbientLight(0xffffff, 1));

        const hemiLight = new THREE.HemisphereLight(0xffffff, 0x000000, 1);
        hemiLight.position.set(0, 100, 0);
        scene.add(hemiLight);

    }

    function initOrbitControls(){
        const  controls = new OrbitControls(curCamera, renderer.domElement);
        controls.minDistance = 5;
        controls.maxDistance = 50;
        controls.enablePan = false; // 禁止 移动操作
    }

    function addObject3Ds(){
        const material1 =  new THREE.MeshStandardMaterial( {
            color: 0xff0000, metalness: 0.0, roughness: 0.5
        } );
        const mesh1 = new THREE.Mesh(new THREE.BoxGeometry(10,5,5),material1);
        mesh1.castShadow =true;
        mesh1.receiveShadow =true;
        scene.add(mesh1);

        const material2 = new THREE.MeshStandardMaterial({color:0xff00cc,metalness:0.0,roughness:0.1});
        const mesh2 = new THREE.Mesh(new THREE.BoxGeometry(5,5,5),material2);
        mesh2.position.set(0,5,2);
        scene.add(mesh2);

        const material3 = new THREE.MeshStandardMaterial({color:0xffcc00,metalness:0.1,roughness:0.1});
        const mesh3 = new THREE.Mesh(new THREE.SphereGeometry(5,30),material3);
        mesh3.position.set(0,5,-2);
        scene.add(mesh3);

    }

    function addGUI(){
        const param = {
            "透视转正交":orthCameraView,
            "透视转正交俯视":orthCameraLookDownView,
            "正交转透视":orthCameraBackPerCamera,

        }

        const gui = new GUI();
        gui.add(param,"透视转正交");
        gui.add(param,"透视转正交俯视");
        gui.add(param,"正交转透视");
    }

    function orthCameraView(){
        const  tmpCamera = createPerCameraToOrthCamera(perCamera);
        tmpCamera.position.set(perCamera.position.x,perCamera.position.y,perCamera.position.z);
        tmpCamera.rotation.set(perCamera.rotation.x,perCamera.rotation.y,perCamera.rotation.z);

        curCamera = tmpCamera;
    }

    function orthCameraLookDownView(){
        //1.计算透视相机到场景 scene 的深度距离 depth,作为 俯视的高度
        let target = scene.position.clone();;
        let camPos = perCamera.position.clone();
        let depth = camPos.sub(target).length();
        const  tmpCamera = createPerCameraToOrthCamera(perCamera);
        tmpCamera.position.set(0,depth,0);
        tmpCamera.rotation.set(- Math.PI/2, 0, 0);

        curCamera = tmpCamera;
    }

    function orthCameraBackPerCamera(){
        curCamera = createPerCamera();
    }

    function animate(){

        requestAnimationFrame(animate);

        render();
    }

    function render(){
        renderer.render(scene, curCamera);
    }


</script>
</body>
</html>

八、案例代码下载

Three之three.js(webgl)透视视角和正交视角,以及透视转正交的视角切换代码-Javascript文档类资源-CSDN下载

猜你喜欢

转载自blog.csdn.net/u014361280/article/details/124544320