three.js源码阅读笔记一

使用three.js已经有一段时间了,为了更深入的理解这个优秀的webgl库的实现原理,从今天开始阅读源码。并且记录下自己的理解。three.js的版本更新速度还是很快的,这里我阅读的版本是r101版本。

打开three.js的项目源码后,我发现它使用了typeScript结合了rollup自动化构建工具。这里我直接阅读编译好的js文件。在src文件目录下,我发现core文件夹内是three.js的核心文件,但是我又发现这里很多文件都依赖了math文件夹内定义好的文件,因此我决定先阅读math文件夹内的文件。

首先这个文件夹内有一个Math文件我决定就从这个文件开始。

这里定义了一个 _Math对象,然后給赋予了一些数据属性,首先前两个属性为

  DEG2RAD: Math.PI / 180,
  RAD2DEG: 180 / Math.PI,

这个很好理解,就是弧度与角度之间的转换。
下面的两个方法用到了这两个属性

    degToRad: function ( degrees ) {

        return degrees * _Math.DEG2RAD;

    },

    radToDeg: function ( radians ) {

        return radians * _Math.RAD2DEG;

    },

即如果是角度转成弧度,根据角度与弧度之前的关系 角度/360 等于 弧度/2PI ,那么弧度就等于 角度 * (2PI/360),约分完后就是 弧度等于 角度*(PI/180);那么则是上面实现的过程。反过来推理类似。

然后下面是一个产生随机字符串的属性方法,它随机产生一个UUID字符串,看一下它的实现

    generateUUID: ( function () {

        // http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/21963136#21963136

        var lut = [];

        for ( var i = 0; i < 256; i ++ ) {

            lut[ i ] = ( i < 16 ? '0' : '' ) + ( i ).toString( 16 );

        }

        return function generateUUID() {

            var d0 = Math.random() * 0xffffffff | 0;
            var d1 = Math.random() * 0xffffffff | 0;
            var d2 = Math.random() * 0xffffffff | 0;
            var d3 = Math.random() * 0xffffffff | 0;
            var uuid = lut[ d0 & 0xff ] + lut[ d0 >> 8 & 0xff ] + lut[ d0 >> 16 & 0xff ] + lut[ d0 >> 24 & 0xff ] + '-' +
                lut[ d1 & 0xff ] + lut[ d1 >> 8 & 0xff ] + '-' + lut[ d1 >> 16 & 0x0f | 0x40 ] + lut[ d1 >> 24 & 0xff ] + '-' +
                lut[ d2 & 0x3f | 0x80 ] + lut[ d2 >> 8 & 0xff ] + '-' + lut[ d2 >> 16 & 0xff ] + lut[ d2 >> 24 & 0xff ] +
                lut[ d3 & 0xff ] + lut[ d3 >> 8 & 0xff ] + lut[ d3 >> 16 & 0xff ] + lut[ d3 >> 24 & 0xff ];

            // .toUpperCase() here flattens concatenated strings to save heap memory space.
            return uuid.toUpperCase();

        };

    } )(),

首先看形式它是一个立即执行并且返回一个函数的一个属性,我觉得这里这样做的目的是为了声明一个只有这个函数才会使用到并且只能这个函数内部才能访问到的内部变量lut数组。它用到了一个Number数据类型的toString()方法,并且以16为基数,也就是将此数字转变成16进制的String类型。这里复习一这个方法的解释Number.prototype.toString()
这里的描述为

Number 对象覆盖了 Object 对象上的 toString() 方法,它不是继承的 Object.prototype.toString()。对于 Number 对象,toString() 方法以指定的基数返回该对象的字符串表示。
如果转换的基数大于10,则会使用字母来表示大于9的数字,比如基数为16的情况,则使用a到f的字母来表示10到15。
如果基数没有指定,则使用 10。
如果对象是负数,则会保留负号。即使radix是2时也是如此:返回的字符串包含一个负号(-)前缀和正数的二进制表示,不是 数值的二进制补码。
进行数字到字符串的转换时,建议用小括号将要转换的目标括起来,防止出错。

上面提到进行数字到字符串的转换时,要用小括号括起来,上面正是这么做的。

这段代码的操作就是生成一个数组,长度为256,里面保存了从00到ff这256个16进制数的string形式。可以将它执行一下看看

Screen Shot 2019-10-10 at 22.31.34.png

然后在它返回的这个generateUUID函数里面,它首先是定义了四个变量,d0,d1,d2,d3,这里都是用一个0.0~1.0之间的随机数 乘以一个16进制的数字0xffffffff, 8个g,对应到10进制就是16的8次方,即4294967295,这么大的数,然后与0进行或运算,即 只要前面这个随机数不为0,就返回这个随机数。 这里的或运算是首先将两个数字转换成二进制表示,然后按位进行或运算。这里我暂时理解成用或运算符又增加了一次随机运算,算是为了增大随机的概率吧。 最后就是生成uuid的时候的,它随机用d0~d3与一个256内的数字进行与运算,这样保证了最后取值在256之内,不会对lut数组越界,进行与运算的时候,还同时在进行有符号的右移运算。总之一句话,就是为了尽可能增加随机性。最后把随机取到的数字用字符串的toUpperCase方法进行大写转换,就得到了输出的随机字符串。但每两位为一组,是一个16进制数的字符串大写,看到这里就了解了它是如何产生一个随机的字符串了。总结一下就是这里面用了很多右移、与运算、或运算、js的Math的随机函数,就是为了增加随机的概率。

接下来是一个clamp函数,这个函数接收三个参数,它的功能是根据第一个参数的值是否在第二个参数与第三个参数的范围内,来返回值。如果第一个参数在第二个参数和第三个参数之间,就返回第一个参数的值,如果第一个参数小于第二个参数,返回第二个参数,如果第一个参数大于第三个参数,返回第三个参数的值。

    clamp: function ( value, min, max ) {

        return Math.max( min, Math.min( max, value ) );

    },

可以看到它是先取了第一个参数与第三个参数中较小的那个值,再与第一个参数比较取较大的那个值。这样相当于检测第一个参数是否在第二个参数和第三个参数之间,并且返回的值保证在第二与第三参数之间。

然后下面一个函数是取余的运算,也叫取模运算,就是去第一个参数除以第二个参数后的余数,看实现过程

    // compute euclidian modulo of m % n
    // https://en.wikipedia.org/wiki/Modulo_operation

    euclideanModulo: function ( n, m ) {

        return ( ( n % m ) + m ) % m;

    },

这里贴上了维基百科对于模运算的解释

In computing, the modulo operation finds the remainder after division of one number by another (called the modulus of the operation).

其实就是解释了模就是余数的概念。但是明明可以直接写n%m的,不太明白这里为什么要写这么复杂。其实我们可以根据维基百科上的模运算的几个定理进行推导,可以发现( ( n % m ) + m ) % m 就等于 n%m。
根据下面两个公式

(a mod n) mod n = a mod n.
(a + b) mod n = [(a mod n) + (b mod n)] mod n.

我们可以推导( ( n % m ) + m ) % m 等于[( n % m ) %m + m % m] %m
等于[n%m + 0] % m 等于n%m;

也就是上面这个函数就是一个取模运算。

我们接着往下看

    // Linear mapping from range  to range 

    mapLinear: function ( x, a1, a2, b1, b2 ) {

        return b1 + ( x - a1 ) * ( b2 - b1 ) / ( a2 - a1 );

    },

这个方法我们从字面上理解就是线性映射,即将值x,在a1,与a2之间所占的比例位置线性映射到b1与b2之间的位置。实现过程也比较好理解,就是先计算出x在a1与a2之间占了多少比例,即( x - a1 )/( a2 - a1 ),然后再乘以范围( b2 - b1 ),就是它在b2与b1之间的范围。再加上b1,则是x映射到b1与b2之间的值。

下一个函数是线性插值,看一下实现过程

    // https://en.wikipedia.org/wiki/Linear_interpolation

    lerp: function ( x, y, t ) {
 
        return ( 1 - t ) * x + t * y;

    },

维基百科,线性插值
也就是它会返回x与y区间的差值的t倍,加上x。 举个例子, lerp(3,5,0.2)会返回3.4。 即 3 + (5-3)*0.2 = 3.4;上面的实现过程也可以推导出来( 1 - t ) * x + t * y 等于 (y-x) * t + x, 这样比较好理解。

接下来是是smoothstep这个方法,它按照指定的函数变化曲线去返回一个0到1之间的数字,根据x到后面两个参数的距离。

    // http://en.wikipedia.org/wiki/Smoothstep

    smoothstep: function ( x, min, max ) {

        if ( x <= min ) return 0;
        if ( x >= max ) return 1;

        x = ( x - min ) / ( max - min );

        return x * x * ( 3 - 2 * x );

    },

维基百科,Smoothstep
维基百科上说这是一个sigma家族的函数,在边界处变化的很缓慢

Screen Shot 2019-10-11 at 11.27.18.png

下面这个方法也类似,只是换了一个函数

    smootherstep: function ( x, min, max ) {

        if ( x <= min ) return 0;
        if ( x >= max ) return 1;

        x = ( x - min ) / ( max - min );

        return x * x * x * ( x * ( x * 6 - 15 ) + 10 );

    },

只是它在临界点附近变化的更加陡峭。

接下来三个方法是随机取一个范围内的整数、浮点数,还有以参数range为中点随机取-range/2到range/2之间的浮点数。

    // Random integer from  interval

    randInt: function ( low, high ) {

        return low + Math.floor( Math.random() * ( high - low + 1 ) );

    },

    // Random float from  interval

    randFloat: function ( low, high ) {

        return low + Math.random() * ( high - low );

    },

    // Random float from <-range/2, range/2> interval

    randFloatSpread: function ( range ) {

        return range * ( 0.5 - Math.random() );

    },

这个实现都很好理解,用到了Math.floor函数,向下取整数。

下面是一个判断给定的参数是否是2的次幂

    isPowerOfTwo: function ( value ) {

        return ( value & ( value - 1 ) ) === 0 && value !== 0;

    },

这里的实现思路是 用参数和参数减去1进行与运算,如果结果为0,并且参数不为0,则该参数就是2的次幂。这里实现的很巧妙,值得我们学习。

最后两个方法,是返回给定参数n的大于等于n的最小的2的次幂,和小于等于n的最大的2的次幂。

    ceilPowerOfTwo: function ( value ) {

        return Math.pow( 2, Math.ceil( Math.log( value ) / Math.LN2 ) );

    },

    floorPowerOfTwo: function ( value ) {

        return Math.pow( 2, Math.floor( Math.log( value ) / Math.LN2 ) );

    }

这里的实现用到了Math.ceil(向上取整),Math.floor(向下取整),Math.log(返回自然对数),Math.LN2代表ln2的值。我们以第一个实现过程举列分析,看它是如何实现的。假设给定的参数为value,那么我们要找的数字就是大于等于value的最小的2的次幂。假设是x次幂。也就是说 找的是 2的x次幂要大于等于value。 我们假设value = 2的y次幂,那我们我们要找的其实就是x>y的最小整数。那我们对于y,有2的y次幂等于value,两边都取自然对数,就有ln(2的y次幂)=ln(value),化简就有y*ln(2) = ln(value),那么y就等于ln(value)/ln(2); 然后对y向上取整就是x的值了,最后返回2的x次幂。过程还是很好理解的,下面那个实现思路类似。

到此Math的数据属性就都分析完了。这里他们先实现了后面可能会用到的一些数学公式的实现,方便后面的调用。

下一篇我会开始分析矩阵Matrix4的实现以及方法。

你可能感兴趣的:(three.js源码阅读笔记一)