2

【WebGL】从零开始的Web端渲染器(1)——框架搭建

 2 years ago
source link: https://1keven1.github.io/2022/04/11/%E3%80%90WebGL%E3%80%91%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%9A%84Web%E7%AB%AF%E6%B8%B2%E6%9F%93%E5%99%A8%EF%BC%881%EF%BC%89%E2%80%94%E2%80%94%E6%A1%86%E6%9E%B6%E6%90%AD%E5%BB%BA/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

SuzhiのBlog

【WebGL】从零开始的Web端渲染器(1)——框架搭建

发表于2022-04-11|更新于2022-05-10|技术笔记案例制作Web
字数总计:3.6k|阅读时长:18分钟|阅读量:3|评论数:0

在制作之前,首先阅读了WebGL Programming Guide,对WebGL进行了简单的学习,读书笔记:https://1keven1.github.io/2021/11/25/%E3%80%90%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0%E3%80%91WebGLProgramingGuide/

根据WebGL的学习,借鉴了Unity的表现形式后,进行了如下图的流程设计:

采用更简单的前向渲染,其中标红的为以后再实现的功能。简单来说,就是首先搭建场景,然后加载Obj模型以及Shader文件并解码编译,一切都完成后开始渲染循环。

在渲染循环中,先绘制灯光的shadowmap,然后再逐个灯光,逐个物体绘制。最后进行后处理操作。

为了提升性能,将只用计算一次的矩阵提到循环初期,比如相机和光照的VP矩阵,模型的M矩阵等。

如上图所示,简单设计了一些类,学习UE引擎,有一个Actor基类,从其上派生出Camera、Light和Mesh

Mesh包含Material和Model,Model即为OBJ模型,Material包含几个Shader以及一些参数

下面就根据设计图进行初步实现

类的初步实现

先获取canvas以及gl

const canvas = document.querySelector('canvas');
if (!canvas)
{
console.console.error('获取Canvas失败');
}
const gl = getWebGLContext(canvas);
if (!gl)
{
console.error("Get WebGL Render Context Failed");
}
let width = canvas.width;
let height = canvas.height;

Model类

传入文件以及模型缩放,然后实现load函数,加载解码Obj文件,并在加载完成后调用回调函数。

class Model{
constructor(objFile, scale=1){
this.objFile = objFile;
this.objSource = null;
this.scale = scale;

this.vertices = new Array(0);
this.texCoords = new Array(0);
this.normals = new Array(0);
this.faces = new Array(0);

this.vertexBuffer = null;
this.texcoordBuffer = null;
this.normalBuffer = null;
this.indexBuffer = null;
this.indexNum = -1;

this.bLoaded = false;
}

load(){
this.bLoaded = true;
this.loadOBJFile()
}

loadOver() {}

loadOBJFile(){
let request = new XMLHttpRequest();
request.onreadystatechange = () => {
if (request.readyState === 4 && request.status !== 404){
// 读取Obj文件
this.objSource = request.responseText;
// 分析为信息
if (!this.parseOBJ()){
console.log("分析OBJ文件失败: " + this.objFile);
}
// 解码数据为WebGL可用的Buffer
this.decodeBufferArrays();
this.loadOver()
}
}
request.open('GET', this.objFile, true);
request.send();
}
parseOBJ(){} // 解码OBJ文件
decodeBufferArrays(){} // 储存为WebGL认识的Buffer形式
}

具体的解码以及储存为Buffer函数比较复杂,请看这里:https://github.com/1keven1/WebGLRenderer/blob/main/Model.js

Shader类

传入顶点和片元着色器,实现load函数,读取编译shader,然后调用回调函数

class Shader{
constructor(vShaderFile, fShaderFile)
{
this.vShaderFile = vShaderFile;
this.fShaderFile = fShaderFile;
this.vShaderSource = null;
this.fShaderSource = null;
this.program = null;


this.a_Position = -1;
this.a_TexCoord = -1;
this.a_Normal = -1;
this.u_Matrix_MVP = null;
this.u_Matrix_M_I = null;
this.u_LightPos = null;
this.u_LightColor = null;
this.u_Matrix_Light = null;
}

load(){
this.readShaderFile()
}

loadOver() { }

readShaderFile(){
let vRequest = new XMLHttpRequest();
let fRequest = new XMLHttpRequest();

vRequest.onreadystatechange = () => {
if (vRequest.readyState === 4 && vRequest.status !== 404){
this.vShaderSource = vRequest.responseText;
if (this.vShaderSource && this.fShaderSource) this.compileShader();
}
}
fRequest.onreadystatechange = () => {
if (fRequest.readyState === 4 && fRequest.status !== 404){
this.fShaderSource = fRequest.responseText;
if (this.vShaderSource && this.fShaderSource) this.compileShader();
}
}

vRequest.open('GET', this.vShaderFile, true);
vRequest.send();
fRequest.open('GET', this.fShaderFile, true);
fRequest.send();
}

compileShader(){
this.program = createProgram(gl, this.vShaderSource, this.fShaderSource);
if (!this.program){
console.error(this.vShaderFile + ':编译失败');
}
this.loadOver();
}
}

具体createProgram函数的实现请看:https://github.com/1keven1/WebGLRenderer/blob/main/Lib/cuon-utils.js

Material类

首先新建材质类型的枚举值:

let MATERIAL_TYPE = {
OPAQUE: Symbol(0),
MASKED: Symbol(1),
TRANSLUCENT: Symbol(2),
ADDITIVE: Symbol(3)
}
Object.freeze(MATERIAL_TYPE);

在这里只考虑一个光照的情况,所以材质只包含一个base shader,后期会添加Add和Shadow Caster。同样也是实现load函数和回调函数。

class Material{
constructor(baseShader, materialType = MATERIAL_TYPE.OPAQUE, queueOffset = 0){
this.baseShader = baseShader;

this.setMaterialType(materialType, queueOffset);

this.bLoaded = false;
}

setMaterialType(materialType, offset){
switch (materialType){
case MATERIAL_TYPE.OPAQUE:
this.bDepthTest = true;
this.bCull = true;
this.bBlend = false;
this.srcFactor = gl.SRC_ALPHA;
this.desFactor = gl.ONE_MINUS_SRC_ALPHA;
this.renderQueue = 1000 + offset;
break;
case MATERIAL_TYPE.MASKED:
this.bDepthTest = true;
this.bCull = true;
this.bBlend = false;
this.srcFactor = gl.SRC_ALPHA;
this.desFactor = gl.ONE_MINUS_SRC_ALPHA;
this.renderQueue = 1500 + offset;
break;
case MATERIAL_TYPE.TRANSLUCENT:
this.bDepthTest = false;
this.bCull = true;
this.bBlend = true;
this.srcFactor = gl.SRC_ALPHA;
this.desFactor = gl.ONE_MINUS_SRC_ALPHA;
this.renderQueue = 2000 + offset;
break;
case MATERIAL_TYPE.ADDITIVE:
this.bDepthTest = false;
this.bCull = true;
this.bBlend = true;
this.srcFactor = gl.ONE;
this.desFactor = gl.ONE;
this.renderQueue = 2000 + offset;
break;
default:
this.bDepthTest = true;
this.bCull = true;
this.bBlend = false;
this.srcFactor = gl.SRC_ALPHA;
this.desFactor = gl.ONE_MINUS_SRC_ALPHA;
this.renderQueue = 1000 + offset;
console.error('材质类型不存在:' + materialType);
break;
}
}

setBlendFactor(srcFactor, desFactor){
this.srcFactor = srcFactor;
this.desFactor = desFactor;
}

load(){
this.bLoaded = true;
this.loadShader();
}

loadOver() { }

loadShader(){
// 加载Base Shader
this.baseShader.loadOver = this.shaderLoadOver.bind(this);
this.baseShader.load();
}

shaderLoadOver(){
// 初始化必要Shader变量
this.baseShader.a_Position = gl.getAttribLocation(this.baseShader.program, 'a_Position');
this.baseShader.a_TexCoord = gl.getAttribLocation(this.baseShader.program, 'a_TexCoord');
this.baseShader.a_Normal = gl.getAttribLocation(this.baseShader.program, 'a_Normal');

this.baseShader.u_Matrix_MVP = gl.getUniformLocation(this.baseShader.program, 'u_Matrix_MVP');
this.baseShader.u_Matrix_M_I = gl.getUniformLocation(this.baseShader.program, 'u_Matrix_M_I');
this.baseShader.u_LightPos = gl.getUniformLocation(this.baseShader.program, 'u_LightPos');
this.baseShader.u_LightColor = gl.getUniformLocation(this.baseShader.program, 'u_LightColor');
this.baseShader.u_Matrix_Light = gl.getUniformLocation(this.baseShader.program, 'u_Matrix_Light');

this.loadOver();
}
}

然后实现了一些工具函数,比如设置材质参数之类的,具体可以看:https://github.com/1keven1/WebGLRenderer/blob/main/Material.js

Actor基类以及子类

Transform类以及函数

function Transform(location = new Vector3([0, 0, 0]), rotation = new Vector3([0, 0, 0]), scale = new Vector3([1, 1, 1])){
this.location = location;
this.rotation = rotation;
this.scale = scale;
}

Transform.prototype.copy = function (){
let newTrans = new Transform(this.location.copy(), this.rotation.copy(), this.scale.copy());
return newTrans;
}

Actor类,实现一些对于Transform的函数

class Actor{
constructor(transform = new Transform()){
this.transform = transform;
}

getLocation(){
return this.transform.location.copy();
}

getRotation(){
return this.transform.rotation.copy();
}

getScale(){
return this.transform.scale.copy();
}

setLocation(location){
this.transform.location = location;
}

setRotation(rotation){
this.transform.rotation = rotation;
}

setLocationAndRotation(location, rotation){
this.transform.location = location;
this.transform.rotation = rotation;
}

setScale(scale)
{
this.transform.scale = scale;
}

addLocationOffset(offset){
this.transform.location.add(offset);
}

addRotationOffset(offset){
this.transform.rotation.add(offset);
}
}

Mesh类,包含一个模型和一个材质,实现了构建M矩阵的方法

class Mesh extends Actor{
constructor(transform, model, material, bCastShadow = true){
super(transform);
this.model = model;
this.material = material;
this.bCastShadow = bCastShadow;
this.mMatrix = new Matrix4();
this.mIMatrix = new Matrix4();
}

copy(){
let newMesh = new Mesh(this.transform.copy(), this.model, this.material, this.bCastShadow);

return newMesh;
}

bulidMMatrix(){
this.mMatrix.setTranslate(this.transform.location.x(), this.transform.location.y(), this.transform.location.z()).
rotate(this.transform.rotation.x(), 1, 0, 0).rotate(this.transform.rotation.y(), 0, 1, 0).rotate(this.transform.rotation.z(), 0, 0, 1).
scale(this.transform.scale.x(), this.transform.scale.y(), this.transform.scale.z());
this.mIMatrix.setInverseOf(this.mMatrix);
}
}

Light类,首先还是实现灯光类型的枚举值

let LIGHT_TYPE = {
DIRECTIONAL: Symbol(0),
POINT: Symbol(1),
SPOT: Symbol(2)
}
Object.freeze(LIGHT_TYPE);

灯光类包含灯光颜色,强度,类型,根据类型实现了VP矩阵的构建,方便后期制作shadow map使用

class Light extends Actor{
constructor(transform, lightColor, intensity, lightType = LIGHT_TYPE.DIRECTIONAL){
super(transform);
this.lightColor = lightColor;
this.intensity = intensity;
this.lightType = lightType;
this.vpMatrix = new Matrix4();
this.shadowMap = null;

if (lightType === LIGHT_TYPE.DIRECTIONAL) this.w = 0;
else this.w = 1;
}

bulidVPMatrix(){
switch (this.lightType){
case LIGHT_TYPE.DIRECTIONAL:
let rotateMatrix = new Matrix4().setRotate(this.transform.rotation.x(), 1, 0, 0).rotate(this.transform.rotation.y(), 0, 1, 0).rotate(this.transform.rotation.z(), 0, 0, 1);
let lookVec = rotateMatrix.multiplyVector3(new Vector3([0, 0, -1]));
let upVec = rotateMatrix.multiplyVector3(new Vector3([0, 1, 0]));

this.vpMatrix.setOrtho(-7, 7, -7, 7, 1, 100).
lookAt(this.transform.location.x(), this.transform.location.y(), this.transform.location.z(),
lookVec.x() + this.transform.location.x(), lookVec.y() + this.transform.location.y(), lookVec.z() + this.transform.location.z(),
upVec.x(), upVec.y(), upVec.z());
break;
case LIGHT_TYPE.POINT:
break;
case LIGHT_TYPE.SPOT:
break;
default:
break;
}
}
}

Camera类,有FOV和远近剪裁平面组成,实现了计算相机VP矩阵的方法

class Camera extends Actor{
constructor(transform, FOV = 60, nearClip = 0.1, farClip = 100){
super(transform);
this.FOV = FOV;
this.nearClip = nearClip;
this.farClip = farClip;

this.vpMatrix = new Matrix4();
}

bulidVPMatrix(){
let rotateMatrix = new Matrix4().setRotate(this.transform.rotation.x(), 1, 0, 0).rotate(this.transform.rotation.y(), 0, 1, 0).rotate(this.transform.rotation.z(), 0, 0, 1);
let lookVec = rotateMatrix.multiplyVector3(new Vector3([0, 0, -1]));
let upVec = rotateMatrix.multiplyVector3(new Vector3([0, 1, 0]));

this.vpMatrix.setPerspective(this.FOV, width / height, this.nearClip, this.farClip).
lookAt(
this.transform.location.x(), this.transform.location.y(), this.transform.location.z(),
lookVec.x() + this.transform.location.x(), lookVec.y() + this.transform.location.y(), lookVec.z() + this.transform.location.z(),
upVec.x(), upVec.y(), upVec.z());
}
}

Texture类

输入文件路径和贴图单元序号即可,同样也需要加载和加载完成的回调函数

class Texture{
constructor(texFile, texUnit, texType = gl.TEXTURE_2D)
{
this.texFile = texFile;
this.texUnit = texUnit;
this.texType = texType;

this.texture = null;
}

load(){
// 创建Texture
let texture = gl.createTexture();
if (!texture){
console.error(this.texFile + ':创建Texture失败');
}

// 创建Image
let image = new Image();

image.onload = () => {
// Write the image data to texture object
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); // Flip the image Y coordinate

gl.activeTexture(this.texUnit);
gl.bindTexture(this.texType, texture);
gl.texParameteri(this.texType, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texImage2D(this.texType, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);

this.loadOver();
};
image.src = this.texFile;

this.texture = texture;
}

loadOver() { }
}

Scene类

Scene包含着整个场景中所有资源和要渲染的Mesh,所以要实现加载所有资源的方法。如果加载后发现MeshList中某个Mesh的资源没有加载,则剔除这个Mesh并报警告信息,以防止渲染错误。

class Scene{
constructor(modelList, materialList, textureList, meshList, lightList, camera){
this.modelList = modelList;
this.materialList = materialList;
this.textureList = textureList;
this.meshList = meshList;
this.lightList = lightList;
this.camera = camera;

this.modelLoadedNum = 0;
this.materialLoadedNum = 0;
this.textureLodedNum = 0;
}

load(){
console.log('loading scene');
// 加载Model
this.modelList.forEach((model, index, arr) =>{
model.loadOver = this.modelLoadOver.bind(this);
model.load()
})
// 加载Material
this.materialList.forEach((material, index, arr) =>{
material.loadOver = this.materialLoadOver.bind(this);
material.load();
})
// 加载贴图
this.textureList.forEach((texture, index, arr) =>{
texture.loadOver = this.textureLoadOver.bind(this);
texture.load();
})
}

loadOver() { }

modelLoadOver(){
console.log('mesh load over');
this.modelLoadedNum++;
this.checkIfLoadOver();
}

materialLoadOver(){
console.log('mesh load over');
this.materialLoadedNum++;
this.checkIfLoadOver();
}

textureLoadOver(){
console.log('texture load over');
this.textureLodedNum++;
this.checkIfLoadOver();
}

checkIfLoadOver(){
if (this.modelLoadedNum === this.modelList.length &&
this.materialLoadedNum === this.materialList.length &&
this.textureLodedNum === this.textureList.length){
this.meshList.forEach((mesh, index, arr) => {
if (!mesh.model.bLoaded || !mesh.material.bLoaded){
if (!mesh.model.bLoaded) console.warn(mesh.model.objFile + ':没有加载完整');
if(!mesh.material.bLoaded) console.warn(mesh.material.baseShader.vShaderFile + ':没有加载');
this.meshList.splice(index, 1);
}
})
this.loadOver();
}
}
}

