W.js ,一个超级小的三维 WebGL 引擎的使用方法
前言
我们知道,在网页上搞三维,three.js 和 babylon.js 都是成熟最佳的选择,可是有时候它们体积又显得太大了。
我以前有研究过各种网页三维引擎,体积都挺大。我为什么要找体积小的呢?因为我对三维一窍不通,体积小让我有对它运行方式的探索欲。
我曾经找过一个最小的 https://github.com/wakufactory/wwg ,这个只有不到 10 kb 大小,但感觉有点太底层了。
后来我又找到了这个, https://github.com/xem/W/ ,它的官网是 https://xem.github.io/W/ ,这个显然做的用心多了,因为它是为极致小体积的JS游戏提供三维解决方案而设计,所以语法非常精妙,玩了一会儿,感觉很有意思。今天我就来分享一下它的使用(它官网上写的教程文档太简短了...)。
阅读它的源代码,可能需要一些 WebGL 的知识,大家可以在 https://www.cnblogs.com/duyuanshang/p/18791113 这里简单入门 ~
它的特点
它可以引入 3D 模型(OBJ 格式),上色纹理透明度也可以调,而且还可以搞灯光!总共体积只有个位数 KB 大小。
动画、索引等也支持。还不错。极致迷你版的 three.js ,不过仓库有两年没有维护过了,我真想好好的将其源码研究透,并继续迭代优化更强大和实用的功能。
它的特性是「微小」,这是它的关键特性。作者 xem 说:“我开发 W 的主要目标是针对大小受限的游戏马拉松(例如 js13kgames.com)”。
API
在官网只列出了以下几个 API,不过我觉得很够用了!把最复杂的 Webgl 里的操作封装起来,怎么都要比类似于计算器只提供加减按键要好。
// 初始化框架
W.reset(canvasElement);
// 设置清屏颜色(默认为"#fff")
W.clearColor("#rgb");
// 设置相机
// 参数:位置(x,y,z),旋转角度(rx,ry,rz),视野(fov)
W.camera({x, y, z, rx, ry, rz, fov});
// 设置光源方向
W.light({x, y, z});
// 设置环境光强度(0到1之间)
W.ambient(f);
// 创建对象组:名称,位置,旋转
W.group({n, x, y, z, rx, ry, rz});
// 绘制内置3D对象
// 参数:所属组,名称,尺寸,位置,旋转,背景,纹理,平滑度...
let settings = {g, n, size, x, y, z, rx, ry, rz, b, t, mix, s, ns, mode};
W.plane(settings); // 平面
W.billboard(settings); // 公告板
W.cube(settings); // 立方体
W.pyramid(settings); // 金字塔
W.sphere(settings); // 球体
// 添加并绘制自定义3D模型(见下方OBJ文件加载器)
W.add("custom_model", { vertices, uv, indices });
W.custom_model(settings);
// 移动/缩放组或对象:名称,位置,旋转,动画效果,延迟(毫秒)
W.move({n, size, x, y, z, rx, ry, rz, a}, delay);
// 也可以使用M参数设置自定义变换矩阵
W.move({n, M}, delay);
// 移动相机/光源:参数设置,动画效果,延迟
W.camera({x, y, z, rx, ry, rz, a}, delay);
W.light({x, y, z, a}, delay);
// 删除元素:名称,延迟
W.delete(n, delay);
入门使用
下面是一个最极简的使用:
(可以试试在 https://img1.ccgxk.com/htmleditor/index.html 这个页面调试代码)
<script src="https://img1.ccgxk.com/wjs/w.js"></script>
<canvas width="450" height="430" id=canvasElement>
<script>
onload = () => {
W.reset(canvasElement); // 初始化
W.camera({x:9,y:8,z:20,rx:-13,ry:15}); // 摄像机位置,视角 15 度
W.cube({x:5,w:10,h:.5,d:.5,b:"f44"}); // (放置)立方体
W.cube({y:5,h:10,w:.5,d:.5,b:"4f4"}); // x y z 是位置
W.cube({z:5,d:10,w:.5,h:.5,b:"44f"}); // w h 是宽高, b 是背景
W.pyramid({size:1,x:10,rz:-90,b:"f44"}); // 金字塔
W.pyramid({size:1,y:10,b:"4f4"}); // rz 等是旋转,size 是大小
W.pyramid({size:1,z:10,rx:90,b:"44f"});
// 下面是一个摄像机的模型
W.group({n:"cam"}); // 创建一个组(cam),n 代表名称
W.cube({g:"cam",w:2,h:2,d:4}); // 立方体,g 代表所属的组
W.pyramid({g:"cam",w:3,h:2,d:3,z:-3,rx:90}); // 金字塔,也属于那个组
}
</script>
我将它的核心库 w.js ,传到了我的 CDN 上:
带注释版: https://img1.ccgxk.com/wjs/w.js (23.8 kB) ,
压缩版: https://img1.ccgxk.com/wjs/min/w.min.js (9.9kb),大家感兴趣可以下载
效果如下:

三个立方体被搞成了长方体,三个金字塔体,外加一个像摄像机一样的组合(一个正方体和一个金字塔体)。
W.light({x:0,y:-1,z:0}); // 环境光的方向
W.ambient(0.22); // 环境光的强度
W.clearColor("#FFFFFF"); // 背景颜色(白色)
如果向 js 后面追加以上的代码,还可以设置环境光的方向、强度,以及背景颜色。这便是入门了!
内置模型
这个迷你的库里,甚至内置了五种最常用的模型:
- W.plane :一个平面
- W.billboard :一个始终对着摄像头的平面
- W.cube :立方体
- W.pyramid :金字塔
- W.sphere :球
这样写,就是设置一个模型了:
W.cube({x:5,w:10, h:.5,d:.5,b:"f44"});
内部的属性是这样解释的:
名称:
n就是 name ,名称g就是所属的 group 组合名
大小:
whd设置宽度、高度、纵深,默认 1size就是将 w 和 h d 设置成一样的值,默认 1
位置:
xyz是位置,默认 0 0 0rxryrz是旋转,默认 0 0 0M:自定义变换矩阵(Array(16),替换 x, y, z, rx, ry, rz)
纹理:
b是背景颜色,是一个 #rgb 颜色,默认 #888 (井号 # 可以省略)t是背景纹理,一个存在于 DOM 中的 IMG、CANVAS 或 VIDEO 元素mix是混合纹理,要求同时设置了背景颜色 b 和背景纹理 t,然后混合。0 代表完全纹理, 1 代表完全颜色
渲染:
s是平滑着色,默认为 false。这个需要完整版的库。mode绘图方式,是 webgl 里的内容,这里默认是 TRIANGLES,常用的有 LINE_LOOP 线框...ns: 默认情况下,形状将根据方向光进行着色,但可以通过此"无着色"选项禁用
这五个形状就在这里了:
<script src="https://img1.ccgxk.com/wjs/w.js"></script>
<canvas width="450" height="430" id=canvasElement>
<script>
onload = () => {
W.reset(canvasElement);
W.group({n:"g"});
W.camera({z:5});
W.light({z:-1});
W.plane({g:"g",n:"a",x:-1,y:1});
W.billboard({g:"g",n:"b",x:1,y:1});
W.cube({g:"g",n:"c",x:-1.5,y:-1});
W.pyramid({g:"g",n:"d",x:0,y:-1});
W.sphere({g:"g",n:"e",x:1.5,y:-1});
}
</script>

导入自己的模型
W 导入自己的模型很简单,第一步是将 OBJ 信息导入,第二步直接配置即可,如下所示,我把一个立方体搞了进去。
<script src="https://img1.ccgxk.com/wjs/w.js"></script>
<canvas width="450" height="430" id=canvasElement>
<script>
onload = () => {
W.reset(canvasElement);
W.group({n:"g"});
W.camera({z:5});
W.light({z:-1});
W.add("cubeobj", { // 添加一个自定义模型
vertices: [1,-1,-1,1,-1,1,-1,-1,1,-1,-1,-1,1,1,-1,1,1,1,-1,1,1,-1,1,-1],
indices: [0,1,2,0,2,3,4,7,6,4,6,5,1,5,6,1,6,2,2,6,7,2,7,3,4,0,3,4,3,7,0,4,5,0,5,1]
});
W.cubeobj({n:"M", x:2}); // 配置到视图
}
</script>
那个 OBJ 模型呢,常用的三种数据为:
W.add("model", {vertices: [...], uv: [...], indices: [...]});
vertices: 这个必须要有,是顶点数据uv:这个可选,是 uv 纹理坐标数组,如果你有纹理的话indices:如果模型使用索引缓冲区,这个就是索引数组
注意,默认采用的配置属性里,是应用的平面着色,而不是光溜溜的平滑着色。
这个网址 https://xem.github.io/W/obj2js/ 是一个工具,用于将 OBJ 文件转换为 W 自定义模型,同时也可以缩减一下体积之类的。
动画
这个是最好玩的!
<script src="https://img1.ccgxk.com/wjs/w.js"></script>
<canvas width="450" height="430" id=canvasElement>
<script>
onload = () => {
W.reset(canvasElement);
W.group({n:"g"});
W.camera({z:5});
W.light({z:-1});
W.cube({n:'testCube',x:1, rx:30}); // 绘制一个立方体
W.move({n:'testCube',x:1, rx:70, a:1000}, 3000); // 三秒后执行一个时长一秒的动画
}
</script>
W.move({}) 这个函数可以移动我们指定名称的物体,它的移动效果。
上面有两个参数,一个是 a:1000,一个是 3000,前者这个 a 如果不定义,则没有那种过渡的动画效果,也就是直接就变化了... 后面的 3000 则是动画延时,以毫秒为计。写多少,就多少毫秒后执行!
当然,不止如此,W.camera({}) 和 W.light({}) 可以直接移动摄像机和灯光:
<script src="https://img1.ccgxk.com/wjs/w.js"></script>
<canvas width="450" height="430" id=canvasElement>
<script>
onload = () => {
W.reset(canvasElement);
W.group({n:"g"});
W.camera({z:5});
W.light({z:-1});
W.cube({n:'testCube',x:1, rx:30});
W.move({n:'testCube', rx:70, a:1000}, 1000); // 一秒后旋转
W.camera({z:9, a:1000}, 3000); // 三秒后摄像头移动
W.light({z:-0.1, a:3000}, 5000); // 五秒后灯光移动
}
</script>
自定义投影矩阵
默认的投影方式是视角投影,大概意思是近小远大,符合人眼。
W.js 支持自定义投影,我们在里面可以导入我们的投影矩阵。
比如正交投影,物体无论远近,大小都看起来一样。
<script src="https://img1.ccgxk.com/wjs/w.js"></script>
<canvas id=c5 width=320 height=340></canvas>
<label><input type=radio name=p id=p1 checked>视角投影(默认)</label>
<label><input type=radio name=p id=p2>正交投影(自定义)</label>
<script>
// 投影计算函数
orthogonal = ({left, right, top, bottom, near, far }) => {
return new DOMMatrix([
2 / (right - left), 0, 0, 0,
0, 2 / (top - bottom), 0, 0,
0, 0, -2 / (far - near), 0,
-(right + left) / (right - left), -(top + bottom) / (top - bottom), -(far + near) / (far - near), 1
]);
}
onload = () => {
W.reset(c5);
W.light({x:.5,y:0,z:-.5});
W.camera({x:4.5,y:2,z:0,rx:-20,ry:30,fov:22});
W.cube({x:-2,y:-2,z:-6,b:"146"});
W.pyramid({x:0,y:-2,z:-6,b:"146"});
W.sphere({x:2,y:-2,z:-6,b:"146"});
W.cube({x:-2,y:-1,z:-8});
W.pyramid({x:0,y:-1,z:-8});
W.sphere({x:2,y:-1,z:-8});
W.cube({x:-2,y:0,z:-10,b:"16f8"});
W.pyramid({x:0,y:0,z:-10,b:"16f8"});
W.sphere({x:2,y:0,z:-10,b:"16f8"});
onchange = oninput = () => {
if(p1.checked){ // 如果是视角投影
W.camera({x:4.5,y:2,z:0,rx:-20,ry:30,fov:22});
} else { // 如果是正交投影
W.projection = orthogonal({left:-4,right:4,top:4,bottom:-4,near:1,far:99});
W.camera({x:4.5,y:2,z:0,rx:-20,ry:30});
}
}
};
</script>
在这个例子中,右下角有个单选框,可以切换投影方式:
视角投影:

正交投影:

当然,这个启用方式是手动启用的。我发现直接写到代码里,好像不怎么管用...
摄像机与主角绑定
根据官网的例子,我做了下精简:
<script src="https://img1.ccgxk.com/wjs/w.js"></script>
<canvas id=c width=320 height=300></canvas>
<table style="text-align:center;margin:10px">
<tr>
<td colspan=2>
<button id=u>前</button>
<tr>
<td>
<button id=l>左</button>
<td>
<button id=r>右</button>
<tr>
<td colspan=2>
<button id=d>后</button>
</table>
<script>
onload = () => {
keys = {u:0, l:0, r:0, d:0};
W.ambient(0.7);
W.reset(c);
W.light({x:.5,y:-.3,z:-.5});
for(i = 0; i < 10; i++){
for(j = 0; j < 10; j++){
W.billboard({size:3,x:(i-5)*5,z:(j-5)*5,ns:1, b:"555"});
}
}
W.sphere({n:"M",size:1,y:-.9, z:18,rx:-90,ry:0, b:"ccc",s:1});
W.sphere({g:"M", n:"head",size:0.6,y:0, z:0.6, rx:-90,ry:0, b:"ccc",s:1}); // 头绑定身体 M,注意,坐标以 M 为基准
W.camera({g:"M",z:.5,y:-2.5,rx:90}); // 摄像机,此时它的 g 属性为主角 sphere 的 n 属性,摄像机与主角绑定
W.clearColor("8Af");
W.plane({g:"camera",size:150,b:"3d2",z:-100,y:-75, ns:1});
X = 0;
Z = 18;
RY = 0;
onmousedown = e => {
keys[e.target.id] = 1;
}
onmouseup = e => {
keys[e.target.id] = 0;
}
setInterval(()=>{ // 很巧妙的函数,将移动的坐标记录到了变量里
if(keys.u || keys.d)
W.move({n:"M", z: Z += (-keys.u + keys.d) * Math.cos(RY*Math.PI/180) / 40, x: X += (-keys.u + keys.d) * Math.sin(RY*Math.PI/180) / 40});
if(keys.r || keys.l){
RY += (-keys.r + keys.l)/4;
W.move({n:"M", ry: RY});
}
});
}
</script>

按下前后左右,摄像机会随着球而运动。这是因为下面这两行代码将两者绑定到了一起:
W.sphere({n:"M",size:1,y:-.9,z:18,rx:-90,ry:0, b:"ccc",s:1});
W.camera({g:"M",z:.5,y:-2.5,rx:90}); // 摄像机,此时它的 g 属性为主角 sphere 的 n 属性,摄像机与主角绑定
顺便一提,W 库里面是无法访问当前某元素的坐标情况的,所以需要我们使用第三方的变量来记录。
解除绑定
为了更便于理解,这次搞的是一个「头身分离」的动作。
将下面的代码追加入上一小节的代码:
<button id="headBodySeparation">头身分离</button>
<script>
headBodySeparation.onclick = function(){
W.sphere({g:null, n:"head",size:0.6,y:0, x:X, z:Z, rx:-90,ry:RY, b:"ccc",s:1}); // 只要将 `g` 设置为 null,就等于解除绑定了
}
</script>

移动到合适的位置后,我们单击这个「头身分离」的按键,这样「头」就和「身体」解除绑定了!摄像机也一样,我们感兴趣可以尝试一下!

至此,10kb 大小的 W.js 这个三维引擎 JS 库的主要学习内容就完成了。(当然,还有个 自定义变换矩阵 的知识点,不过看起来用处不大就不说了)
W.js ,一个超级小的三维 WebGL 引擎的使用方法的更多相关文章
- js一个游戏小笔记
昨天写了个飞机大战的游戏,没弄好的一点是如何移动炮台. 开始我把移动代码写到了炮台类里面,但是怎么按都不移动.(最烦,代码对,效果不对,╮(╯▽╰)╭) 问过老师才知道,这种移动类游戏,应该把 控制 ...
- 一个简单用原生js实现的小游戏----FlappyBird
这是一个特别简单的用原生js实现的一个小鸟游戏,比较简单,适合新手练习 这是html结构 <!DOCTYPE html><html lang="en">&l ...
- 【转】利用 three.js 开发微信小游戏的尝试
前言 这是一次利用 three.js 开发微信小游戏的尝试,并不能算作是教程,只能算是一篇笔记吧. 微信 WeChat 6.6.1 开始引入了微信小游戏,初期上线了一批质量相当不错的小游戏.我在查阅各 ...
- pygame学习笔记(6)——一个超级简单的游戏
转载请注明:@小五义 http://www.cnblogs.com/xiaowuyi 学了这么长时间的Pygame,一直想写个游戏实战一下.看起来很简单的游戏,写其来怎么这么难.最初想写个俄罗斯方块 ...
- 手把手教你写一个RN小程序!
时间过得真快,眨眼已经快3年了! 1.我的第一个App 还记得我14年初写的第一个iOS小程序,当时是给别人写的一个单机的相册,也是我开发的第一个完整的app,虽然功能挺少,但是耐不住心中的激动啊,现 ...
- 【AngularJS】—— 3 我的第一个AngularJS小程序
通过前面两篇的学习,基本上对AngularJS的使用有了一定的了解. 本篇将会自己手动写一个小程序,巩固下理解. 首先要注意的是,引用AngularJS的资源文件angular.min.js文件. 由 ...
- js 创建书签小工具之理论
我们一直在寻找增加浏览体验的方法,有的方法众所周知,有的则鲜为人知.我原本认为书签小工具属于后者,非常令人讨厌的东西.令我非常懊恼的是我发现在这个问题上我完全是错误的.它并不是令人厌烦的,而是以用户为 ...
- 分享一个超级好用的php程序员工具箱
分享一个超级好用的php程序员工具箱,是由php中文网开发的. 集合了php环境搭建.在线小工具.原生手册.文字与视频教程.问答社区等 (php程序员工具箱 v0.1版本,点此下载:http://ww ...
- 用js实现2048小游戏
用js实现2048小游戏 笔记仓库:https://github.com/nnngu/LearningNotes 1.游戏简介 2048是一款休闲益智类的数字叠加小游戏.(文末给出源代码和演示地址) ...
- 用Vue.js开发微信小程序:开源框架mpvue解析
前言 mpvue 是一款使用 Vue.js 开发微信小程序的前端框架.使用此框架,开发者将得到完整的 Vue.js 开发体验,同时为 H5 和小程序提供了代码复用的能力.如果想将 H5 项目改造为小程 ...
随机推荐
- kvm实验环境的准备
在虚拟机上最小化安装centos7,看一下我的版本 [root@kvm1 yum.repos.d]# cat /etc/redhat-release CentOS Linux release 7.9. ...
- [国家集训队] Tree2 题解
加边删边 \(LCT\),标记下放同 \(luogu\) 线段树 \(2\) 一题. 时间复杂度 \(O(n\log n)\),第一次交的时候我维护 \(sum\) 不维护 \(sz\ WA\) 完了 ...
- LVGL 8.3.0常用函数快速使用
LVGL 8.3.0使用快速上手教程(使用篇) 定义页面通用样式style // 创建页面样式 static lv_style_t style; lv_style_init(&style); ...
- ZLMediaKit: 快速入门
目录 ZLMediaKit是什么 编译 安装依赖库 构建项目 问题处理 问题1: srtp 未找到, WebRTC 相关功能打开失败 问题2:依赖库问题 安装 配置和运行 问题处理 问题1: 无权限监 ...
- 【2022_12_2】Fibersim安装记录
Fibersim 安装记录 1. 为什么要写这个文章? 因为我前前后后装了四天才装成功.在我的电脑上,fibersim14 16 17 15 挂到UG10 12 CatiaV5-6R2019 2018 ...
- Navicat 如何将表恢复默认状态下
场景: 测试一套流程后,造测试数据非常麻烦的情况下,如何通过更改数据库为默认情况即初始表数据 案例: 比如表原有结构如下图(一) 修改后数据如下图(二): 需求:将图二数据恢复到图一内容下 操作思想: ...
- RSA算法详解及相关数学原理解析
RSA算法详解及相关数学原理解析 前言 为了记录自己学习密码学的过程,也是为了便于个人应付相关课程的考核,故写此博客. 本博客总结了怎么用C++手搓一个RSA算法,以及补补欠缺的一些数学知识和可能 ...
- 超级详细的mysql数据库安装指南
https://zhuanlan.zhihu.com/p/37152572 2,073 人赞同了该文章 如果你的电脑是mac,参考社群会员 @奔跑的土豆 的分享: mac下mysql的安装步骤 227 ...
- golang结构体判断是否为空
前言 使用任何编程语言都会遇到判空的问题,那么Golang对于自定义的结构体类型如何判空呢? 其实空结构体可不是简单的与nil做比较哦.请看下面两种方法: package main import ( ...
- 妙用PHP函数处理数组
PHP的数组是一种很强大的数据类型,与此同时PHP内置了一系列与数组相关的函数可以轻松地实现日常开发功能. 1. 取数组指定键名列 对于某些关联数组,有时候我们只想取指定键名的那部分,比如数组为 [' ...