

【WebGL】从零开始的Web端渲染器(1)——框架搭建
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.

在制作之前,首先阅读了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
Recommend
-
100
404 | 百度EUX 页面搬家啦,点击此处跳转3 s 后将自动跳转
-
52
分享创造 - @heyzxz - http://www.heyzxz.me/pcol/
-
11
IVWEB 玩转 WASM 系列-WEBGL YUV渲染图像实践最近ivweb团队在用 WASM + FFmpeg 打造一个 WEB 播放器。我们是通过写 C 语言用 FFmpeg 解码视频,通过编译 C 语言转 WASM 运行在浏览器上与 JavaScript 进行通信。默认 FFmpeg 去解码出来的数...
-
5
osu!web - WebGL & Web Audio June 14, 2015 on Drew DeVault's blog I’ve taken a liking to a video game called osu! over the pa...
-
13
golang web框架Gin入门1-环境搭建 作者: dreamfly 分类: go,个人博客 发布时间...
-
4
JELLY | WebGl渲染器小游戏实战(上)WebGl渲染器小游戏实战(上)上传日期:2021.10.25经过对GLSL的了解,以及shadertoy上各种项目的洗礼,现在开发简单交互图形应该不是一个怎么困难的问题了。下面开始来对一些已有业务逻...
-
5
WebGl渲染器小游戏实战(下)上传日期:2021.10.25经过对GLSL的了解,以及shadertoy上各种项目的洗礼,现在来尝试开发使用GLSL渲染的小游戏 承接上篇的进度,接下来这一篇说一下渲染器的实现先说一下这...
-
8
WebGL着色器渲染小游戏实战 - 凹凸实验室 - OSCHINA - 中文开源技术交流社区 经过对 GLSL 的了解,以及 shadertoy 上各种项目的洗礼,现在开发简单交互图形应该不是一个怎么困难的问题了。下面开始来对一些已有业务逻辑的项目做GLSL渲染器替换开发。
-
5
SuzhiのBlog【WebGL】从零开始的Web端渲染器(2)——添加阴影发表于2022-05-11
-
7
SuzhiのBlog【WebGL】纯手写Web端渲染器及其API发表于2022-05-10|更新...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK