【企业数字化转型】数据可视化技术:Three.js 用Physijs在场景中添加物理效果

2020-01-15 16:55:25 浏览数 (1)

Three.js 极简教程

简介

Three.JS 是什么

  • Three.JS是基于WebGL的Javascript开源框架,简言之,就是能够实现3D效果的JS库。

Three.JS 能做什么

  • 利用Three.JS可以制作出很多酷炫的3D动画,并且Three.js还可以通过鼠标、键盘、拖拽等事件形成交互,在页面上增加一些3D动画和3D交互可以产生更好的用户体验。通过Three.JS可以实现全景视图,这些全景视图应用在房产、家装行业能够带来更直观的视觉体验。在电商行业利用Three.JS可以实现产品的3D效果,这样用户就可以360度全方位地观察商品了,给用户带来更好的购物体验。另外,使用Three.JS还可以制作类似微信跳一跳那样的小游戏。随着技术的发展、基础网络的建设,web3D技术还能得到更广泛的应用。

WebGL

  • WebGL是一种Javascript的3D图形接口,把JavaScript和OpenGL ES 2.0(OpenGL for Embedded Systems)结合在一起。

OpenGL

  • OpenGL是开放式图形标准,跨编程语言、跨平台,Javascript、Java 、C、C 、 python 等都能支持OpenG ,OpenGL的Javascript实现就是WebGL,另外很多CAD制图软件都采用这种标准。OpenGL ES 2.0是OpenGL的子集,针对手机、游戏主机等嵌入式设备而设计。

Canvas

  • Canvas是HTML5的画布元素,在使用Canvas时,需要用到Canvas的上下文,可以用2D上下文绘制二维的图像,也可以使用3D上下文绘制三维的图像,其中3D上下文就是指WebGL。

基础知识

主要组件(现实世界的抽象3D模型)

  • 场景(scene)
    • 场景是一个容器,可以看做摄影的房间,在房间中可以布置背景、摆放拍摄的物品、添加灯光设备等。
  • 相机(camera)
    • 相机是用来拍摄的工具,通过控制相机的位置和方向可以获取不同角度的图像。
    • 常用相机
      • 透视相机

透视相机模拟的效果与人眼看到的景象最接近,在3D场景中也使用得最普遍,这种相机最大的特点就是近大远小,同样大小的物体离相机近的在画面上显得大,离相机远的物体在画面上显得小。 PerspectiveCamera( fov : Number, aspect : Number, near : Number, far : Number )

fov — 摄像机视锥体垂直视野角度 aspect — 摄像机视锥体长宽比 near — 摄像机视锥体近端面 far — 摄像机视锥体远端面

  • 正交相机 使用正交相机时无论物体距离相机远或者近,在最终渲染的图片中物体的大小都保持不变。 OrthographicCamera( left : Number, right : Number, top : Number, bottom : Number, near : Number, far : Number )

left — 摄像机视锥体左侧面 right — 摄像机视锥体右侧面 top — 摄像机视锥体上侧面 bottom — 摄像机视锥体下侧面 near — 摄像机视锥体近端面 far — 摄像机视锥体远端面

  • 渲染器(renderer)
    • 渲染器利用场景和相机进行渲染,渲染过程好比摄影师拍摄图像,如果只渲染一次就是静态的图像,如果连续渲染就能得到动态的画面。在JS中可以使用requestAnimationFrame实现高效的连续渲染。

代码实例

  • 在Three.js中,要渲染物体到网页中,我们需要3个组建:场景(scene)、相机(camera)和渲染器(renderer)。有了这三样东西,才能将物体渲染到网页中去。
代码语言:javascript复制
var scene = new THREE.Scene();  // 场景
var camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);// 透视相机
var renderer = new THREE.WebGLRenderer();   // 渲染器
renderer.setSize(window.innerWidth, window.innerHeight);    // 设置渲染器的大小为窗口的内宽度,也就是内容区的宽度
document.body.appendChild(renderer.domElement);

进阶实战

组织网络协同图谱

源代码:

代码语言:javascript复制
<!DOCTYPE html>
<html>
<head>
    <style>
        body {
            /* set margin to 0 and overflow to hidden, to go fullscreen */
            margin: 0;
            overflow: hidden;
            background-color: #000000;
        }
    </style>

    <title>ANT</title>

    <script src="/libs/three.js" type="text/javascript"></script>
    <script src="/libs/stats.js" type="text/javascript"></script>
    <script src="/libs/physi.js" type="text/javascript"></script>
    <script src="/libs/dat.gui.js" type="text/javascript"></script>
    <script src="/libs/chroma.js" type="text/javascript"></script>
    <script src="/libs/perlin.js" type="text/javascript"></script>

    <script type="text/javascript">

        'use strict';

        Physijs.scripts.worker = '/libs/physijs_worker.js';
        Physijs.scripts.ammo = '/libs/ammo.js';

        var scale = chroma.scale(['green', 'red']);

        var initScene, render, meshes = [], lines = [],
            renderer, scene, light, camera;

        initScene = function () {
            renderer = new THREE.WebGLRenderer({antialias: true});
            renderer.setSize(window.innerWidth, window.innerHeight);

            renderer.setClearColor(new THREE.Color(0x000000));
            renderer.shadowMapEnabled = true;

            document.getElementById('viewport').appendChild(renderer.domElement);

            scene = new Physijs.Scene({reportSize: 20, fixedTimeStep: 1 / 60});

            scene.setGravity(new THREE.Vector3(0, 0, 0));

            camera = new THREE.PerspectiveCamera(
                35,
                window.innerWidth / window.innerHeight,
                1,
                1000
            );
            camera.position.set(105, 85, 85);
            camera.lookAt(new THREE.Vector3(0, 0, 0));
            scene.add(camera);

            // ambi
            var ambi = new THREE.AmbientLight(0x222222);
            scene.add(ambi);

            // Light
            light = new THREE.SpotLight(0xFFFFFF);
            light.position.set(40, 50, 100);
            light.castShadow = true;
            light.shadowMapDebug = true;
            light.shadowCameraNear = 10;
            light.shadowCameraFar = 200;
            light.intensity = 1.5;
            scene.add(light);

            var controls = new function () {
                this.addSphereMesh = function (radius, widthSegments, heightSegments) {
                    var sphere = new Physijs.SphereMesh(
                        // function ( radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength )
                        new THREE.SphereGeometry(radius, widthSegments, heightSegments),
                        getMaterial()
                    );

                    setPosAndShade(sphere);

                    meshes.push(sphere);
                    scene.add(sphere);
                };

                this.clearMeshes = function () {
                    meshes.forEach(function (e) {
                        scene.remove(e);
                    });
                    meshes = [];
                }

            };

            // add sphere nodes
            for (let i = 0; i < 7; i  ) {
                controls.addSphereMesh(3, 20, 20);
            }
            // link sphere nodes
            drawLinks();
            addConstraints();

            requestAnimationFrame(render);
            scene.simulate();
        };

        function addConstraints() {
            for (let i = 0; i < meshes.length - 1; i  ) {
                let node1 = meshes[i];
                let node2 = meshes[i   1];
                // 铰链约束 HingeConstraint
                var constraint = new Physijs.HingeConstraint(node1, node2, node2.position, new THREE.Vector3(0, 2, 0));
                scene.addConstraint(constraint);
                constraint.setLimits(
                    -3.14, // minimum angle of motion, in radians, from the point object 1 starts (going back)
                    3.14, // maximum angle of motion, in radians, from the point object 1 starts (going forward)
                    0.1, // applied as a factor to constraint error, how big the kantelpunt is moved when a constraint is hit
                    0.2 // controls bounce at limit (0.0 == no bounce)
                );
            }
        }

        function drawLinks() {
            lines.forEach(function (e) {
                scene.remove(e);
            });
            lines = [];

            // 中心节点跟所有节点相连
            for (let i = 1; i < meshes.length; i  ) {
                let node1 = meshes[0];
                let node2 = meshes[i];
                // add a line to link cube and sphere
                // 声明一个几何体geometry
                var geometry = new THREE.Geometry();
                var material = new THREE.LineBasicMaterial({
                    vertexColors: true
                });
                // 定义两种颜色,分别表示线条两个端点的颜色
                var color1 = new THREE.Color(0x444444), color2 = new THREE.Color(0x00FF00);
                // 线的材质可以由2点的颜色决定, 定义2个顶点的位置,并放到geometry中
                var p1 = new THREE.Vector3(node1.position.x, node1.position.y, node1.position.z);
                var p2 = new THREE.Vector3(node2.position.x, node2.position.y, node2.position.z);
                // 几何体里面有一个vertices变量,用来存放点。
                geometry.vertices.push(p1);
                geometry.vertices.push(p2);
                geometry.colors.push(color1, color2);
                // 定义线条,使用THREE.Line类
                var line = new THREE.Line(geometry, material, THREE.LineSegments);
                lines.push(line);
                // 将这条线加入到场景中
                scene.add(line);
            }

            // 首尾相连
            for (let i = 0; i < meshes.length - 1; i  ) {
                let node1 = meshes[i];
                let node2 = meshes[i   1];
                // add a line to link cube and sphere
                // 声明一个几何体geometry
                var geometry = new THREE.Geometry();
                var material = new THREE.LineBasicMaterial({
                    vertexColors: true
                });
                // 定义两种颜色,分别表示线条两个端点的颜色
                var color1 = new THREE.Color(0x444444), color2 = new THREE.Color(0x00FF00);
                // 线的材质可以由2点的颜色决定, 定义2个顶点的位置,并放到geometry中
                var p1 = new THREE.Vector3(node1.position.x, node1.position.y, node1.position.z);
                var p2 = new THREE.Vector3(node2.position.x, node2.position.y, node2.position.z);
                // 几何体里面有一个vertices变量,用来存放点。
                geometry.vertices.push(p1);
                geometry.vertices.push(p2);
                geometry.colors.push(color1, color2);
                // 定义线条,使用THREE.Line类
                var line = new THREE.Line(geometry, material, THREE.LineSegments);
                lines.push(line);
                // 将这条线加入到场景中
                scene.add(line);
            }
        }

        function setPosAndShade(obj) {
            let X = Math.random() * 20;
            let Y = 0;
            let Z = Math.random() * 20;
            obj.position.set(X, Y, Z);

            obj.rotation.set(Math.random() * 2 * Math.PI, Math.random() * 2 * Math.PI, Math.random() * 2 * Math.PI);
            obj.castShadow = true;
        }

        function getMaterial() {
            var material = Physijs.createMaterial(new THREE.MeshLambertMaterial({
                color: scale(Math.random()).hex(),
                // opacity: 0.8,
                // transparent: true
            }), 0.5, 0.7);
            return material;
        }

        render = function () {
            requestAnimationFrame(render);
            renderer.render(scene, camera);
            drawLinks();
            addConstraints();
            scene.simulate(undefined, 2);
        };
        window.onload = initScene;

    </script>
</head>

<body>
<div id="viewport"></div>
</body>

</html>

用Physijs在场景中添加物理效果

创建一个Physijs的Three.js场景非常简单,只要几个步骤即可。首先我们要包含正确的文件, 需要引入physi.js文件。实际模拟物理场景时非常耗费CPU的,如果我么能在render线程中做的话,场景的帧频会受到严重的影响。为了弥补这一点,Physijs选择在后台线程中执行计算。这里的后台是有Web workers(网页线程)规范定义的额,现在大多数浏览器都实现了该功能。

对Physijs来说也就意味着我们需要配置一个带有执行任务的JavaScipt文件,并告诉Physijs在哪里可以找到用来模拟场景的ammo.js文件。所以需要添加以下代码:

代码语言:javascript复制
Physijs.scripts.worker = "../libs/physijs_worker.js";
Physijs.scripts.ammo = "../libs/ammo.js";

Physijs在Three.js的普通场景外又提供了一个包装器,所以我们代码可以想这样创建场景:

代码语言:javascript复制
scene = new Physijs.Scene();
scene.setGravity(new THREE.Vector3(0, -50, 0));

在模拟物理效果之前,我们需要在场景中添加一些对象。为此,我们可以使用Three.js的普通方法来定义对象,但必须用一个特定的Physijs对象将这些对象包裹起来:

代码语言:javascript复制
var stoneGeom = new THREE.BoxGeometry(0.6, 6, 2);
                        var stone = new Physijs.BoxMesh(stoneGeom, Physijs.createMaterial(new THREE.MeshPhongMaterial({
                            color: scale(Math.random()).hex(),
                            transparent: true,
                            opacity: 0.8
                        })));
                       ...
                        scene.add(stone);

我们第一个Physijs场景中的各个部分都有了。剩下要做的就是告诉Physijs模拟物理效果,并更新场景中各对象的位置和角色。为此,我们可以调用创建的场景的simulate方法。修改基础render循环代码:

代码语言:javascript复制
render = function(){
            requestAnimationFrame(render);
            renderer.render(scene, camera);
            render_stats.update();

            scene.simulate(undefined, 1);
        }

约束

Physijs提供了一些可以用来包装几何体的图形类。使用这些几何体唯一要做的就是讲THREE.Mesh的构造函数替换成这些网格对象的构造函数。下表是Physijs中所有网格对象的概览:

代码语言:javascript复制
Physijs.PlaneMesh/这个网格可以用来创建一个厚度为0的平面。这样的平面也可以用BoxMesh对象包装一个高度很低的THREE.CubeGeometry来表示

Physijs.BoxMesh/如果是类似方块的几何体,你可以使用这个网格。例如,它的属性跟THREE.CubeGeometry的属性很相配

Physijs.SphereMesh/对于球形可以使用这个网格。它跟THREE.SphereGeometry的属性很相配

Physijs.CylinderMesh/通过设置THREE.Cylinder的属性你可以创建出各种柱状图形。Physijs为各种柱性提供了不同网格。Physijs.CylinderMesh可以用于一般的、上下一致的圆柱形

Physijs.ConeMesh/如果顶部的半径为0,底部的半径值大于0,那么你可以用THREE.Cylinder创建一个圆锥体。如果你想在这样一个对象上应用物理效果,那么可以使用的、最相匹配的网格类就是ConeMesh

Physijs.CapsuleMesh(胶囊网格)/跟THREE.Cylinder属性很相似,但其底部和底部是圆的

Physijs.ConvexMesh(凸包网格)/Physijs.ConvexMesh是一种比较粗略的图形,可用于多数复杂退行。它可以创建一个模拟复杂图形的凸包

Physijs.ConcaveMesh/ConvexMesh是一个比较粗略的图形,而ConcaveMesh则可以对负责图形进行比较细致的表现。需要注意的是使用ConcaveMesh对效率的影响比较大

Physijs.HeightfieldMesh(高度场网格)/这是一种非常特别的网格。通过该网格你可以从一个THREE.PlaneGeometry对象创建出一个高度场。

使用约束限制对象移动: 我们已经了解到各种图形如何对重力、摩擦和弹性做出反应。并影响碰撞。Physijs还提供了一些高级对象,让i可以限制对象的移动。在Physijs里,这些对象呗称作约束。下表是Physijs中可用约束概览:

代码语言:javascript复制
PointConstraint/通过这个约束,你可以将一个对象与另一个对象之间的位置固定下来。例如一个对象动了,另一个对象也会随着移动,它们之间的距离和方向保持不变

HingeConstraint/通过活页约束,你可以限制一个对象只能像活页一样移动,例如门

SliderConstraint/将对象的移动限制在一个轴上。例如移门

ConeTwistConstraint/通过这个约束,你可以用一个对象限制另一个对象的旋转和移动。这个约束的功能类似于一个球削式关节。例如,胳膊在肩关节中的活动

DOFConstraint/通过自由度约束,你可以限制对象在任意轴上的活动,你可以设置对象活动的额最小、最大角度。这是最灵活的约束方式
点对点
代码语言:javascript复制
var constraint = new Physijs.PointConstraint(
    physijs_mesh_a, // First object to be constrained
    physijs_mesh_b, // OPTIONAL second object - if omitted then physijs_mesh_1 will be constrained to the scene
    new THREE.Vector3( 0, 10, 0 ) // point in the scene to apply the constraint
);
scene.addConstraint( constraint );
铰链约束
代码语言:javascript复制
var constraint = new Physijs.HingeConstraint(
    physijs_mesh_a, // First object to be constrained
    physijs_mesh_b, // OPTIONAL second object - if omitted then physijs_mesh_1 will be constrained to the scene
    new THREE.Vector3( 0, 10, 0 ), // point in the scene to apply the constraint
    new THREE.Vector3( 1, 0, 0 ) // Axis along which the hinge lies - in this case it is the X axis
);
scene.addConstraint( constraint );
constraint.setLimits(
    low, // minimum angle of motion, in radians
    high, // maximum angle of motion, in radians
    bias_factor, // applied as a factor to constraint error
    relaxation_factor, // controls bounce at limit (0.0 == no bounce)
);
constraint.enableAngularMotor( target_velocity, acceration_force );
constraint.disableMotor();
滑块约束
代码语言:javascript复制
var constraint = new Physijs.SliderConstraint(
    physijs_mesh_a, // First object to be constrained
    physijs_mesh_b, // OPTIONAL second object - if omitted then physijs_mesh_1 will be constrained to the scene
    new THREE.Vector3( 0, 10, 0 ), // point in the scene to apply the constraint
    new THREE.Vector3( 1, 0, 0 ) // Axis along which the hinge lies - in this case it is the X axis
);
scene.addConstraint( constraint );
constraint.setLimits(
    linear_lower, // lower limit of linear movement, expressed in world units
    linear_upper, // upper limit of linear movement, expressed in world units
    angular_lower, // lower limit of angular movement, expressed in radians
    angular_upper // upper limit of angular movement, expressed in radians
);
constraint.setRestitution(
    linear, // amount of restitution when reaching the linear limits
    angular // amount of restitution when reaching the angular limits
);
constraint.enableLinearMotor( target_velocity, acceration_force );
constraint.disableLinearMotor();
constraint.enableAngularMotor( target_velocity, acceration_force );
constraint.disableAngularMotor();
锥形约束
代码语言:javascript复制
var constraint = new Physijs.ConeTwistConstraint(
    physijs_mesh_a, // First object to be constrained
    physijs_mesh_b, // Second object to be constrained
    new THREE.Vector3( 0, 10, 0 ), // point in the scene to apply the constraint
);
scene.addConstraint( constraint );
constraint.setLimit( x, y, z ); // rotational limit, in radians, for each axis
constraint.setMotorMaxImpulse( max_impulse ); // float value of the maximum impulse the motor can apply toward its target
constraint.setMotorTarget( target ); // target is the desired rotation for the constraint and can be expressed by a THREE.Vector3, THREE.Matrix4, or THREE.Quaternion
constraint.enableMotor();
constraint.disableMotor();
自由度约束
代码语言:javascript复制
var constraint = new Physijs.DOFConstraint(
    physijs_mesh_a, // First object to be constrained
    physijs_mesh_b, // OPTIONAL second object - if omitted then physijs_mesh_1 will be constrained to the scene
    new THREE.Vector3( 0, 10, 0 ), // point in the scene to apply the constraint
);
scene.addConstraint( constraint );
constraint.setLinearLowerLimit( new THREE.Vector3( -10, -5, 0 ) ); // sets the lower end of the linear movement along the x, y, and z axes.
constraint.setLinearUpperLimit( new THREE.Vector3( 10, 5, 0 ) ); // sets the upper end of the linear movement along the x, y, and z axes.
constraint.setAngularLowerLimit( new THREE.Vector3( 0, -Math.PI, 0 ) ); // sets the lower end of the angular movement, in radians, along the x, y, and z axes.
constraint.setAngularUpperLimit( new THREE.Vector3( 0, Math.PI, 0 ) ); // sets the upper end of the angular movement, in radians, along the x, y, and z axes.
constraint.configureAngularMotor(
    which, // which angular motor to configure - 0,1,2 match x,y,z
    low_limit, // lower limit of the motor
    high_limit, // upper limit of the motor
    velocity, // target velocity
    max_force // maximum force the motor can apply
);
constraint.enableAngularMotor( which ); // which angular motor to configure - 0,1,2 match x,y,z
constraint.disableAngularMotor( which ); // which angular motor to configure - 0,1,2 match x,y,z

冻结一个对象

可以使用两种方法来使对象冻结或不可移动。

  1. 如果对象始终是静态的,例如地面,则可以0使用第三个参数创建网格时将其设置为质量:new Physijs.BoxMesh( geometry, material, 0)。任何具有质量的对象0将永远是静态的。
  2. 用于对象在某些时候是静态的,并且在其他方​​面是动态的。 The second method can be used for objects when they will be static at some times and dynamic at others, like in the jenga example. If you call object.setAngularFactor and object.setLinearFactor with a THREE.Vector3( 0, 0, 0 ) then no energy will be applied to the object. You can use object.setAngularVelocity and object.setLinearVelocity in the same way to clear any velocities the object may already have. At a later point you can reset the object's linear and angular factors to ( 1, 1, 1 ) , again as it's done in the jenga example.

材质Materials

在THREE材质基础上增加了摩擦度和恢复度

代码语言:javascript复制
var friction = 0.8; // 摩擦度
var restitution = 0.3; // 恢复度
var material = Physijs.createMaterial(
    new THREE.MeshBasicMaterial({ color: 0x888888 }),
    friction,
    restitution
);
var mesh = new Physijs.BoxMesh(
    new THREE.CubeGeometry( 5, 5, 5 ),
    material
);

暂停/恢复模拟

代码语言:javascript复制
var render = function() {
    if (!isPaused) {
        scene.simulate();
    }
    renderer.render();
};
var unpauseSimulation = function() {
    isPaused = false;
    scene.onSimulationResume();
};

恢复模拟需要调用场景的onSimulationResume方法.

场景配置

代码语言:javascript复制
var scene = new Physijs.Scene({ reportsize: 50, fixedTimeStep: 1 / 60 });
  1. fixedTimeStep default=1/60 此数字确定模拟步骤的模拟时间。数字越小,模拟越准确
  2. broadphase 指定将使用哪个宽带,选择是dynamic和sweepprune。
  3. reportsize default 50 作为优化,包含对象位置的世界报告基于此数字预先初始化。最好将其设置为您的场景将具有的对象数量。
  4. setGravity方法 default ( 0, -10, 0 ) 设定重力的数量和方向
  5. setFixedTimeStep 在构造函数中default 1 / 60 重置fixedTimeStep给定的值

更新对象的位置和旋转

有一个方面,无法与three.js进行无缝集成:更改对象的位置和/或旋转。如果这样做,您必须将该对象__dirtyPosition或__dirtyRotation标志设置为true,否则将从模拟中的最后一个已知值覆盖。

代码语言:javascript复制
var mesh = new Physijs.BoxMesh( geometry, material );
scene.add( mesh );

var render = function() {
    // Change the object's position
    mesh.position.set( 0, 0, 0 );
    mesh.__dirtyPosition = true;

    // Change the object's rotation
    mesh.rotation.set(0, 90, 180);
    mesh.__dirtyRotation = true;

    // You may also want to cancel the object's velocity
    mesh.setLinearVelocity(new THREE.Vector3(0, 0, 0));
    mesh.setAngularVelocity(new THREE.Vector3(0, 0, 0));

    scene.simulate();
    renderer.render();
};

参考资料:

https://github.com/mrdoob/three.js https://threejs.org/ https://github.com/chandlerprall/Physijs http://www.hewebgl.com/article/articledir/1 https://github.com/to-be-architect/learning-threejs https://threejs.org/examples/#webgl_postprocessing_nodes_pass https://www.zhihu.com/topic/20024068/hot https://www.cnblogs.com/w-wanglei/p/6790942.html


Kotlin 开发者社区

国内第一Kotlin 开发者社区公众号,主要分享、交流 Kotlin 编程语言、Spring Boot、Android、React.js/Node.js、函数式编程、编程思想等相关主题。

越是喧嚣的世界,越需要宁静的思考。

0 人点赞