终于看懂了threejs源码

本文将直接从threejs的源码的基础几何体正方体和球体入手,深度解析原理。如果有webgl的基础的话,那么几何体的制作是非常容易的。

终于看懂了threejs源码

正方体

我们先看正方体的源码实现

import { BufferGeometry } from '../core/BufferGeometry.js';
import { Float32BufferAttribute } from '../core/BufferAttribute.js';
import { Vector3 } from '../math/Vector3.js';

class BoxGeometry extends BufferGeometry {

	constructor( width = 1, height = 1, depth = 1, widthSegments = 1, heightSegments = 1, depthSegments = 1 ) {

		super();
         // ....

}

export { BoxGeometry };

终于看懂了threejs源码

我们可以看到正方体定义了1,1,1对应的长宽高。对于标志明确的正方体,如果像以往我们去定义点的坐标是很费力的,但是threejs通过转换坐标轴来实现一个坐标通吃所有xyz的赋值。

前置条件

终于看懂了threejs源码

我们可以看到parameters来存信息,方便我们外部编辑的传参。segments来做细分,同时保证了是整数的前提。
buffers存储了索引、顶点、法线、uv纹理,但是好像没有颜色的数据。numberOfVertices用于做索引的间隔标识。groupStart用于计算buildPlane的进度

转换坐标轴

buildPlane是我们制作6个面的步骤。按照我们正常的右手坐标系,那么应该是z轴朝向我们,我们可以看到 -1, -1代表对前面轴的变换。

终于看懂了threejs源码

其他的gridX和gridY就是基本的细分,然后计算每个网格坐标的xy信息。通过depth控制z轴。

终于看懂了threejs源码

由于法线其实是单位向量并且转换了坐标系,那么我们只要考虑z轴面向我们的坐标系即可,depth控制正反面。

在uv上,我们可以想象球体展开,我们对材质做分割。注意这里纹理坐标在屏幕坐标需要翻转Y,所以需要用1减去。(纹理坐标的范围在[0, 1])

索引

终于看懂了threejs源码

我们这里只要6个索引值,但是我们缓冲区只要4个顶点。大致的执行逻辑是这样的:

终于看懂了threejs源码

渲染和辅助逻辑

终于看懂了threejs源码

将数据传递到buffer缓冲区,threejs会有另外一套渲染的逻辑这里不阐述。copy在保留原有实例覆盖了参数创造新的实例。静态方法fromJSON通过传递对象data。

球体

for ( let iy = 0; iy <= heightSegments; iy ++ ) {

        const verticesRow = [];
        
        // 用于计算坐标、以及纹理
        const v = iy / heightSegments;

        // special case for the poles
    
        let uOffset = 0;

        if ( iy === 0 && thetaStart === 0 ) {

                uOffset = 0.5 / widthSegments;

        } else if ( iy === heightSegments && thetaEnd === Math.PI ) {

                uOffset = - 0.5 / widthSegments;

        }

        for ( let ix = 0; ix <= widthSegments; ix ++ ) {

                const u = ix / widthSegments;

                // 这里只需要记住公式即可,y轴不难理解

                vertex.x = - radius * Math.cos( phiStart + u * phiLength ) * Math.sin( thetaStart + v * thetaLength );
                vertex.y = radius * Math.cos( thetaStart + v * thetaLength );
                vertex.z = radius * Math.sin( phiStart + u * phiLength ) * Math.sin( thetaStart + v * thetaLength );

                vertices.push( vertex.x, vertex.y, vertex.z );

                // 法线需要将顶点归一化,变为单位向量

                normal.copy( vertex ).normalize();
                normals.push( normal.x, normal.y, normal.z );

                // uv

                uvs.push( u + uOffset, 1 - v );

                verticesRow.push( index ++ );

        }

        grid.push( verticesRow );

}

// indices

for ( let iy = 0; iy < heightSegments; iy ++ ) {

        for ( let ix = 0; ix < widthSegments; ix ++ ) {

                const a = grid[ iy ][ ix + 1 ];
                const b = grid[ iy ][ ix ];
                const c = grid[ iy + 1 ][ ix ];
                const d = grid[ iy + 1 ][ ix + 1 ];
                
                // 注意临界值
                if ( iy !== 0 || thetaStart > 0 ) indices.push( a, b, d );
                if ( iy !== heightSegments - 1 || thetaEnd < Math.PI ) indices.push( b, c, d );

        }

}

球体其他情况跟正方体差不多,主要是弧面需要三角函数的计算,在临界值需要专门做索引、uv的处理。

uOffset的妙用

 if ( iy === 0 && thetaStart === 0 ) {

        uOffset = 0.5 / widthSegments;

} else if ( iy === heightSegments && thetaEnd === Math.PI ) {

        uOffset = - 0.5 / widthSegments;

}

这里在球体的顶端和底部会有纹理坐标的问题,我们可以仔细想想顶部其实直接看就是三角面汇聚到一个点,此时纹理坐标会被拉伸,所以我们需要以0.5,也就是一半的u值,然后除以份数,通过这个偏移值来抵消少一个三角面的影响。

原文链接:https://juejin.cn/post/7257519248053125177 作者:谦宇

(0)
上一篇 2023年7月20日 上午10:57
下一篇 2023年7月20日 上午11:07

相关推荐

发表回复

登录后才能评论