Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

环境搭建 & 项目架构介绍 #1

Open
Vigilans opened this issue Oct 2, 2018 · 5 comments
Open

环境搭建 & 项目架构介绍 #1

Vigilans opened this issue Oct 2, 2018 · 5 comments
Assignees

Comments

@Vigilans
Copy link
Owner

Vigilans commented Oct 2, 2018

复述一下环境配置过程:

Prerequisites

  1. Node.js & npm, 通过 npm 安装 gulp 和 typescript: npm install -g gulp typescript
  2. VSCode, 推荐安装的插件有:
  • Debugger for Chrome
  • Shader languages support for VS Code
  • glsl-canvas
  • 3D Viewer for VSCode
  1. Chrome 浏览器。(当然用 FireFox 也可以,需要自己改对应插件和 launch.json 相关设置。

Development

  1. git clone到本地后,用 VSCode 打开文件夹,在终端中运行 npm install
  2. 通过 Ctrl+Shift+B终端->运行生成任务运行 Gulp Server (端口为8080);
  3. 在左侧的调试界面中,任选一个任务运行:
  • 此后,按 F5/Ctrl+F5 便可直接运行当前任务;
  • 每个任务对应一次作业,单独用一个文件夹存放;
  • 调试新作业时,在launch.json中新增一个条目即可。
  1. 每次修改代码,Gulp 服务器便会自动 Build 并刷新浏览器。
@Vigilans Vigilans changed the title 环境搭建 环境搭建 & 项目架构介绍 Nov 11, 2018
@Vigilans Vigilans assigned Vigilans and unassigned Vigilans, linshiC and zjdx1998 Nov 11, 2018
@Vigilans
Copy link
Owner Author

Vigilans commented Nov 11, 2018

Typescript

绝大部分关于Typescript语言本身的问题,建议查询官方文档:
https://www.tslang.cn/docs/handbook/basic-types.html

至于如何实现某种功能/怎样做才是最佳实践,建议多查Google与Stack Overflow。

tsconfig.json

我们的tsconfig.json的配置为:

{
    "compilerOptions": {
        "noImplicitAny": false, // 可以有隐式any, 方便导入js文件
        "watch": true,          // 监视模式,当ts文件更改时会自动重新编译
        "sourceMap": true,      // 可以通过ts代码来调试实际运行的js
        "target": "es2018",     // ts将编译到最新的es2018 js代码
        "outDir": "./dist",     // 所有编译出来的文件将送到dist文件夹,免得跟ts源代码纠缠在一起
        "rootDir": "./",        // 当前目录是我们的工作根目录
        "declaration": true     // 编译出js的同时附上带有声明的.d.ts文件(相当于头文件)
    }
}

@Vigilans
Copy link
Owner Author

Vigilans commented Nov 12, 2018

Gulp.js

Gulp是一个自动化构建工具。在本项目中我们利用Gulp来实现自动化编译、更新文件、刷新页面等功能。Gulp的构建流由gulpfile.js中定义的一系列task构成,通过gulp taskname命令启动对应的task。

Gulp的API文档请参阅:https://www.gulpjs.com.cn/docs/api/
node-glob的匹配语法请参阅:https://github.com/isaacs/node-glob#glob-primer

下面通过介绍gulpfile.js的代码来解释自动化构建流的实现过程。

gulp.task('connect')

我们定义一个运行服务器的任务,这个服务器的根目录在dist文件夹下,并具有自动刷新的功能:

let gulp = require('gulp');
let connect = require('gulp-connect');

gulp.task('connect', () => 
    connect.server({
        root: './dist',
        livereload: true
    })
);

gulp.task('reload')

我们定义一个刷新页面的任务:

gulp.task('reload', () => {
    return gulp.src('./dist/**/*.html').pipe(connect.reload());
});

./dist/**/*.html将匹配dist文件夹下的所有html文件,通过gulp.src提取包装后经pipe函数推送给服务器进行刷新。

gulp.task('copy')

我们定义一个拷贝非ts源代码(ts代码是被编译去的)至发布文件夹的任务:

gulp.task('copy', () => {
    gulp.src('./@(core|+([0-9]).+(?))/**/*.@(html|js|json|glsl[vf])').pipe(gulp.dest('./dist'));
});

gulp.src里面的通配符匹配「core文件夹或序号开头的文件夹里的所有html/js/json/glslv/glslf文件」:

  • glsl[vf]匹配glslv或glslf;
  • @(html|js)匹配一次且仅一次html或js;
  • /**/匹配任意文件夹及其子文件夹;
  • +([0-9])匹配1个以上的数码;
  • ?匹配1个任意字符,+(?)匹配1个或多个任意字符
    因此:
  • @(core|+([0-9]).+(?))匹配「core文件夹或序号开头的文件夹」
  • *.@(html|js|json|glsl[vf])匹配「所有html/js/json/glslv/glslf文件」

匹配的内容通过pipe推送到目标位置gulp.dest('./dist'),从而实现更新效果。

gulp.task('watch')

我们定义一个监视任务,从而正式实现自动刷新效果:

gulp.task('watch', () => {
    gulp.watch(['./@(core|+([0-9]).+(?))/**/*.@(html|js|json|glsl[vf])'], ['copy']);
    gulp.watch(['./dist/**/*.@(html|js|json|glsl?)'], ['reload']);
});

当源代码(@(core|+([0-9]).+(?)))的内容发生变化时,自动触发copy任务拷贝新代码至发布文件夹。
当发布代码(dist/**/*.@(html|js|json|glsl?))的内容发生变化时,自动触发浏览器刷新任务。

gulp.task('compile')

我们定义一个typescript实时编译的任务,借tsconfig中的watch参数达到监视效果:

gulp.task('compile', () => {
    const cmd = os.platform() == 'win32' ? 'tsc.cmd' : 'tsc';
    const childProcess = require('child_process');
    const child = childProcess.spawn(cmd, []);
    return child;
});

当ts文件发生变化时,处于watch状态的tsc会自动检测到并编译至发布文件夹。
gulp.watch任务随即会检测到编译出的js文件发生变化,从而触发copyreload任务。

gulp.task('default')

最后,我们定义一个默认任务,将以上各任务串接起来,提供一个总入口:

gulp.task('default', ['connect', 'copy', 'watch', 'compile']);

之后,我们可以在命令行中通过gulp default启动Gulp,或是借助VSCode,在.vscode/task.json中添加以下任务,通过Ctrl+Shift+B运行:

{
    "type": "gulp",
    "task": "default",
    "group": {
        "kind": "build",
        "isDefault": true
    }
}

这样,就可以通过localhost:8080来查看我们的项目了。
.vscode/launch.json中添加如下代码,便可实现对指定作业进行调试:

{
    "name": "2.i-is-fish",
    "type": "chrome",
    "request": "launch",
    "webRoot": "${workspaceRoot}/dist",
    "sourceMaps": true,
    "url": "http://localhost:8080/2.i-is-fish"
}

@Vigilans
Copy link
Owner Author

Vigilans commented Jan 6, 2019

WebGL Extension

原生的WebGL接口太弱,简直如编写汇编一般,一个函数只完成一个极其小的功能点。因此, 有必要对其进行初步封装。WebGL Extension模块提供了如下功能的包装:

Method Function
initShader 通过着色器代码与种类(顶点/片元),生成一个着色器
initProgram 通过顶点与片元着色器,生成一个WebGL程序
initTexture 若传入图像,则为图像生成材质;否则生成一个绑定帧缓冲的空材质
createBufferInfo 根据attributes列表,生成一个缓冲并绑定属性数据
createProgramInfo 根据一个WebGL程序与模式,生成一个带有控制功能的程序信息
createFrameBufferInfo 生成一个绑定有指定尺寸的空材质的帧缓冲

@Vigilans
Copy link
Owner Author

Vigilans commented Jan 6, 2019

Rendering Object

本模块对WebGL中的一些基础概念进行层层包装,最终得到一个用来进行画图的可渲染对象。

Uniform

一个Uniform属性像是一个全局变量,仅仅用一个number或是一个Array就可表示,没有其他的多余信息。

因此,Uniform可被声明为一个复合类型:

type WebGLUniformType = number | Array<number> | WebGLArray;

Attribute & BufferInfo

一个Attribute是一系列顶点拥有的数据集合。它包含以下几个重要属性:

  • data: WebGLArray:顶点的数据源,包含了所有顶点的数据。
  • numComponents: number:一个顶点占了数据缓冲的几个元素,用于分割data
  • buffer: WebGLBuffer:顶点的数据缓冲储存的位置。
  • drawType, normalize, stride, offset:一些用于设置顶点性质的属性。

一个渲染模型仅仅只有一系列Attributes是不够的。一个WebGLAttribute包含了一系列顶点的属性,我们还需要一种手段指定如何使用这些顶点。可用的两种方式有:

  • gl.drawArray:直接升序使用顶点。
  • gl.drawElement:利用一个indices数组指定如何使用顶点。

因此,我们还需要一个BufferInfo,将这些信息组织在一起:

interface WebGLBufferInfo {
    numElements: number, // 顶点数量
    indices?: WebGLBuffer, // 索引,若没有则使用drawArray绘制,否则用drawElement
    attributes: Map<string, WebGLAttribute>; // 根据名字索引属性
}

ProgramInfo

ProgramInfo存储了一个WebGL程序,以及从中提取出的程序需要的Attributes与Uniforms集合。

interface WebGLProgramInfo {
    program: WebGLProgram;
    mode?: number,
    attributeSetters: { [key: string]: (info: WebGLAttribute) => void };
    uniformSetters: { [key: string]: (info: WebGLUniformType) => void };
}

attributeSettersuniformSetters指定了WebGL程序所需的attributesuniforms,在渲染时通过调用这些函数来设置程序所需的变量。

这说明在uniform中添加多余的变量是完全可以的,ProgramInfo只会使用自己需要的变量。因此uniform字段可以用来作为临时的变量存储域。

TextureInfo

TextureInfo保存了材质、材质所在的寄存器、帧缓冲及渲染缓冲:

interface WebGLTextureInfo {
    frameBuffer?: WebGLFramebuffer;
    renderBuffer?: WebGLRenderbuffer;
    texture: WebGLTexture;
    level: number;
}

其中,帧缓冲和渲染缓冲是可选的,只在材质并没有绑定在某个具体数据上时被使用。

RenderingObject

现在,我们将以上定义好的概念包装起来,构造一个WebGL里的”可渲染对象“:

class WebGLRenderingObject {
    programInfo: WebGLProgramInfo;
    bufferInfo: WebGLBufferInfo;
    uniforms: WebGLUniformMap;
    worldMatrix: MV.Matrix = MV.mat4(); // 初始为恒等矩阵
    center: MV.Vector3D = MV.vec3(); // 初始为坐标原点
    
    setModel(m: MV.Matrix);  // 设置变换矩阵 
    transform(m: MV.Matrix); // 应用变换矩阵
    draw(); // 正式绘图
}

programInfo, bufferInfouniforms标定了所有渲染所需的信息。

worldMatrixcenter标定了物体本身的运动性质,这将在uniform字段的u_WorldMatrix中体现。

最终,可以通过draw函数进行正式绘图。

OrientedObject

OrientedObject继承自RenderingObject,代表了一个有方向概念的可渲染对象。

class WebGLOrientedObject extends WebGLRenderingObject {
    sideAxis: MV.Vector3D; // 朝向 × 法线形成的第三条轴
    direction: MV.Vector3D;
    normal: MV.Vector3D;
}

其中,direction是朝向,normal是法线,sideAxis是由(朝向×法线)形成的一个侧线。这三个向量和在一起组成了一个自然坐标系。

@Vigilans
Copy link
Owner Author

Vigilans commented Jan 6, 2019

Canvas

Canvas通过封装HTMLCanvasElementWebGLRenderingContext,抽象出了一个绘图环境,用于统一管理所有绘图相关数据。

class Canvas {
    public canvas: HTMLCanvasElement;
    public gl: WebGLRenderingContext;
    public matrixStack: MatrixStack = new MatrixStack();
    public textureInfos: Array<WebGLTextureInfo> = Array(32).fill(null);
    public objectsToDraw: Array<WebGLRenderingObject> = [];
    public updatePipeline: Array<(c: Canvas, time?, deltaTime?) => void> = [];
	
    public newObject(source, mode, attributes?, uniforms?);
    public newTexture(image?, level, size);
    public render(anime?);
}

Canvas构造了一个绘图的渲染管道,介绍如下:

Initializer

这是一个尚未实现,但应存在的功能。主要作用是在正式调用render前进行一系列配置,最后在正式Rendering前对所有配置进行初始化并编译程序。

比如,先制作一个Rendering Object,接下来为其绑定光照、阴影、材质、控制器,最后调用render(),则Initializer为前述配置生成着色器代码并正式初始化,接下来才进入渲染流程。

Rendering

render函数中的主要逻辑现由一个闭包函数mainLoop实现。当需要动画时,通过在mainLoop的末尾调用requestAnimeFrame(mainLoop),即可达到效果。

一轮mainLoop在渲染阶段完成了以下工作:

  • 重置Color Buffer与Depth Buffer;
  • 遍历每个待绘制的Rendering Object:
    • 设置WebGL使用当前Object的程序;
    • 绑定当前Object的索引;
    • 设置当前Object的所有Attribute(若被重用过则跳过);
    • 设置当前Object的所有Uniform;
    • 调用Object的绘制函数。
  • 若要求有动画效果,则调用requestAnimeFrame(mainLoop)

Update Pipeline

在每帧渲染前,都会进行一轮更新流程,更新相关Rendering Object对象的状态,从而实现动态效果。

为了提高扩展性,我们将更新流程拆成一系列子过程,并用一个队列串接起来。这便是Update Pipeline机制:updatePipeline: Array<(c: Canvas, time?, deltaTime?) => void>

一轮mainLoop在更新阶段完成了以下工作:

for (let update of this.updatePipeline) {
    update(this, now, then - now);
}

也即通过Canvas的引用、当前时间以及两帧之间的时间差,轮流调用更新管道中的每个回调函数。

例如,我们可以通过以下代码,完成让光源绕Y轴持续旋转:

canvas.updatePipeline.push((cv, time, deltaTime) => {
    const R = MV.rotateY(2 * deltaTime);
    lighting.transform(R);
});

Source Loading

Canvas提供了若干个函数用于读取、创建资源:

  • sourceBy{Dom/File}:从HTML DOM/文件中读取着色器代码;
  • loadImage:异步读取图片;
  • newObject:创建一个新的Rendering Object,送至objectsToDraw中;
  • newTexture:创建一个新的Texture Info,送至textureInfos中。

目前,Model的最佳支持格式是具有WebGLAttributeMap格式的JSON。

以后从.obj.dae中读取文件的算法也可添加在这里。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants