我觉得对于完3D的新手来说,最大的成就感就是自己建模,然后让他动起来,如果觉得有问题,就再加一个自己学会写Shader。那么这个教程就是为了让大家能够快速的上手,创建自己的3D模型,并让通过Shader它动起来。
快速开始
-
你需要一个Cocos Creator,我这里用的是3.8.2版本
-
了解下创建网格的接口:官方文档
utils.MeshUtils.createMesh
这个接口是用来创建网格的,你可以通过这个接口创建一个网格,绑定到节点的MeshRender的mesh属性上,就可以看到你的模型了。
为了方便,我封装了一个网格创建的工具类(MeshCreator
),我们可以直接使用这个工具类来创建网格,文档最后会附加项目文件,你可以直接下载下来,然后直接使用。
起点:显示一个三角形
三角形是3D世界的基本单位,我们先从一个三角形开始,然后再慢慢的扩展到更多的三角形,最后就是一个完整的模型了。
private createTriangle() {
this._meshCreator.reset();
let p0 = vec3Pool.alloc().set(0, 0, 0);
let p1 = vec3Pool.alloc().set(1, 0, 0);
let p3 = vec3Pool.alloc().set(0, -1, 0);
this._meshCreator.verticles.push(p0 , p1, p3);
let uv0 = vec2Pool.alloc().set(0, 0);
let uv1 = vec2Pool.alloc().set(1, 0);
let uv2 = vec2Pool.alloc().set(0, 1);
this._meshCreator.uvs.push(uv0, uv1, uv2);
this._meshCreator.indices.push(0, 2, 1);
this._meshRenderer.mesh = this._meshCreator.buildMesh();
}
代码看起来是不是很简单,我们只需要创建三个点,然后创建三个uv,最后创建一个三角形,然后就可以看到一个三角形了,如果你不用显示贴图的话,甚至于可以不用计算uv,乱写一个就行了。
其实创建网格还有很多参数,这里我们都可以先忽略它们!
进阶:显示一个正方形
正方形是两个三角形组成的,我们只需要创建两个三角形,然后组合在一起,就可以显示一个正方形了。
private createQuad() {
this._meshCreator.reset();
let p0 = vec3Pool.alloc().set(0, 0, 0);
let p1 = vec3Pool.alloc().set(1, 0, 0);
let p2 = vec3Pool.alloc().set(1, -1, 0);
let p3 = vec3Pool.alloc().set(0, -1, 0);
this._meshCreator.verticles.push(p0, p1, p2, p3);
let uv0 = vec2Pool.alloc().set(0, 0);
let uv1 = vec2Pool.alloc().set(1, 0);
let uv2 = vec2Pool.alloc().set(1, 1);
let uv3 = vec2Pool.alloc().set(0, 1);
this._meshCreator.uvs.push(uv0, uv1, uv2, uv3);
this._meshCreator.indices.push(0, 2, 1, 0, 3, 2);
this._meshRenderer.mesh = this._meshCreator.buildMesh();
}
与三角形类似,我们只需要创建四个点,然后创建四个uv,最后创建两个三角形,然后就可以看到一个正方形了。
聪明的你可能已经发现了,正方形虽然是两个三角形组成的,但是我们只需要创建一个正方形的四个点,然后通过索引来组合两个三角形,这样就可以减少重复的点了。
看代码还是太累,这里用图片来表示一下顶点:
终极:显示一个飘动的旗帜
旗子其实就是一个长方形,然后通过Shader来让它动起来,唯一不同的是,我们需要给它多个顶点,然后通过Shader控制各个顶点,让它动起来,从而看起来像是飘动的旗帜。
private buildMesh() {
this._dirty = DirtyType.None;
this._meshCreator.reset();
const width = this._size.x;
const height = this._size.y;
const verticePreUnit = this._verticePreUnit;
const widthSegments = Math.floor(width * verticePreUnit);
const heightSegments = Math.floor(height * verticePreUnit);
for(let j = 0; j <= heightSegments; j++) {
const y = - j / heightSegments * height;
for(let i = 0; i <= widthSegments; i++) {
const x = i / widthSegments * width;
const v = vec3Pool.alloc();
v.set(x, y, 0);
this._meshCreator.verticles.push(v);
const uv = vec2Pool.alloc();
uv.set(i / widthSegments, j / heightSegments);
this._meshCreator.uvs.push(uv);
}
}
for(let j = 0; j < heightSegments; j++) {
for(let i = 0; i < widthSegments; i++) {
const a = i + (widthSegments + 1) * j;
const b = i + (widthSegments + 1) * (j + 1);
const c = (i + 1) + (widthSegments + 1) * (j + 1);
const d = (i + 1) + (widthSegments + 1) * j;
this._meshCreator.indices.push(a, b, c);
this._meshCreator.indices.push(a, c, d);
}
}
this._meshRenderer.mesh = this._meshCreator.buildMesh();
}
网格的创建没什么再说的了,但是这里要注意需要精确计算uv坐标,保证纹理显示正确;
而且在shader中需用通过uv坐标来控制顶点的位置,从而让旗帜动起来,简单的来说就是通过uv的横坐标控制顶点偏移的强度,毕竟靠近旗杆的地方是不会动的,而靠近旗尾的地方是会动的。
我们通过shader显示一下旗帜UV水平方向上的变化,可以看到离旗杆越远,旗帜就会动的越强烈:
vec4 SurfacesFragmentModifyBaseColorAndTransparency()
{
return vec4(ALBEDO_UV.x, 0.0, 0.0, 1.0);
}
至此,给网格添加材质普通的材质球后,我们就有了一个静态的旗帜,接下来就是通过Shader让它动起来了。
进化:让旗帜动起来
让旗子动起来很简单,只需要在Cocos Surface Shader的函数SurfacesVertexModifyWorldPos
中修改顶点的位置即可,最简单的方式就是给一个sin,让他随时间的变化平移顶点的位置,这样就可以让旗帜动起来了。
vec3 SurfacesVertexModifyWorldPos(in SurfacesStandardVertexIntermediate In)
{
In.worldPos += a_normal * sin(cc_time.w + In.worldPos.x);
return In.worldPos;
}
效果如下:
好吧,一言难尽,确实是动起来了,但是也飞起来了,旗杆完全没达到约束他的效果,我们用上文提到的uv来控制顶点的偏移,再试试效果:
vec3 SurfacesVertexModifyWorldPos(in SurfacesStandardVertexIntermediate In)
{
In.worldPos += a_normal * sin(cc_time.w + In.worldPos.x) * a_texCoord.x;
return In.worldPos;
}
效果如下:
这次的效果明显好多了,但是还是有点问题,旗帜的波动不够自然,我们可以通过增加一张噪声贴图来让它更自然一些,这里我用了一个简单的噪声贴图,你可以用更复杂的噪声贴图来让它更自然一些,当然这里需要再顶点着色器中做纹理采样,可能有些低端手机或者平台不支持,所以需要注意一下。
当然,你也可以通过更复杂的算法来模拟噪声:
vec3 SurfacesVertexModifyWorldPos(in SurfacesStandardVertexIntermediate In)
{
float noise = texture2D(a_noiseMap, a_texCoord).r;
In.worldPos += a_normal * sin(cc_time.w + In.worldPos.x) * a_texCoord.x * noise;
return In.worldPos;
}
效果如下:
有了噪声贴图,旗帜的波动就更自然了, 但是有些小问题,我再次修改下代码,形成最终的效果:
vec3 SurfacesVertexModifyWorldPos(in SurfacesStandardVertexIntermediate In)
{
float t = sin(cc_time.w * speed + In.worldPos.x);
vec3 oldPos = In.worldPos;
In.worldPos += a_normal * t * height * a_texCoord.x;
vec2 uv = a_texCoord + vec2(t * 0.3, 0.0);
vec4 noise = texture(noiseMap, uv);
In.worldPos -= noise.r * height * a_texCoord.x;
return In.worldPos;
}
效果如下:
效果其实还是有问题,在增加旗帜的接收投影后,旗子上有些奇怪的阴影,这个问题我还没有解决,如果你有解决方案,欢迎留言。
总结
看到没有,从头到尾,我们只关注两件事:
-
创建网格
网格我已经通过工具类封装好了,你只需要调用接口,传入参数,就可以创建一个网格了。
-
编写Shader
而Shader我们从头到尾值关注了一个函数
SurfacesVertexModifyWorldPos
,这个函数是用来修改顶点的位置的,我们只需要在这个函数中修改顶点的位置,就可以让模型动起来了。
所以,3D绘制其实是很简单的,只要你掌握了这两个点,你就可以做出很多很多的东西了,这里只是一个简单的例子,你可以通过这个例子,去尝试更多的东西,比如更复杂的模型,更复杂的Shader,更复杂的动画,这些都是你可以尝试的,希朥这个教程能够帮助到你,也希望你能够享受这个过程。
不知道你有没有发现,从头绘制一个3D模型,并且给予它生命一般的动画,这个过程是多么的有趣,这个过程就像是一个创造者,你可以创造出你想要的一切,这种感觉是多么的美妙,希望你也能够感受到这种美妙。
项目文件
项目文件 (282.1 KB)
文件下载后,直接解压放到你的工程资源文件夹下即可。