然后实现计算矩阵的方法

calculateMatrices(){
// 计算Mesh的M矩阵
this.meshList.forEach((mesh, index, arr) => {
mesh.bulidMMatrix();
})
// 计算灯光VP矩阵
this.lightList.forEach((light, index, arr) => {
light.bulidVPMatrix();
})
// 计算相机VP矩阵
this.camera.bulidVPMatrix();
}

最后也是最重要的,就是渲染方法:

render(){
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, width, height);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

this.lightList.forEach((light, lightIndex, arr) => {
this.meshList.forEach((mesh, meshIndex, arr) => {
this.drawMesh(mesh, light, lightIndex);
})
})
}

drawMesh(mesh, light, lightIndex) {
// Base Shader进行渲染
if (lightIndex === 0) {
gl.useProgram(mesh.material.getBaseProgram());

if (mesh.material.bDepthTest) gl.enable(gl.DEPTH_TEST);
else gl.disable(gl.DIPTH_TEST);

// 绑定Vertex Buffer
if (mesh.material.baseShader.a_Position >= 0) this.bindAttributeToBuffer(mesh.material.baseShader.a_Position, mesh.model.vertexBuffer);
if (mesh.material.baseShader.a_TexCoord >= 0) this.bindAttributeToBuffer(mesh.material.baseShader.a_TexCoord, mesh.model.texCoordBuffer);
if (mesh.material.baseShader.a_Normal >= 0) this.bindAttributeToBuffer(mesh.material.baseShader.a_Normal, mesh.model.normalBuffer);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, mesh.model.indexBuffer);

// 传入默认变量
if (mesh.material.baseShader.u_LightPos) gl.uniform4f(mesh.material.baseShader.u_LightPos, light.getLocation().x(), light.getLocation().y(), light.getLocation().z(), light.w);
if (mesh.material.baseShader.u_LightColor) gl.uniform4f(mesh.material.baseShader.u_LightColor, light.lightColor.x(), light.lightColor.y(), light.lightColor.z(), 1);

let mvpMatrix = new Matrix4().set(camera.vpMatrix).multiply(mesh.mMatrix);
gl.uniformMatrix4fv(mesh.material.baseShader.u_Matrix_MVP, false, mvpMatrix.elements);
gl.uniformMatrix4fv(mesh.material.baseShader.u_Matrix_M_I, false, mesh.mIMatrix.elements);

let mvpMatrixLight = new Matrix4().set(light.vpMatrix).multiply(mesh.mMatrix);
if (mesh.material.baseShader.u_Matrix_Light) gl.uniformMatrix4fv(mesh.material.baseShader.u_Matrix_Light, false, mvpMatrixLight.elements);

// 绘制
gl.drawElements(gl.TRIANGLES, mesh.model.indexNum, mesh.model.indexBuffer.dataType, 0);
}
else { }
}

bindAttributeToBuffer(a_attribute, buffer){
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.vertexAttribPointer(a_attribute, buffer.dataNum, buffer.dataType, false, 0, 0);
gl.enableVertexAttribArray(a_attribute);
}

首先是对所有灯光,然后对所有mesh,进行渲染。

drawMesh相关较为复杂,可以看这里:https://1keven1.github.io/2021/11/25/%E3%80%90%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0%E3%80%91WebGLProgramingGuide/#Hello-Cube

通过Scene构造,在外部调用Start后开始加载资源,加载后开始渲染。其中的customBeginPlay和customTick可以再外部重写以实现一些简单的逻辑。

class WebGLRenderer{
constructor(scene, editableMat = null)
{
this.scene = scene;

gl.clearColor(0, 0, 0, 1);
}

start(){
// 加载场景
this.scene.loadOver = this.startRenderLoop.bind(this);
this.scene.load();
}

customBeginPlay(){
console.warn('未重写Custom Begin Play');
}

startRenderLoop(){
this.customBeginPlay();

console.log('开始渲染循环');
let lastTime = 0;
let deltaSecond = 0;
let renderLoop = (timeStamp) => {
deltaSecond = (timeStamp - lastTime) * 0.01;
lastTime = timeStamp;

this.customTick(deltaSecond);
this.scene.calculateMatrices();
this.scene.render();
requestAnimationFrame(renderLoop);
}
renderLoop(0);
}

customTick(deltaSecond){
console.warn('未重写Custom Tick');
}
}

到这里,框架基本完成了

进行渲染测试

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>WebGL渲染器</title>
</head>
<body>
<canvas width="700" height="700">请使用支持Canvas的浏览器</canvas>
<textarea rows="50" cols="100"></textarea>

<script src="./Lib/webgl-utils.js"></script>
<script src="./Lib/webgl-debug.js"></script>
<script src="./Lib/cuon-utils.js"></script>
<script src="./Lib/cuon-matrix.js"></script>

<script src="./Actor.js"></script>
<script src="./Texture.js"></script>
<script src="./Shader.js"></script>
<script src="./Material.js"></script>
<script src="./Model.js"></script>
<script src="./Scene.js"></script>
<script src="./WebGLRenderer.js"></script>
<script src="./FunctionLibrary.js"></script>

<script src="./Main.js"></script>
</body>
</html>

构建真正运行的脚本:

let camera = new Camera(new Transform(new Vector3([0.0, 2.0, 6.0]), new Vector3([-20, 0, 0])), 60, 0.1, 100);
let light = new Light(new Transform(new Vector3([10.0, 10.0, 10.0]), new Vector3([0, 0, 0])), new Vector3([1.0, 1.0, 1.0]), 10);

let diffuseShader = new Shader('./Resource/DefaultShader/Diffuse.vert', './Resource/DefaultShader/Diffuse.frag');
let diffuseMaterial = new Material(diffuseShader, MATERIAL_TYPE.OPAQUE, 0);
let texShader = new Shader('./Resource/DefaultShader/DiffuseTex.vert', './Resource/DefaultShader/DiffuseTex.frag');
let texMaterial = new Material(texShader);

let cube = new Model('./Resource/Cube.obj');
let plane = new Model('./Resource/Plane.obj');
let sphere = new Model('./Resource/Sphere.obj')

let meshCube = new Mesh(new Transform(), cube, texMaterial);
let floor = new Mesh(new Transform(new Vector3([0, -1, 0])), plane, diffuseMaterial);

let texture = new Texture('./Resource/test.jpg', gl.TEXTURE0, gl.TEXTURE_2D);

let scene = new Scene(
[cube, sphere, plane],
[diffuseMaterial, texMaterial],
[texture],
[meshCube, floor],
[light],
camera
);

let renderer = new WebGLRenderer(scene);


renderer.customBeginPlay = () =>
{
diffuseMaterial.setUniformVector3f('u_AmbientColor', 0.2, 0.2, 0.2);
texMaterial.setUniformVector3f('u_AmbientColor', 0.2, 0.2, 0.2);
texMaterial.setTexture('u_Tex', 0);

document.onmousedown = () =>
{
meshCube.material = diffuseMaterial;
meshCube.model = sphere;
floor.material = texMaterial;
}
document.onmouseup = () =>
{
meshCube.material = texMaterial;
meshCube.model = cube;
floor.material = diffuseMaterial;
}
}

renderer.customTick = (deltaSecond) =>
{
meshCube.addRotationOffset(new Vector3([0, 1.5, 0]).multiplyf(deltaSecond));
}


renderer.start();

上面都在创建资源,然后输入到Scene中,最后传入到WebGLRenderer类里;再重写类的两个函数,在BeginPlay中设置了两个材质的参数和贴图,写了鼠标函数;在Tick中让Cube缓慢旋转,最后Start。

网址:https://1keven1.github.io/BlogSrc/WebGLRenderer/SimpleDemo.html


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK