前端可视化基础(持续更新ing)

142 阅读42分钟

引言

在当今的数字时代,可视化技术已经成为前端开发领域中不可或缺的一部分。前端可视化是指通过使用各种图表、地图、仪表盘等交互式的可视化元素来呈现数据、信息和内容。这种技术不仅可以帮助用户更好地理解数据,还可以提供更好的用户体验,使得信息呈现更加直观、生动。在互联网应用、大数据分析、商业智能等领域,前端可视化已经成为不可或缺的一种技术手段。通过运用前端可视化技术,开发人员可以更加高效地展示数据,从而为用户提供更好的体验。因此,掌握前端可视化技术已经成为前端开发人员必备的技能之一。

Bauhaus.jpg

浏览器中实现可视化的四种方式

  • HTML + CSS
  • SVG
    • SVG 是一种基于 XML 语法的图像格式,可以用图片(img 元素)的 src 属性加载。
    • 元素的属性和数值可以直接对应起来。而 CSS 代码并不能直观体现出数据的数值,需要进行 CSS 规则转换。
  • Canvas 2D
    • 无论是使用 HTML/CSS 还是 SVG,它们都属于声明式绘图系统,也就是我们根据数据创建各种不同的图形元素(或者 CSS 规则),然后利用浏览器渲染引擎解析它们并渲染出来。但是 Canvas2D 不同,它是浏览器提供的一种可以直接用代码在一块平面的“画布”上绘制图形的 API,使用它来绘图更像是传统的“编写代码”,简单来说就是调用绘图指令,然后引擎直接在页面上绘制图形。这是一种指令式的绘图系统。
    • Canvas 能够直接操作绘图上下文,不需要经过 HTML、CSS 解析、构建渲染树、布局等一系列操作。因此单纯绘图的话,Canvas 比 HTML/CSS 和 SVG 要快得多。
  • WebGL
    • 基于 OpenGL ES 规范的浏览器实现

三种可视化的缺点(详)

  • HTML+CSS
    • 首先,HTML 和 CSS 主要还是为网页布局而创造的,使用它们虽然能绘制可视化图表,但是绘制的方式并不简洁。这是因为,从 CSS 代码里,我们很难看出数据与图形的对应关系,有很多换算也需要开发人员自己来做。这样一来,一旦图表或数据发生改动,就需要我们重新计算,维护起来会很麻烦。
    • 其次,HTML 和 CSS 作为浏览器渲染引擎的一部分,为了完成页面渲染的工作,除了绘制图形外,还要做很多额外的工作。比如说,浏览器的渲染引擎在工作时,要先解析 HTML、SVG、CSS,构建 DOM 树、RenderObject 树和 RenderLayer 树,然后用 HTML(或 SVG)绘图。当图形发生变化时,我们很可能要重新执行全部的工作,这样的性能开销是非常大的。而且传统的 Web 开发,因为涉及 UI 构建和内容组织,所以这些额外的解析和构建工作都是必须做的。而可视化与传统网页不同,它不太需要复杂的布局,更多的工作是在绘图和数据计算。所以,对于可视化来说,这些额外的工作反而相当于白白消耗了性能。
    • 因此,相比于 HTML 和 CSS,Canvas2D 和 WebGL 更适合去做可视化这一领域的绘图工作。它们的绘图 API 能够直接操作绘图上下文,一般不涉及引擎的其他部分,在重绘图像时,也不会发生重新解析文档和构建结构的过程,开销要小很多。
  • SVG
    • 在渲染引擎中,SVG 元素和 HTML 元素一样,在输出图形前都需要经过引擎的解析、布局计算和渲染树生成。而且,一个 SVG 元素只表示一种基本图形,如果展示的数据很复杂,生成图形的 SVG 元素就会很多。这样一来,大量的 SVG 元素不仅会占用很多内存空间,还会增加引擎、布局计算和渲染树生成的开销,降低性能,减慢渲染速度。这也就注定了 SVG 只适合应用于元素较少的简单可视化场景。
  • Canvas 2D
    • 因为 HTML 和 SVG 一个元素对应一个基本图形,所以我们可以很方便地操作它们。而同样的功能在 Canvas 上就比较难实现了,因为对于 Canvas 来说,绘制整个图形的过程就是一系列指令的执行过程,其中并没有区分每个图形,这让我们很难单独对 Canvas 绘图的局部进行控制,只能通过数学计算定位的方式来控制局部图形。

使用WebGL的情况

一般情况下,Canvas2D 绘制图形的性能已经足够高了,但是在三种情况下我们有必要直接操作更强大的 GPU 来实现绘图。

  • 第一种情况,如果我们要绘制的图形数量非常多,比如有多达数万个几何图形需要绘制,而且它们的位置和方向都在不停地变化,那我们即使用 Canvas2D 绘制了,性能还是会达到瓶颈。这个时候,我们就需要使用 GPU 能力,直接用 WebGL 来绘制。
  • 第二种情况,如果我们要对较大图像的细节做像素处理,比如,实现物体的光影、流体效果和一些复杂的像素滤镜。由于这些效果往往要精准地改变一个图像全局或局部区域的所有像素点,要计算的像素点数量非常的多(一般是数十万甚至上百万数量级的)。这时,即使采用 Canvas2D 操作,也会达到性能瓶颈,所以我们也要用 WebGL 来绘制。
  • 第三种情况是绘制 3D 物体。因为 WebGL 内置了对 3D 物体的投影、深度检测等特性,所以用它来渲染 3D 物体就不需要我们自己对坐标做底层的处理了。那在这种情况下,WebGL 无论是在使用上还是性能上都有很大优势。

四种可视化方式的优缺点

  • HTML+CSS 的优点是方便,不需要第三方依赖,甚至不需要 JavaScript 代码。如果我们要绘制少量常见的图表,可以直接采用 HTML 和 CSS。它的缺点是 CSS 属性不能直观体现数据,绘制起来也相对麻烦,图形复杂会导致 HTML 元素多,而消耗性能。
  • SVG 是对 HTML/CSS 的增强,弥补了 HTML 绘制不规则图形的能力。它通过属性设置图形,可以直观地体现数据,使用起来非常方便。但是 SVG 也有和 HTML/CSS 同样的问题,图形复杂时需要的 SVG 元素太多,也非常消耗性能。
  • Canvas2D 是浏览器提供的简便快捷的指令式图形系统,它通过一些简单的指令就能快速绘制出复杂的图形。由于它直接操作绘图上下文,因此没有 HTML/CSS 和 SVG 绘图因为元素多导致消耗性能的问题,性能要比前两者快得多。但是如果要绘制的图形太多,或者处理大量的像素计算时,Canvas2D 依然会遇到性能瓶颈。
  • WebGL 是浏览器提供的功能强大的绘图系统,它使用比较复杂,但是功能强大,能够充分利用 GPU 并行计算的能力,来快速、精准地操作图像的像素,在同一时间完成数十万或数百万次计算。另外,它还内置了对 3D 物体的投影、深度检测等处理,这让它更适合绘制 3D 场景。

三大组件

  • 场景: Scene()
  • 相机: PerspectiveCamera( fov : Number, aspect : Number, near : Number, far : Number )
    • fov — 摄像机视锥体垂直视野角度
    • aspect — 摄像机视锥体长宽比
    • near — 摄像机视锥体近端面
    • far — 摄像机视锥体远端面
  • 渲染器: WebGLRenderer
        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);
    
  • 几何体: CubeGeometry(width, height, depth, segmentsWidth, segmentsHeight, segmentsDepth, materials, sides)
  • 基础网格材质: MeshBasicMaterial()
        // 添加物体到场景中
        var geometry = new THREE.CubeGeometry(1,1,1); 
        var material = new THREE.MeshBasicMaterial({color: 0x00ff00});
        var cube = new THREE.Mesh(geometry, material); 
        scene.add(cube);
    
  • 渲染: renderer.render(scene, camera, renderTarget, forceClear)
    • scene:前面定义的场景
    • camera:前面定义的相机
    • renderTarget:渲染的目标,默认是渲染到前面定义的render变量中
    • forceClear:每次绘制之前都将画布的内容给清除,即使自动清除标志autoClear为false,也会清除。
        // 渲染有两种方式:实时渲染和离线渲染(预渲染)
        // 实时渲染:就是需要不停的对画面进行渲染,即使画面中什么也没有改变,也需要重新渲染。下面就是一个渲染循环:
        function render() {
            cube.rotation.x += 0.1;
            cube.rotation.y += 0.1;
            renderer.render(scene, camera);
            requestAnimationFrame(render);
        }
    

Canvas

  • Canvas 是执行绘图指令绘图的“指令式”绘图系统
  • Canvas 元素和 2D 上下文
    • Canvas 元素上的 width 和 height 属性不等同于 Canvas 元素的 CSS 样式的属性。这是因为,CSS 属性中的宽高影响 Canvas 在页面上呈现的大小,而 HTML 属性中的宽高则决定了 Canvas 的坐标系。为了区分它们,我们称 Canvas 的 HTML 属性宽高为画布宽高,CSS 样式宽高为样式宽高。
    • 画布宽高
          <body>
              <canvas width="512" height="512"></canvas>
          </body>
      
    • 样式宽高
          canvas {
              width: 256px;
              height: 256px;
          }
      
    • 在实际绘制的时候,如果我们不设置 Canvas 元素的样式,那么 Canvas 元素的画布宽高就会等于它的样式宽高的像素值。而如果这个时候,我们通过 CSS 设置其他的值指定了它的样式宽高。比如说,我们将样式宽高设置成 256px,那么它实际的画布宽高就是样式宽高的两倍了
  • 坐标系
    • Canvas 的坐标系和浏览器窗口的坐标系类似,它们都默认左上角为坐标原点,x 轴水平向右,y 轴垂直向下
    • y 轴向下,意味着这个坐标系和笛卡尔坐标系不同,它们的 y 轴是相反的。那在实际应用的时候,如果我们想绘制一个向右上平抛小球的动画,它的抛物线轨迹,在 Canvas 上绘制出来的方向就是向下凹的
    • 另外,如果我们再考虑旋转或者三维运动,这个坐标系就会变成“左手系”。而左手系的平面法向量的方向和旋转方向,和我们熟悉的右手系相反
  • 利用 Canvas 绘制几何图形
    • 获取 Canvas 上下文
          const canvas = document.querySelector('canvas');
          const context = canvas.getContext('2d');
      
    • 用 Canvas 上下文绘制图形
      • 我们拿到的 context 对象上会有许多 API,它们大体上可以分为两类:一类是设置状态的 API,可以设置或改变当前的绘图状态,比如,改变要绘制图形的颜色、线宽、坐标变换等等;另一类是绘制指令 API,用来绘制不同形状的几何图形。
    • 在画布的中心位置绘制一个 100*100 的红色正方形
          const rectSize = [100, 100];
          context.fillStyle = 'red';
          context.beginPath();  // 告诉 Canvas 我们现在绘制的路径
          context.rect(0.5 * canvas.width, 0.5 * canvas.height, ...rectSize);
      
          // 方法一
          context.translate(-0.5 * rectSize[0], -0.5 * rectSize[1]);
          // 平移之后需恢复画布
          // context.save(); // 暂存状态
          // 平移
          // context.translate(-0.5 * rectSize[0], -0.5 * rectSize[1]);
          // ... 执行绘制
          // context.restore(); // 恢复状态
      
          // 方法二
          // context.rect(0.5 * (canvas.width - rectSize[0]), 0.5 * (canvas.height - rectSize[1]), ...rectSize);
      
    • 总结
      • 获取 Canvas 对象,通过 getContext(‘2d’) 得到 2D 上下文;
      • 设置绘图状态,比如填充颜色 fillStyle,平移变换 translate 等等;
      • 调用 beginPath 指令开始绘制图形;
      • 调用绘图指令,比如 rect,表示绘制矩形;
      • 调用 fill 指令,将绘制内容真正输出到画布上。

SVG

  • SVG 属于声明式绘图系统,它的绘制方式和 Canvas 不同,它不需要用 JavaScript 操作绘图指令,只需要和 HTML 一样,声明一些标签就可以实现绘图了
  • SVG 坐标系和 Canvas 坐标系完全一样,都是以图像左上角为原点,x 轴向右,y 轴向下的左手坐标系。而且在默认情况下,SVG 坐标与浏览器像素对应,所以 100、50、40 的单位就是 px,也就是像素,不需要特别设置
        <svg xmlns="http://www.w3.org/2000/svg" version="1.1">
            <circle cx="100" cy="50" r="40" stroke="black" stroke-width="2" fill="orange" />
        </svg>
    
  • 在 Canvas 中,为了让绘制出来的图形适配不同的显示设备,我们要设置 Canvas 画布坐标。同理,我们也可以通过给 svg 元素设置 viewBox 属性,来改变 SVG 的坐标系。如果设置了 viewBox 属性,那 SVG 内部的绘制就都是相对于 SVG 坐标系的了。
  • 创建步骤
    1. 获取 SVG 对象
          const svgroot = document.querySelector('svg');
      
    2. 实现 draw 方法从 root 开始遍历数据对象。通过创建 SVG 元素,将元素添加到 DOM 文档里,让图形显示出来。
          function draw(parent, node, {fillStyle = 'rgba(0, 0, 0, 0.2)', textColor = 'white'} = {}) {
              const {x, y, r} = node;
              const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
              circle.setAttribute('cx', x);
              circle.setAttribute('cy', y);
              circle.setAttribute('r', r);
              circle.setAttribute('fill', fillStyle);
              parent.appendChild(circle);
              ...
          }
          draw(svgroot, root);
      
      • 与使用 document.createElement 方法创建普通的 HTML 元素不同,SVG 元素要使用 document.createElementNS 方法来创建
      • 其中,第一个参数是名字空间,对应 SVG 名字空间,第二个参数是要创建的元素标签名
  • SVG 和 Canvas 的不同点
    • 写法上的不同
    • SVG 通过创建标签来表示图形元素,通过元素的 setAttribute 给图形元素赋属性值,这个和操作 HTML 元素是一样的。而 Canvas 先是通过上下文执行绘图指令来绘制图形,再通过上下文设置状态属性。设置的状态只有在绘图指令执行时才会生效
    • 因为 SVG 的声明式类似于 HTML 书写方式,本身对前端工程师会更加友好。但是,SVG 图形需要由浏览器负责渲染和管理,将元素节点维护在 DOM 树中。这样做的缺点是,在一些动态的场景中,也就是需要频繁地增加、删除图形元素的场景中,SVG 与一般的 HTML 元素一样会带来 DOM 操作的开销,所以 SVG 的渲染性能相对比较低。
    • 用户交互实现上的不同
    • 利用 SVG 的一个图形对应一个 svg 元素的机制,我们就可以像操作普通的 HTML 元素那样,给 svg 元素添加事件实现用户交互。所以,SVG 有一个非常大的优点,那就是可以让图形的用户交互非常简单。
    • 和 SVG 相比,利用 Canvas 对图形元素进行用户交互就没有那么容易了。如果我们要绘制的图形不是圆、矩形这样的规则图形,而是一个复杂得多的多边形,该怎样确定鼠标在哪个图形元素的内部?这是一个比较复杂的问题
  • 绘制大量几何图形时 SVG 的性能问题
    • 如果我们要绘制的图形非常复杂,这些元素节点的数量就会非常多。而节点数量多,就会大大增加 DOM 树渲染和重绘所需要的时间。
    • 可以使用虚拟 DOM 方案来尽可能地减少重绘,这样就可以优化 SVG 的渲染。但是这些方案只能解决一部分问题,当节点数太多时,这些方案也无能为力。这个时候,我们还是得依靠 Canvas 和 WebGL 来绘图,才能彻底解决问题。

WebGL

  • 计算机图形系统的主要组成部分
    • 输入设备
    • 中央处理单元
    • 图形处理单元
    • 存储器
    • 帧缓存
    • 输出设备
    • 相关概念
      • 光栅(Raster):几乎所有的现代图形系统都是基于光栅来绘制图形的,光栅就是指构成图像的像素阵列
      • 像素(Pixel):一个像素对应图像上的一个点,它通常保存图像上的某个具体位置的颜色等信息
      • 帧缓存(Frame Buffer):在绘图过程中,像素信息被存放于帧缓存中,帧缓存是一块内存地址
      • CPU(Central Processing Unit):中央处理单元,负责逻辑计算
      • GPU(Graphics Processing Unit):图形处理单元,负责图形计算。
  • 计算机图形绘图过程
    • 首先,数据经过 CPU 处理,成为具有特定结构的几何信息。然后,这些信息会被送到 GPU 中进行处理。在 GPU 中要经过两个步骤生成光栅信息。这些光栅信息会输出到帧缓存中,最后渲染到屏幕上。
    • 绘图过程是现代计算机中任意一种图形系统处理图形的通用过程。它主要做了两件事,一是对给定的数据结合绘图的场景要素(例如相机、光源、遮挡物体等等)进行计算,最终将图形变为屏幕空间的 2D 坐标。二是为屏幕空间的每个像素点进行着色,把最终完成的图形输出到显示设备上。这个过程也叫做渲染管线。在这个过程中,CPU 与 GPU 是最核心的两个处理单。
  • CPU 和 GPU 都属于处理单元,但是结构不同。一个计算机系统会有很多条 CPU 流水线,而且任何一个任务都可以随机地通过任意一个流水线,这样计算机就能够并行处理多个任务了,这样的一条流水线就是我们常说的线程(Thread),一条 CPU 流水线串行处理这些任务的速度,取决于 CPU(管道)的处理能力。处理图像时每处理一个像素点就相当于完成一个简单的任务,而一个图片应用又是由成千上万个像素点组成的,所以,我们需要在同一时间处理成千上万个小任务,要处理这么多的小任务,比起使用若干个强大的 CPU,使用更小、更多的处理单元,是一种更好的处理方式。而 GPU 就是这样的处理单元,它由大量的小型处理单元构成的,可以保证每个单元处理一个简单的任务
  • 利用WebGL 绘制三角形
    1. 创建 WebGL 上下文
      • 创建 WebGL 上下文这一步和 Canvas2D 的使用几乎一样,只需调用 canvas 元素的 getContext 即可,区别是将参数从’2d’换成’webgl’。
            const canvas = document.querySelector('canvas');
            const gl = canvas.getContext('webgl');
        
    2. 创建 WebGL 程序(WebGL Program)
      • 这里的 WebGL 程序是一个 WebGLProgram 对象,它是给 GPU 最终运行着色器的程序,而不是我们正在写的三角形的 JavaScript 程序

      • 需先编写两个着色器(Shader)。着色器是用 GLSL 这种编程语言编写的代码片段,在 GLSL 中,attribute 表示声明变量,vec2 是变量的类型,它表示一个二维向量,position 是变量名。

            const vertex = `
            attribute vec2 position;
            void main() {
                gl_PointSize = 1.0;
                gl_Position = vec4(position, 1.0, 1.0);
            }
            `;
            const fragment = `
            precision mediump float;
            void main() {
                gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
            }    
            `;
        
      • 在片元着色器里,我们可以通过设置 gl_FragColor 的值来定义和改变图形的颜色。gl_FragColor 是 WebGL 片元着色器的内置变量,表示当前像素点颜色,它是一个用 RGBA 色值表示的四维向量数据。在上面的代码中,因为我们写入 vec4(1.0, 0.0, 0.0, 1.0) 对应的是红色,所以三角形是红色的。如果我们把这个值改成 vec4(0.0, 0.0, 1.0, 1.0),那三角形就是蓝色。我们只更改了一个值,就把整个图片的所有像素颜色都改变了。所以WebGL 可以并行地对整个三角形的所有像素点同时运行片元着色器。并行处理是 WebGL 程序非常重要的概念。不论这个三角形是大还是小,有几十个像素点还是上百万个像素点,GPU 都是同时处理每个像素点的。也就是说,图形中有多少个像素点,着色器程序在 GPU 中就会被同时执行多少次。

      • 顶点着色器的作用

        • 通过 gl_Position 设置顶点
          • 把三角形的周长缩小为原始大小的一半: 将 gl_Position = vec4(position, 1.0, 1.0); 修改为 gl_Position = vec4(position * 0.5, 1.0, 1.0)
          • 这个过程中,我们不需要遍历三角形的每一个顶点,只需要是利用 GPU 的并行特性,在顶点着色器中同时计算所有的顶点(WebGL 可以并行计算)
        • 向片元着色器传递数据
          • 除了计算顶点之外,顶点着色器还可以将数据通过 varying 变量传给片元着色器。然后,这些值会根据片元着色器的像素坐标与顶点像素坐标的相对位置做线性插值
                attribute vec2 position;
                varying vec3 color;
            
                void main() {
                    gl_PointSize = 1.0;
                    color = vec3(0.5 + position * 0.5, 0.0);
                    gl_Position = vec4(position * 0.5, 1.0, 1.0);
                }
            

            在这段代码中,我们修改了顶点着色器,定义了一个 color 变量,它是一个三维的向量。我们通过数学技巧将顶点的值映射为一个 RGB 颜色值,映射公式是 vec3(0.5 + position * 0.5, 0.0)。这样一来,顶点[-1,-1]被映射为[0,0,0]也就是黑色,顶点[0,1]被映射为[0.5, 1, 0]也就是浅绿色,顶点[1,-1]被映射为[1,0,0]也就是红色。这样一来,三个顶点就会有三个不同的颜色值。

          • 然后将 color 通过 varying 变量传给片元着色器
                precision mediump float;
                varying vec3 color;
            
                void main() {
                    gl_FragColor = vec4(color, 1.0);
                }
            
          • 可以看到,这个三角形是一个颜色均匀(线性)渐变的三角形,它的三个顶点的色值就是我们通过顶点着色器来设置的。而且你会发现,中间像素点的颜色是均匀过渡的。这就是因为 WebGL 在执行片元着色器程序的时候,顶点着色器传给片元着色器的变量,会根据片元着色器的像素坐标对变量进行线性插值。利用线性插值可以让像素点的颜色均匀渐变这一特点,我们就能绘制出颜色更丰富的图形了。
      • 为什么要创建两个着色器?这就需要我们先来理解顶点和图元这两个基本概念了。在绘图的时候,WebGL 是以顶点和图元来描述图形几何信息的。顶点就是几何图形的顶点。图元是 WebGL 可直接处理的图形单元,由 WebGL 的绘图模式决定,有点、线、三角形等等。所以,顶点和图元是绘图过程中必不可少的。因此,WebGL 绘制一个图形的过程,一般需要用到两段着色器,一段叫顶点着色器(Vertex Shader)负责处理图形的顶点信息,另一段叫片元着色器(Fragment Shader)负责处理图形的像素信息。

      • 可以把顶点着色器理解为处理顶点的 GPU 程序代码。它可以改变顶点的信息(如顶点的坐标、法线方向、材质等等),从而改变我们绘制出来的图形的形状或者大小等等。顶点处理完成之后,WebGL 就会根据顶点和绘图模式指定的图元,计算出需要着色的像素点,然后对它们执行片元着色器程序。简单来说,就是对指定图元中的像素点着色。

      • WebGL 从顶点着色器和图元提取像素点给片元着色器执行代码的过程,就是我们前面说的生成光栅信息的过程,我们也叫它光栅化过程。所以,片元着色器的作用,就是处理光栅化后的像素信息。

      • 我们可以将图元设为线段,那么片元着色器就会处理顶点之间的线段上的像素点信息,这样画出来的图形就是空心的。而如果我们把图元设为三角形,那么片元着色器就会处理三角形内部的所有像素点,这样画出来的图形就是实心的。因为图元是 WebGL 可以直接处理的图形单元,所以其他非图元的图形最终必须要转换为图元才可以被 WebGL 处理。举个例子,如果我们要绘制实心的四边形,我们就需要将四边形拆分成两个三角形,再交给 WebGL 分别绘制出来。

      • 片元着色器对像素点着色的过程是并行的。也就是说,无论有多少个像素点,片元着色器都可以同时处理。这也是片元着色器一大特点。

      • 在 JavaScript 中,顶点着色器和片元着色器只是一段代码片段,所以我们要将它们分别创建成 shader 对象

            const vertexShader = gl.createShader(gl.VERTEX_SHADER);
            gl.shaderSource(vertexShader, vertex);
            gl.compileShader(vertexShader);
        
            const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
            gl.shaderSource(fragmentShader, fragment);
            gl.compileShader(fragmentShader);
        
      • 接着,我们创建 WebGLProgram 对象,并将这两个 shader 关联到这个 WebGL 程序上。WebGLProgram 对象的创建过程主要是添加 vertexShader 和 fragmentShader,然后将这个 WebGLProgram 对象链接到 WebGL 上下文对象上

            const program = gl.createProgram();
            gl.attachShader(program, vertexShader);
            gl.attachShader(program, fragmentShader);
            gl.linkProgram(program);
        
      • 最后,我们要通过 useProgram 选择启用这个 WebGLProgram 对象。这样,当我们绘制图形时,GPU 就会执行我们通过 WebGLProgram 设定的 两个 shader 程序了。

          ```js
          gl.useProgram(program);
          ```
        
    3. 将数据存入缓冲区
      • 定义这个三角形的三个顶点。WebGL 使用的数据需要用类型数组定义,默认格式是 Float32Array。Float32Array 是 JavaScript 的一种类型化数组(TypedArray),JavaScript 通常用类型化数组来处理二进制缓冲区
            const points = new Float32Array([
                -1, -1,
                0, 1,
                1, -1,
            ]);
        
      • 将定义好的数据写入 WebGL 的缓冲区
            const bufferId = gl.createBuffer();  // 创建一个缓存对象
            gl.bindBuffer(gl.ARRAY_BUFFER, bufferId);  // 将它绑定为当前操作对象
            gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);  // 再把当前的数据写入缓存对象
        
    4. 将缓冲区数据读取到 GPU
      • 现在我们已经把数据写入缓存了,但是我们的 shader 现在还不能读取这个数据,还需要把数据绑定给顶点着色器中的 position 变量。
            const vPosition = gl.getAttribLocation(program, 'position');  // 获取顶点着色器中的position变量的地址
            gl.vertexAttribPointer(vPosition, 2, gl.FLOAT, false, 0, 0);  // 给变量设置长度和类型
            gl.enableVertexAttribArray(vPosition);  // 激活这个变量
        
      • 经过这样的处理,在顶点着色器中,我们定义的 points 类型数组中对应的值,就能通过变量 position 读到了。
    5. 执行着色器程序完成绘制
      • 先调用 gl.clear 将当前画布的内容清除,然后调用 gl.drawArrays 传入绘制模式。这里我们选择 gl.TRIANGLES 表示以三角形为图元绘制,再传入绘制的顶点偏移量和顶点数量,WebGL 就会将对应的 buffer 数组传给顶点着色器,并且开始绘制
            gl.clear(gl.COLOR_BUFFER_BIT);
            gl.drawArrays(gl.TRIANGLES, 0, points.length / 2);
        

场景,相机,渲染器之间的关系

  • Three.js中的场景是一个物体的容器,开发者可以将需要的角色放入场景中
  • 相机的作用就是面对场景,在场景中取一个合适的景,把它拍下来
  • 渲染器的作用就是将相机拍摄下来的图片,放到浏览器中去显示

相机Camera

  • 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 );
        scene.add( camera );
    
  • PerspectiveCamera( fov, aspect, near, far )[透视投影相机]
    • fov:视角的大小,如果设置为0,相当于闭上眼睛了,什么也看不到,如果设置为180,那么可以认为你的视界很广阔,但是在180度的时候,往往物体很小,因为他在你的整个可视区域中的比例变小了。人眼的正常视角是120度左右,但是要集中注意力看清楚东西,视角应该在30-40度比较好
    • near:近平面,表示你近处的裁面的距离。也可以认为是眼睛距离近处的距离,请不要设置为负值
    • far:远平面,表示你远处的裁面,
    • aspect:实际窗口的纵横比,即宽度除以高度(与名称相反)。这个值越大,说明宽度越大。
        var camera = new THREE.PerspectiveCamera( 45, width / height, 1, 1000 );
        scene.add( camera );
    
  • 正投影和透视投影的区别是:透视投影有一个基本点,就是远处的物体比近处的物体小,而正投影的特点是,远近高低比例都相同

点线面

  • 定义一个点
        var point1 = new THREE.Vecotr3(4,8,9);
        // 或者
        var point1 = new THREE.Vector3();
        point1.set(4,8,9);
        // Threejs中没有提供单独画点的函数,它必须被放到一个THREE.Geometry形状中,这个结构中包含一个数组vertices
    
  • 定义一条线
    1. 声明一个几何体geometry
          var geometry = new THREE.Geometry();
          //几何体里面有一个vertices变量,可以用来存放点。
      
    2. 定义线条材质: LineBasicMaterial( parameters )
      • Parameters是一个定义材质外观的对象,它包含多个属性来定义材质,这些属性是:
        • Color:线条的颜色,用16进制来表示,默认的颜色是白色。
        • Linewidth:线条的宽度,默认时候1个单位宽度。
        • Linecap:线条两端的外观,默认是圆角端点,当线条较粗的时候才看得出效果,如果线条很细,那么你几乎看不出效果了。
        • Linejoin:两个线条的连接点处的外观,默认是“round”,表示圆角。
        • VertexColors:定义线条材质是否使用顶点颜色,这是一个boolean值。意思是,线条各部分的颜色会根据顶点的颜色来进行插值。
        • Fog:定义材质的颜色是否受全局雾效的影响。
          var material = new THREE.LineBasicMaterial( { vertexColors: true } );
      
    3. 定义分别表示线条两个端点的颜色
          var color1 = new THREE.Color( 0x444444 ), color2 = new THREE.Color( 0xFF0000 );
      
    4. 定义2个顶点的位置,并放到geometry中
          var p1 = new THREE.Vector3( -100, 0, 100 );
          var p2 = new THREE.Vector3(  100, 0, -100 );
      
          geometry.vertices.push(p1);
          geometry.vertices.push(p2);
      
    5. 为4中定义的2个顶点,设置不同的颜色
          geometry.colors.push( color1, color2 );
          // geometry中colors表示顶点的颜色,必须材质中vertexColors等于THREE.VertexColors 时,颜色才有效,如果vertexColors等于THREE.NoColors时,颜色就没有效果了。那么就会去取材质中color的值
      
    6. 定义一条线
          var line = new THREE.Line( geometry, material, THREE.LinePieces );
      // 第一个参数是几何体geometry,里面包含了2个顶点和顶点的颜色。第二个参数是线条的材质,或者是线条的属性,表示线条以哪种方式取色。第三个参数是一组点的连接方式
      
    7. 添加线到场景中
          scene.add(line);
      
  • 网格
    1. 定义2个点
          geometry.vertices.push( new THREE.Vector3( - 500, 0, 0 ) );
          geometry.vertices.push( new THREE.Vector3( 500, 0, 0 ) );
      
    2. 算法
      • 这两个点决定了x轴上的一条线段,将这条线段复制20次,分别平行移动到z轴的不同位置,就能够形成一组平行的线段。
      • 同理,将p1p2这条线先围绕y轴旋转90度,然后再复制20份,平行于z轴移动到不同的位置,也能形成一组平行线。
      • 经过上面的步骤,就能够得到坐标网格了。
          for ( var i = 0; i <= 20; i ++ ) {
              var line = new THREE.Line( geometry, new THREE.LineBasicMaterial( { color: 0x000000, opacity: 0.2 } ) );
              line.position.z = ( i * 50 ) - 500;
              scene.add( line );
      
              var line = new THREE.Line( geometry, new THREE.LineBasicMaterial( { color: 0x000000, opacity: 0.2 } ) );
              line.position.x = ( i * 50 ) - 500;
              line.rotation.y = 90 * Math.PI / 180;	//  旋转90度
              scene.add( line );
          }
      
    • 完整代码
          <!DOCTYPE html>
          <html>
              <head>
                  <meta charset="UTF-8">
                  <title>Three框架</title>
                  <script src="js/Three.js"></script>
                  <style type="text/css">
                      div#canvas-frame {
                          border: none;
                          cursor: pointer;
                          width: 100%;
                          height: 600px;
                          background-color: #EEEEEE;
                      }
                  </style>
                  <script>
                      var renderer;
                      function initThree() {
                          width = document.getElementById('canvas-frame').clientWidth;
                          height = document.getElementById('canvas-frame').clientHeight;
                          renderer = new THREE.WebGLRenderer({
                              antialias : true
                          });
                          renderer.setSize(width, height);
                          document.getElementById('canvas-frame').appendChild(renderer.domElement);
                          renderer.setClearColor(0xFFFFFF, 1.0);
                      }
      
                      var camera;
                      function initCamera() {
                          camera = new THREE.PerspectiveCamera(45, width / height, 1, 10000);
                          camera.position.x = 0;
                          camera.position.y = 1000;
                          camera.position.z = 0;
                          camera.up.x = 0;
                          camera.up.y = 0;
                          camera.up.z = 1;
                          camera.lookAt({
                              x : 0,
                              y : 0,
                              z : 0
                          });
                      }
      
                      var scene;
                      function initScene() {
                          scene = new THREE.Scene();
                      }
      
                      var light;
                      function initLight() {
                          light = new THREE.DirectionalLight(0xFF0000, 1.0, 0);
                          light.position.set(100, 100, 200);
                          scene.add(light);
                      }
      
                      var cube;
                      function initObject() {
                          var geometry = new THREE.Geometry();
                          geometry.vertices.push( new THREE.Vector3( - 500, 0, 0 ) );
                          geometry.vertices.push( new THREE.Vector3( 500, 0, 0 ) );
      
                          for ( var i = 0; i <= 20; i ++ ) {
                              var line = new THREE.Line( geometry, new THREE.LineBasicMaterial( { color: 0x000000, opacity: 0.2 } ) );
                              line.position.z = ( i * 50 ) - 500;
                              scene.add( line );
      
                              var line = new THREE.Line( geometry, new THREE.LineBasicMaterial( { color: 0x000000, opacity: 0.2 } ) );
                              line.position.x = ( i * 50 ) - 500;
                              line.rotation.y = 90 * Math.PI / 180;
                              scene.add( line );
                          }
                      }
      
                      function threeStart() {
                          initThree();
                          initCamera();
                          initScene();
                          initLight();
                          initObject();
                          renderer.clear();
                          renderer.render(scene, camera);
                      }
                  </script>
              </head>
      
              <body onload="threeStart();">
                  <div id="canvas-frame"></div>
              </body>
          </html>
      

位置

  • 相机位置 .position
        // 设置相机位置(眼睛位置或者说相机篇拍照位置)
        camera.position.set(200, 300, 200);
    
  • 相机观察点 .lookAt()
        //摄像机镜头指向的具体坐标位置
        camera.lookAt(0,0,0);
    
  • 相机以哪个方向为上方 .up
  • 漫游(视线不变)或平移效果

    通过改变相机的位置属性.position,可以在三维场景中产生漫游效果,就好像一个人走在院子等场景中,人眼看到的效果一样。 改变.position属性后,如果不执行.lookAt()方法,相机的观察方向不变,因为改变.position属性只会改变相机对象视图矩阵的平移部分,只有执行.lookAt()方法才会从.position属性提取数据计算视图矩阵的旋转部分。如果你对视图矩阵没有概念,只需要记住仅仅改变.position属性,不再执行.lookAt()方法,相当于生活中,摄像机移动,但是镜头指向的方向不变,或者说人一直走,但是人的头不转动。 - 粉底试色最好去专柜,或者是购买小样和分装类产品。在试色时要选择和自己脖子颜色相近的色号,试色后最好能带妆保留一天,看看粉底液的表现力和持妆度如何,再来决定是否要购买。

        // 可以在threejs周期性执行的渲染函数中改变相机的位置参数产生小场景模型平移或大场景中漫游的效果
        function render() {
            renderer.render(scene, camera);
            requestAnimationFrame(render);
            //相机位置x坐标一直变大,可以理解为人沿着x轴方向漫游或者说场景在x轴方向平移
            camera.position.x += 1
        }
    
  • 绕转观察

    一个相机对象绕转一个坐标中心做圆周运动,同时观看方向一直指向该坐标中心,需要每次改变.position属性后,重新渲染一遍.lookAt()方法以便于更新相机对象视图矩阵的旋转部分,因为相机镜头指向的方向不会自动随着相机位置变化而自动变化。

        // 声明一个变量表示角度值
        var angle = 0
        function render() {
            renderer.render(scene, camera); //执行渲染操作
            requestAnimationFrame(render);
            // 每次执行render函数,累加0.005改变角度值angle
            angle+=0.005
            // 重新设置相机位置,相机在XOY平面绕着坐标原点旋转运动
            camera.position.x=200*Math.sin(angle)
            camera.position.y=200*Math.cos(angle)
            // 相机位置改变后,注意执行.looAt()方法重新计算视图矩阵旋转部分
            // 如果不执行.looAt()方法,相当于相机镜头方向保持在首次执行`.lookAt()`的时候
            camera.lookAt(0,0,0);
        }
    

光源Light

  • 光源基类: Light( color : Integer, intensity : float )
    • color: (可选参数) 16进制表示光的颜色。 缺省值 0xffffff (白色)。
    • intensity: (可选参数) 光照强度。 缺省值 1。
        var redLight = new THREE.Light(0xFF0000);
    
  • 由基类派生出来的其他种类光源
    • 环境光: AmbientLight( color : Integer, intensity : Float )
      • 经过多次反射而来的光称为环境光,无法确定其最初的方向。环境光是一种无处不在的光。环境光源放出的光线被认为来自任何方向。因此,当你仅为场景指定环境光时,所有的物体无论法向量如何,都将表现为同样的明暗程度。 (这是因为,反射光可以从各个方向进入您的眼睛)
      • 环境光不能用来投射阴影
          var light = new THREE.AmbientLight( 0x404040 ); // soft white light
          scene.add( light );
      
    • 点光源: PointLight( color : Integer, intensity : Float, distance : Number, decay : Float )
      • distance - 这个距离表示从光源到光照强度为0的位置。 也就是从光源所在的位置,经过distance这段距离之后,光的强度将衰减为0 当设置为0时,光永远不会消失(距离无穷大)。缺省值 0.
      • decay - 沿着光照距离的衰退量。缺省值 1。 在 physically correct 模式中,decay = 2。
      • 由这种光源放出的光线来自同一点,且方向辐射自四面八方
      • 点光源是理想化为质点的向四面八方发出光线的光源。点光源是抽象化了的物理概念,为了把物理问题的研究简单化。就像平时说的光滑平面,质点,无空气阻力一样,点光源在现实中也是不存在的,指的是从一个点向周围空间均匀发光的光源
      • 点光源的特点是发光部分为一个小圆面,近似一个点
      • 一个面分前后两个面的,只有被光源照射的那个面才能够被看到。如果在边上被边挡住,只有内部受到光源,而外部面没有受到光源的,因此呈现黑色
          var light = new THREE.PointLight( 0xff0000, 1, 100 );
          light.position.set( 50, 50, 50 );
          scene.add( light );
      
    • 聚光灯: SpotLight( color : Integer, intensity : Float, distance : Float, angle : Radians, penumbra : Float, decay : Float )
      • 这种光源的光线从一个锥体中射出,在被照射的物体上产生聚光的效果。使用这种光源需要指定光的射出方向以及锥体的顶角α
      • angle : 光线散射角度,这个角度是和光源的方向形成的角度,最大为Math.PI/2
      • penumbra : 聚光锥的半影衰减百分比。在0和1之间的值。默认为0
          var spotLight = new THREE.SpotLight( 0xffffff );
          spotLight.position.set( 100, 1000, 100 );
          spotLight.castShadow = true;
          // castShadow: 此属性设置为 true 聚光灯将投射阴影。警告: 这样做的代价比较高而且需要一直调整到阴影看起来正确
          spotLight.shadow.mapSize.width = 1024;
          spotLight.shadow.mapSize.height = 1024;
          spotLight.shadow.camera.near = 500;
          spotLight.shadow.camera.far = 4000;
          spotLight.shadow.camera.fov = 30;
          scene.add( spotLight );
      
    • 平行光(方向光): DirectionalLight( color : Integer, intensity : Float )
      • 平行光是沿着特定方向发射的光,是一组没有衰减的平行的光线。这种光的表现像是无限远,从它发出的光线都是平行的。常常用平行光来模拟太阳光 的效果; 太阳足够远,因此我们可以认为太阳的位置是无限远,所以我们认为从太阳发出的光线也都是平行的
      • 平行光可以投射阴影
      • 方向由位置和原点(0,0,0)来决定,方向光只与方向有关,与离物体的远近无关。分别将平行光放到(0,0,100),(0,0,50),(0,0,25),(0,0,1),渲染的结果是一样的
  • 环境光和方向光同时存在的情况
    • 方向光没有照射到的部分呈现环境光的颜色
    • 方向光和环境光同时存在的情况呈现叠加色
  • 材质与光源的关系
    • 材质是表面各可视属性的结合,这些可视属性是指表面的色彩、纹理、光滑度、透明度、反射率、折射率、发光度等
    • 但这不是材质的真相,因为离开光材质是无法体现的
    • 当没有任何光源的时候,最终的颜色将是黑色,无论材质是什么颜色
  • Lambert材质
    • 这是在灰暗的或不光滑的表面产生均匀散射而形成的材质类型。比如一张纸,它粗糙不均匀,不会产生镜面效果
    • Lambert材质会受环境光的影响,呈现环境光的颜色,与材质本身颜色关系不大

纹理Texture

  • 从本质上来说,纹理只是图片而已,它是由像素点组成。无论在内存还是显存中,它都是由4个分量组成,这四个分量是R、G、B和A。唯一的不同的,在显存中,会比内存中更快的渲染到显示器上
  • 图片和canvas的差别: 图片是通过图像处理软件,如photoshop来处理的。而canvas是通过浏览器的绘图API来绘制的
  • Texture( image, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy, encoding )
    • .image: 一个图片对象,通常由TextureLoader.load方法创建。 该对象可以是被three.js所支持的任意图片(例如PNG、JPG、GIF、DDS)或视频(例如MP4、OGG/OGV)格式。要使用视频来作为纹理贴图,你需要有一个正在播放的HTML5 Video元素来作为你纹理贴图的源图像, 并在视频播放时不断地更新这个纹理贴图。——VideoTexture 类会对此自动进行处理
          var texture = new THREE.TextureLoader().load( 'textures/land_ocean_ice_cloud_2048.jpg' );
          // 立即使用纹理进行材质创建
          var material = new THREE.MeshBasicMaterial( { map: texture } );
      
    • .mapping(number): 图像将如何应用到物体(对象)上。默认值是THREE.UVMapping对象类型, 即UV坐标将被用于纹理映射
    • .wrapS(number): 定义了纹理贴图在水平方向上将如何包裹,在UV映射中对应于U。默认值是THREE.ClampToEdgeWrapping,即纹理边缘将被推到外部边缘的纹素
    • .wrapT(number): 定义了纹理贴图在垂直方向上将如何包裹,在UV映射中对应于V。请注意:纹理中图像的平铺,仅有当图像大小(以像素为单位)为2的幂(2、4、8、16、32、64、128、256、512、1024、2048、……)时才起作用。 宽度、高度无需相等,但每个维度的长度必须都是2的幂。 这是WebGL中的限制,不是由three.js所限制的。
    • .magFilter(number): 当一个纹素覆盖大于一个像素时,贴图将如何采样。默认值为THREE.LinearFilter, 它将获取四个最接近的纹素,并在他们之间进行双线性插值。 另一个选项是THREE.NearestFilter,它将使用最接近的纹素的值。
    • .minFilter(number): 当一个纹素覆盖小于一个像素时,贴图将如何采样。默认值为THREE.LinearMipmapLinearFilter, 它将使用mipmapping以及三次线性滤镜
    • .format(number): 默认值为THREE.RGBAFormat, 但TextureLoader将会在载入JPG图片时自动将这个值设置为THREE.RGBFormat
    • .type(number): 这个值必须与.format相对应。默认值为THREE.UnsignedByteType(无符号型), 它将会被用于绝大多数纹理格式
    • anisotropy(number): 各向异性过滤,使用各向异性过滤能够使纹理的效果更好,但是会消耗更多的内存、CPU、GPU时间。沿着轴,通过具有最高纹素密度的像素的样本数。 默认情况下,这个值为1。设置一个较高的值将会产生比基本的mipmap更清晰的效果,代价是需要使用更多纹理样本。 使用renderer.getMaxAnisotropy() 来查询GPU中各向异性的最大有效值;这个值通常是2的幂
    • .encoding(number): 默认值为THREE.LinearEncoding。 请注意,如果在材质被使用之后,纹理贴图中这个值发生了改变, 需要触发Material.needsUpdate,来使得这个值在着色器中实现。
  • 纹理坐标: 在正常的情况下,在0.0到1.0的范围内指定纹理坐标,这个纹理坐标将被对应到一个形状上
  • 步骤:
    1. 画一个平面

      通过PlaneGemotry画一个平面:

          var geometry = new THREE.PlaneGeometry( 500, 300, 1, 1 );
      

      这个平面的宽度是500,高度是300.

    2. 为平面赋予纹理坐标

      为平面的4个顶点指定纹理坐标。纹理坐标由顶点的uv成员来表示,uv被定义为一个二维向量THREE.Vector2()

          geometry.vertices[0].uv = new THREE.Vector2(0,0);
          geometry.vertices[1].uv = new THREE.Vector2(1,0);
          geometry.vertices[2].uv = new THREE.Vector2(1,1);
          geometry.vertices[3].uv = new THREE.Vector2(0,1);
      

      注意:(0,0),(1,0),(1,1),(0,1)他们之间的顺序是逆时针方向。在给平面赋纹理坐标的时候也要注意方向,不然three.js是分不清楚的

    3. 加载纹理

      纹理作为一张图片,可以来源于互联网,或者本地服务器,但是就是不能来源于类似C:\pic\a.jpg这样的本地路径。这是因为javascript没有加载本地路径文件的权限

          var texture = new THREE.TextureLoader().load( "textures/water.jpg" );
      
    4. 将纹理应用于材质
          var material = new THREE.MeshBasicMaterial({map:texture});
          var mesh = new THREE.Mesh( geometry,material );
          scene.add( mesh );
      
  • 将canvas作为纹理,将动画作为纹理
    • 在canvas上画图
    • 将canvas传递给THREE.Texture纹理
          texture = new THREE.Texture( canvas);
      
    • 将纹理传递给THREE.MeshBasicMaterial材质
          var material = new THREE.MeshBasicMaterial({map:texture});
      
    • 构造THREE.Mesh
          mesh = new THREE.Mesh( geometry,material );
      
    • 注意:在定义了纹理之后,需要将texture.needsUpdate设置为true,如果不设置为true,那么纹理就不会更新,原因是纹理没有被载入之前,就开始渲染了,而渲染使用了默认的材质颜色。纹理的绘制是需要一段时间的,javascript是可以异步运行的,在canvas绘制出图形之前,可能three.js就开始根据纹理渲染图形了。另一个方面,canvas如果绘制的是动画,每隔一段时间都会重新绘制一次,也需要不断的更新纹理,所以需要将needUpdate设置为true

让场景动起来

  • 方法
    • 让物体在坐标系里面移动,摄像机不动
    • 让摄像机在坐标系里面移动,物体不动
  • 渲染循环
    • 物体运动还有一个关键点,就是要渲染物体运动的每一个过程,让它显示给观众。渲染的时候,我们调用的是渲染器的render() 函数。代码如下:
          renderer.render( scene, camera );
      
    • 如果我们改变了物体的位置或者颜色之类的属性,就必须重新调用render()函数,才能够将新的场景绘制到浏览器中去。不然浏览器是不会自动刷新场景的。如果不断的改变物体的颜色,那么就需要不断的绘制新的场景,所以我们最好的方式,是让画面执行一个循环,不断的调用render来重绘,这个循环就是渲染循环,在游戏中,也叫游戏循环。
    • 为了实现循环,我们需要调用requestAnimationFrame函数,传递一个callback参数,则在下一个动画帧时,会调用callback这个函数。
          function animate() {
              render();
              requestAnimationFrame( animate );
          }
      
  • 改变相机的位置,实现物体移动
        function animation() {
            camera.position.x += 1;
            renderer.render(scene, camera);
            requestAnimationFrame(animation);
        }
    
  • 改变物体自身的位置,实现物体移动
        function animation() {
            mesh.position.x -= 1;
            // 其中mesh就是指的物体,它有一个位置属性position,这个position是一个THREE.Vector3类型变量
            renderer.render(scene, camera);
            requestAnimationFrame(animation);
        }
    
  • 物体运动后,怎么评估程序的性能
    • 帧数:图形处理器每秒能刷新几次,通常用fps(Frames Per Second)来表示

      当物体在快速运动时,人眼所看到的影像消失后,人眼仍能继续保留其影像1/24秒左右的图像,这种现象被称为视觉暂留现象。是人眼具有的一种性质。人眼观看物体时,成像于视网膜上,并由视神经输入人脑,感觉到物体的像。一帧一帧的图像进入人脑,人脑就会将这些图像给连接起来,形成动画

    • 性能监视器Stats
      • FPS: 表示上一秒的帧数,这个值越大越好,一般都为60左右
      • MS: 表示渲染一帧需要的毫秒数,这个数字是越小越好
    • 性能监视器Stats的使用
          var stats = new Stats();
          stats.showPanel(1); // 0: fps, 1: ms, 2: mb, 3+: custom
          // 将stats的界面对应左上角
          stats.domElement.style.position = 'absolute';
          stats.domElement.style.left = '0px';
          stats.domElement.style.top = '0px';
          document.body.appendChild( stats.domElement );
          function animate() {
              stats.begin();
              // monitored code goes here
              stats.end();
              requestAnimationFrame( animate );
          }
          requestAnimationFrame( animate );
      
    • 步骤
      1. new 一个stats对象
            stats = new Stats();
        
      2. 将这个对象加入到html网页中去
            stats.domElement.style.position = 'absolute';
            stats.domElement.style.left = '0px';
            stats.domElement.style.top = '0px';
        
      3. 调用stats.update()函数来统计时间和帧数
  • 使用动画引擎Tween.js来创建动画
    • 通过移动相机和移动物体来产生动画的效果。使用的方法是在渲染循环里去移动相机或者物体的位置。如果动画稍微复杂一些,这种方式实现起来就比较麻烦一些了。为了使程序编写更容易一些,可以使用动画引擎来实现动画效果。和three.js紧密结合的动画引擎是Tween.js
    • 步骤
      1. 构建一个Tween对象,对Tween进行初始化
            function initTween() {
                new TWEEN.Tween( mesh.position).to( { x: -400 }, 3000 ).repeat( Infinity ).start();
            }
        
        • to函数,接受两个参数,第一个参数是一个集合,里面存放的键值对,键x表示mesh.position的x属性,值-400表示,动画结束的时候需要移动到的位置。第二个参数,是完成动画需要的时间,这里是3000ms。
        • repeat( Infinity )表示重复无穷次,也可以接受一个整形数值
        • Start表示开始动画,默认情况下是匀速的将mesh.position.x移动到-400的位置
      2. 在渲染函数中去不断的更新Tween,这样才能够让mesh.position.x移动位置
            function animation() {
                renderer.render(scene, camera);
                requestAnimationFrame(animation);
                stats.update();
                TWEEN.update();
            }
        

3D模型的加载与使用

  • 3D模型定义: 3D模型由顶点(vertex)组成,顶点之间连成三角形或四边形(在一个平面上),多个三角形或者四边形就能够组成复杂的立体模型。3D模型由一个个网格组成,所以也叫其为网格模型
  • 模型在three.js中的表示: 模型是由面组成,面分为三角形和四边形面。三角形和四边形面组成了网格模型。在Three.js中用THREE.Mesh来表示网格模型。THREE.Mesh可以和THREE.Line相提并论,区别是THREE.Line表示的是线条。THREE.Mesh表示面的集合
  • 模型的加载
    • 服务器上的模型文件以文本的方式存储,除了以three.js自定义的文本方式存储之外,也可以以二进制的方式存储
    • 浏览器下载文件到本地
    • Javascript解析模型文件,生成Mesh网格模型
    • 显示在场景中。

未完待续.......