您的位置:奥门新浦京网址 > Wed前段 > 前端基础进阶,教你用webgl快速创建一个小世界

前端基础进阶,教你用webgl快速创建一个小世界

发布时间:2019-10-24 03:06编辑:Wed前段浏览(129)

    教你用webgl快速创建一个小世界

    2017/03/25 · HTML5 · AlloyTeam

    原文出处: AlloyTeam   

    Webgl的魅力在于可以创造一个自己的3D世界,但相比较canvas2D来说,除了物体的移动旋转变换完全依赖矩阵增加了复杂度,就连生成一个物体都变得很复杂。

    什么?!为什么不用Threejs?Threejs等库确实可以很大程度的提高开发效率,而且各方面封装的非常棒,但是不推荐初学者直接依赖Threejs,最好是把webgl各方面都学会,再去拥抱Three等相关库。

    上篇矩阵入门中介绍了矩阵的基本知识,让大家了解到了基本的仿射变换矩阵,可以对物体进行移动旋转等变化,而这篇文章将教大家快速生成一个物体,并且结合变换矩阵在物体在你的世界里动起来。

    注:本文适合稍微有点webgl基础的人同学,至少知道shader,知道如何画一个物体在webgl画布中

    前端基础进阶(六):在chrome开发者工具中观察函数调用栈、作用域链与闭包

    2017/02/26 · CSS, 基础技术 · 1 评论 · Chrome, 作用域链, 函数调用栈, 闭包

    原文出处: 波同学   

    图片 1

    配图与本文无关

    在前端开发中,有一个非常重要的技能,叫做断点调试

    在chrome的开发者工具中,通过断点调试,我们能够非常方便的一步一步的观察JavaScript的执行过程,直观感知函数调用栈,作用域链,变量对象,闭包,this等关键信息的变化。因此,断点调试对于快速定位代码错误,快速了解代码的执行过程有着非常重要的作用,这也是我们前端开发者必不可少的一个高级技能。

    当然如果你对JavaScript的这些基础概念[执行上下文,变量对象,闭包,this等]了解还不够的话,想要透彻掌握断点调试可能会有一些困难。但是好在在前面几篇文章,我都对这些概念进行了详细的概述,因此要掌握这个技能,对大家来说,应该是比较轻松的。

    为了帮助大家对于this与闭包有更好的了解,也因为上一篇文章里对闭包的定义有一点偏差,因此这篇文章里我就以闭包有关的例子来进行断点调试的学习,以便大家及时纠正。在这里认个错,误导大家了,求轻喷 ~ ~

    理解JavaScript的作用域链

    2015/10/31 · JavaScript · 作用域链

    原文出处: 田小计划   

    上一篇文章中介绍了Execution Context中的三个重要部分:VO/AO,scope chain和this,并详细的介绍了VO/AO在JavaScript代码执行中的表现。

    本文就看看Execution Context中的scope chain。

    为什么说webgl生成物体麻烦

    我们先稍微对比下基本图形的创建代码
    矩形:
    canvas2D

    JavaScript

    ctx1.rect(50, 50, 100, 100); ctx1.fill();

    1
    2
    ctx1.rect(50, 50, 100, 100);
    ctx1.fill();

    webgl(shader和webgl环境代码忽略)

    JavaScript

    var aPo = [     -0.5, -0.5, 0,     0.5, -0.5, 0,     0.5, 0.5, 0,     -0.5, 0.5, 0 ];   var aIndex = [0, 1, 2, 0, 2, 3];   webgl.bindBuffer(webgl.ARRAY_BUFFER, webgl.createBuffer()); webgl.bufferData(webgl.ARRAY_BUFFER, new Float32Array(aPo), webgl.STATIC_DRAW); webgl.vertexAttribPointer(aPosition, 3, webgl.FLOAT, false, 0, 0);   webgl.vertexAttrib3f(aColor, 0, 0, 0);   webgl.bindBuffer(webgl.ELEMENT_ARRAY_BUFFER, webgl.createBuffer()); webgl.bufferData(webgl.ELEMENT_ARRAY_BUFFER, new Uint16Array(aIndex), webgl.STATIC_DRAW);   webgl.drawElements(webgl.TRIANGLES, 6, webgl.UNSIGNED_SHORT, 0);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    var aPo = [
        -0.5, -0.5, 0,
        0.5, -0.5, 0,
        0.5, 0.5, 0,
        -0.5, 0.5, 0
    ];
     
    var aIndex = [0, 1, 2, 0, 2, 3];
     
    webgl.bindBuffer(webgl.ARRAY_BUFFER, webgl.createBuffer());
    webgl.bufferData(webgl.ARRAY_BUFFER, new Float32Array(aPo), webgl.STATIC_DRAW);
    webgl.vertexAttribPointer(aPosition, 3, webgl.FLOAT, false, 0, 0);
     
    webgl.vertexAttrib3f(aColor, 0, 0, 0);
     
    webgl.bindBuffer(webgl.ELEMENT_ARRAY_BUFFER, webgl.createBuffer());
    webgl.bufferData(webgl.ELEMENT_ARRAY_BUFFER, new Uint16Array(aIndex), webgl.STATIC_DRAW);
     
    webgl.drawElements(webgl.TRIANGLES, 6, webgl.UNSIGNED_SHORT, 0);

    完整代码地址:
    结果:
    图片 2

    圆:
    canvas2D

    JavaScript

    ctx1.arc(100, 100, 50, 0, Math.PI * 2, false); ctx1.fill();

    1
    2
    ctx1.arc(100, 100, 50, 0, Math.PI * 2, false);
    ctx1.fill();

    webgl

    JavaScript

    var angle; var x, y; var aPo = [0, 0, 0]; var aIndex = []; var s = 1; for(var i = 1; i <= 36; i++) {     angle = Math.PI * 2 * (i / 36);     x = Math.cos(angle) * 0.5;     y = Math.sin(angle) * 0.5;       aPo.push(x, y, 0);       aIndex.push(0, s, s+1);       s++; }   aIndex[aIndex.length - 1] = 1; // hack一下   webgl.bindBuffer(webgl.ARRAY_BUFFER, webgl.createBuffer()); webgl.bufferData(webgl.ARRAY_BUFFER, new Float32Array(aPo), webgl.STATIC_DRAW); webgl.vertexAttribPointer(aPosition, 3, webgl.FLOAT, false, 0, 0);   webgl.vertexAttrib3f(aColor, 0, 0, 0);   webgl.bindBuffer(webgl.ELEMENT_ARRAY_BUFFER, webgl.createBuffer()); webgl.bufferData(webgl.ELEMENT_ARRAY_BUFFER, new Uint16Array(aIndex), webgl.STATIC_DRAW);   webgl.drawElements(webgl.TRIANGLES, aIndex.length, webgl.UNSIGNED_SHORT, 0);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    var angle;
    var x, y;
    var aPo = [0, 0, 0];
    var aIndex = [];
    var s = 1;
    for(var i = 1; i <= 36; i++) {
        angle = Math.PI * 2 * (i / 36);
        x = Math.cos(angle) * 0.5;
        y = Math.sin(angle) * 0.5;
     
        aPo.push(x, y, 0);
     
        aIndex.push(0, s, s+1);
     
        s++;
    }
     
    aIndex[aIndex.length - 1] = 1; // hack一下
     
    webgl.bindBuffer(webgl.ARRAY_BUFFER, webgl.createBuffer());
    webgl.bufferData(webgl.ARRAY_BUFFER, new Float32Array(aPo), webgl.STATIC_DRAW);
    webgl.vertexAttribPointer(aPosition, 3, webgl.FLOAT, false, 0, 0);
     
    webgl.vertexAttrib3f(aColor, 0, 0, 0);
     
    webgl.bindBuffer(webgl.ELEMENT_ARRAY_BUFFER, webgl.createBuffer());
    webgl.bufferData(webgl.ELEMENT_ARRAY_BUFFER, new Uint16Array(aIndex), webgl.STATIC_DRAW);
     
    webgl.drawElements(webgl.TRIANGLES, aIndex.length, webgl.UNSIGNED_SHORT, 0);

    完整代码地址:
    结果:
    图片 3

    总结:我们抛开shader中的代码和webgl初始化环境的代码,发现webgl比canvas2D就是麻烦很多啊。光是两种基本图形就多了这么多行代码,抓其根本多的原因就是因为我们需要顶点信息。简单如矩形我们可以直接写出它的顶点,但是复杂一点的圆,我们还得用数学方式去生成,明显阻碍了人类文明的进步。
    相比较数学方式生成,如果我们能直接获得顶点信息那应该是最好的,有没有快捷的方式获取顶点信息呢?
    有,使用建模软件生成obj文件。

    Obj文件简单来说就是包含一个3D模型信息的文件,这里信息包含:顶点、纹理、法线以及该3D模型中纹理所使用的贴图
    下面这个是一个obj文件的地址:

    一、基础概念回顾

    函数在被调用执行时,会创建一个当前函数的执行上下文。在该执行上下文的创建阶段,变量对象、作用域链、闭包、this指向会分别被确定。而一个JavaScript程序中一般来说会有多个函数,JavaScript引擎使用函数调用栈来管理这些函数的调用顺序。函数调用栈的调用顺序与栈数据结构一致。

    作用域

    开始介绍作用域链之前,先看看JavaScript中的作用域(scope)。在很多语言中(C++,C#,Java),作用域都是通过代码块(由{}包起来的代码)来决定的,但是,在JavaScript作用域是跟函数相关的,也可以说成是function-based。

    例如,当for循环这个代码块结束后,依然可以访问变量”i”。

    JavaScript

    for(var i = 0; i < 3; i++){ console.log(i); } console.log(i); //3

    1
    2
    3
    4
    5
    for(var i = 0; i < 3; i++){
        console.log(i);
    }
     
    console.log(i); //3

    对于作用域,又可以分为全局作用域(Global scope)和局部作用域(Local scpoe)。

    全局作用域中的对象可以在代码的任何地方访问,一般来说,下面情况的对象会在全局作用域中:

    • 最外层函数和在最外层函数外面定义的变量
    • 没有通过关键字”var”声明的变量
    • 浏览器中,window对象的属性

    局部作用域又被称为函数作用域(Function scope),所有的变量和函数只能在作用域内部使用。

    JavaScript

    var foo = 1; window.bar = 2; function baz(){ a = 3; var b = 4; } // Global scope: foo, bar, baz, a // Local scope: b

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var foo = 1;
    window.bar = 2;
     
    function baz(){
        a = 3;
        var b = 4;
    }
    // Global scope: foo, bar, baz, a
    // Local scope: b

    简单分析一下这个obj文件

    图片 4
    前两行看到#符号就知道这个是注释了,该obj文件是用blender导出的。Blender是一款很好用的建模软件,最主要的它是免费的!

    图片 5
    Mtllib(material library)指的是该obj文件所使用的材质库文件(.mtl)
    单纯的obj生成的模型是白模的,它只含有纹理坐标的信息,但没有贴图,有纹理坐标也没用

    图片 6
    V 顶点vertex
    Vt 贴图坐标点
    Vn 顶点法线

    图片 7
    Usemtl 使用材质库文件中具体哪一个材质

    图片 8
    F是面,后面分别对应 顶点索引 / 纹理坐标索引 / 法线索引

    这里大部分也都是我们非常常用的属性了,还有一些其他的,这里就不多说,可以google搜一下,很多介绍很详细的文章。
    如果有了obj文件,那我们的工作也就是将obj文件导入,然后读取内容并且按行解析就可以了。
    先放出最后的结果,一个模拟银河系的3D文字效果。
    在线地址查看:

    在这里顺便说一下,2D文字是可以通过分析获得3D文字模型数据的,将文字写到canvas上之后读取像素,获取路径。我们这里没有采用该方法,因为虽然这样理论上任何2D文字都能转3D,还能做出类似input输入文字,3D展示的效果。但是本文是教大家快速搭建一个小世界,所以我们还是采用blender去建模。

    二、认识断点调试工具

    在尽量新版本的chrome浏览器中(不确定你用的老版本与我的一致),调出chrome浏览器的开发者工具。

    浏览器右上角竖着的三点 -> 更多工具 -> 开发者工具 -> Sources

    1
    浏览器右上角竖着的三点 -> 更多工具 -> 开发者工具 -> Sources

    界面如图。

    图片 9

    断点调试界面

    在我的demo中,我把代码放在app.js中,在index.html中引入。我们暂时只需要关注截图中红色箭头的地方。在最左侧上方,有一排图标。我们可以通过使用他们来控制函数的执行顺序。从左到右他们依次是:

    • resume/pause script execution
      恢复/暂停脚本执行
    • step over next function call
      跨过,实际表现是不遇到函数时,执行下一步。遇到函数时,不进入函数直接执行下一步。
    • step into next function call
      跨入,实际表现是不遇到函数时,执行下一步。遇到到函数时,进入函数执行上下文。
    • step out of current function
      跳出当前函数
    • deactivate breakpoints
      停用断点
    • don‘t pause on exceptions
      不暂停异常捕获

    其中跨过,跨入,跳出是我使用最多的三个操作。

    上图左侧第二个红色箭头指向的是函数调用栈(call Stack),这里会显示代码执行过程中,调用栈的变化。

    左侧第三个红色箭头指向的是作用域链(Scope),这里会显示当前函数的作用域链。其中Local表示当前的局部变量对象,Closure表示当前作用域链中的闭包。借助此处的作用域链展示,我们可以很直观的判断出一个例子中,到底谁是闭包,对于闭包的深入了解具有非常重要的帮助作用。

    作用域链

    通过前面一篇文章了解到,每一个Execution Context中都有一个VO,用来存放变量,函数和参数等信息。

    在JavaScript代码运行中,所有用到的变量都需要去当前AO/VO中查找,当找不到的时候,就会继续查找上层Execution Context中的AO/VO。这样一级级向上查找的过程,就是所有Execution Context中的AO/VO组成了一个作用域链。

    所以说,作用域链与一个执行上下文相关,是内部上下文所有变量对象(包括父变量对象)的列表,用于变量查询。

    JavaScript

    Scope = VO/AO + All Parent VO/AOs

    1
    Scope = VO/AO + All Parent VO/AOs

    看一个例子:

    JavaScript

    var x = 10; function foo() { var y = 20; function bar() { var z = 30; console.log(x + y + z); }; bar() }; foo();

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    var x = 10;
     
    function foo() {
        var y = 20;
     
        function bar() {
            var z = 30;
     
            console.log(x + y + z);
        };
     
        bar()
    };
     
    foo();

    上面代码的输出结果为”60″,函数bar可以直接访问”z”,然后通过作用域链访问上层的”x”和”y”。

    图片 10

    • 绿色箭头指向VO/AO
    • 蓝色箭头指向scope chain(VO/AO + All Parent VO/AOs)

    再看一个比较典型的例子:

    JavaScript

    var data = []; for(var i = 0 ; i < 3; i++){ data[i]=function() { console.log(i); } } data[0]();// 3 data[1]();// 3 data[2]();// 3

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var data = [];
    for(var i = 0 ; i < 3; i++){
        data[i]=function() {
            console.log(i);
        }
    }
     
    data[0]();// 3
    data[1]();// 3
    data[2]();// 3

    第一感觉(错觉)这段代码会输出”0,1,2″。但是根据前面的介绍,变量”i”是存放在”Global VO”中的变量,循环结束后”i”的值就被设置为3,所以代码最后的三次函数调用访问的是相同的”Global VO”中已经被更新的”i”。

    具体实现

    三、断点设置

    在显示代码行数的地方点击,即可设置一个断点。断点设置有以下几个特点:

    • 在单独的变量声明(如果没有赋值),函数声明的那一行,无法设置断点。
    • 设置断点后刷新页面,JavaScript代码会执行到断点位置处暂停执行,然后我们就可以使用上边介绍过的几个操作开始调试了。
    • 当你设置多个断点时,chrome工具会自动判断从最早执行的那个断点开始执行,因此我一般都是设置一个断点就行了。

    结合作用域链看闭包

    在JavaScript中,闭包跟作用域链有紧密的关系。相信大家对下面的闭包例子一定非常熟悉,代码中通过闭包实现了一个简单的计数器。

    JavaScript

    function counter() { var x = 0; return { increase: function increase() { return ++x; }, decrease: function decrease() { return --x; } }; } var ctor = counter(); console.log(ctor.increase()); console.log(ctor.decrease());

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function counter() {
        var x = 0;
     
        return {
            increase: function increase() { return ++x; },
            decrease: function decrease() { return --x; }
        };
    }
     
    var ctor = counter();
     
    console.log(ctor.increase());
    console.log(ctor.decrease());

    下面我们就通过Execution Context和scope chain来看看在上面闭包代码执行中到底做了哪些事情。

    1. 当代码进入Global Context后,会创建Global VO

    图片 11.

    • 绿色箭头指向VO/AO
    • 蓝色箭头指向scope chain(VO/AO + All Parent VO/AOs)

     

    1. 当代码执行到”var cter = counter();”语句的时候,进入counter Execution Context;根据上一篇文章的介绍,这里会创建counter AO,并设置counter Execution Context的scope chain

    图片 12

    1. 当counter函数执行的最后,并退出的时候,Global VO中的ctor就会被设置;这里需要注意的是,虽然counter Execution Context退出了执行上下文栈,但是因为ctor中的成员仍然引用counter AO(因为counter AO是increase和decrease函数的parent scope),所以counter AO依然在Scope中。

    图片 13

    1. 当执行”ctor.increase()”代码的时候,代码将进入ctor.increase Execution Context,并为该执行上下文创建VO/AO,scope chain和设置this;这时,ctor.increase AO将指向counter AO。

    图片 14

    • 绿色箭头指向VO/AO
    • 蓝色箭头指向scope chain(VO/AO + All Parent VO/AOs)
    • 红色箭头指向this
    • 黑色箭头指向parent VO/AO

     

    相信看到这些,一定会对JavaScript闭包有了比较清晰的认识,也了解为什么counter Execution Context退出了执行上下文栈,但是counter AO没有销毁,可以继续访问。

    本文由奥门新浦京网址发布于Wed前段,转载请注明出处:前端基础进阶,教你用webgl快速创建一个小世界

    关键词:

上一篇:没有了

下一篇:没有了