HTML5 2D游戏引擎研发系列 第八章 <Canvas技术篇-基于顶点绘制与变形>
~\(≥▽≤)/~HTML5游戏开发者社区(群号:326492427)
- HTML5 2D游戏引擎研发系列 第一章 <一切的开始>
- HTML5 2D游戏引擎研发系列 第二章 <磨剑>
- HTML5 2D游戏引擎研发系列 第三章 <Canvas技术篇-画布技术-显示图片>
- HTML5 2D游戏引擎研发系列 第四章 <Canvas技术篇-画布技术-基于手动切片动画>
- HTML5 2D游戏引擎研发系列 第五章 <Canvas技术篇-画布技术-纹理集复杂动画>
- HTML5 2D游戏引擎研发系列 第六章 <Canvas技术篇-画布技术-混色特效和粒子>
- HTML5 2D游戏引擎研发系列 第七章 <Canvas技术篇-画布技术-鼠标侦听器>
- HTML5 2D游戏引擎研发系列 第八章 <Canvas技术篇-基于顶点绘制与变形>
- WEBGL 2D游戏引擎研发系列 第一章 <新的开始>
- WEBGL 2D游戏引擎研发系列 第二章 <显示图片>
- WEBGL 2D游戏引擎研发系列 第三章 <正交视口>
- WEBGL 2D游戏引擎研发系列 第四章 <感想以及矩阵>
- WEBGL 2D游戏引擎研发系列 第五章 <操作显示对象>
- WEBGL 2D游戏引擎研发系列 第六章 <第一次封装>
- WEBGL 2D游戏引擎研发系列 第七章 <混色>
- WEBGL 2D游戏引擎研发系列 第八章 <批处理(影分身之术)>
- WEBGL 2D游戏引擎研发系列 第九章 <基于UV的模拟切片>
- WEBGL 2D游戏引擎研发系列 第十章 <纹理集动画>
- WEBGL 2D游戏引擎研发系列 第十一章 <着色器-shader>
- WEBGL 2D游戏引擎研发系列 第十二章 <粒子发射器>
- WEBGL 2D游戏引擎研发系列 第十三章 <Shader分享>
- 游戏技法 2D游戏引擎研发系列 第一章 <优化技巧>
- 游戏技法 2D游戏引擎研发系列 第二章 <计时器>
- 游戏技法 2D游戏引擎研发系列 第三章 <基于UV的无缝图像滚动>
- 游戏技法 2D游戏引擎研发系列 第四章 <基于形状的碰撞检测>
HI,大家好,今天本章节的内容属于扩展篇,利用它你可以做出非常炫酷的伪3D效果,或者动态的水流,小河等等,回想看看,我们之前所使用的canvas的绘图技术都是绘制一张矩形的图片,虽然我们可以对它们进行缩放旋转等一些变换,如果是要基于某个顶点的变形可能不行了,而真正的3D技术的图像都是通过顶点的组合来绘制出漂亮的画面的,今天我们就来学习学习如何在canvas里利用矩阵的变换来实现这个效果,很抱歉,我不是故意要说得这么复杂,如果你还不太理解矩阵的话,我可能需要整理思路重新写一篇教程,而且有可能把我自己搞晕,但是再后面的章节中我们会很频繁的接触矩阵,实践是最好的学习,所以我希望通过一系列的文章和实践告诉你为何要使用矩阵,现在,如果你不太懂不要紧的,我告诉你一个技巧如何去学习他们,首先是使用,再是理解,也就是说有很多公式和原理你虽然不懂,但你至少要保证知道如何使用,而且你可以把他们收集起来当作你的工具库,这也就是为何要自己写引擎的目的,引擎是一个全面的架构,你可以把自己学到的知识通过引擎存储下来,方便你在任何平台快速的找到立柱之地,好了,说了这么说只是希望你不要对复杂的技术产生抵触,因为之后可能越来越复杂,但是你的收获也会越来越多.
要实现本章的效果我们可以分为2个部分,一个部分是基于3个顶点去绘制一个图形,另外一个部分是控制这3个顶点,这里有一个新的API可以给你使用createPattern,它替代了之前的直接传入图片的方法,利用它我们可以绘制一系列的路径之后用图片填充这个路径里面的内容,所以我们可以绘制一个三角形,然后用图片去填充,具体的解释你可以参考W3School,之后就是绘制路径的API,他们组合起来是这样的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
//获取图片填充的句柄
this
.imgPattern = context.createPattern(img,
"repeat-y"
);
//重置路径
context.beginPath();
//设置绘制的句柄
context.fillStyle =
this
.imgPattern;
//绘制路线,第一个点
context.moveTo(
this
.drawPoint1.x,
this
.drawPoint1.y);
//绘制路线第二个点
context.lineTo(
this
.drawPoint2.x,
this
.drawPoint2.y);
//绘制路线第三个点
context.lineTo(
this
.drawPoint3.x,
this
.drawPoint3.y);
//结束路径
context.closePath();
//开始填充
context.fill();
|
这段代码是针对原图的坐标开始填充图形,如果没有问题,这段代码执行的效果应该是这样
好了,很简单,第一步就这样完成了,第二步就是变形了,其实我们让三角形做某个顶点的拉升实际上并不是真的对顶点拉升了,而是针对这整个图形的一个变换操作,这个概念很重要,为了验证这个理论,你可以打开你电脑上的任何绘图软件,然后绘制一个三角形,然后再任意创建3个点,你可以通过你手动操作这个图形的倾斜缩放缩放操作而让这个图形的3个点完全吻合你创建的3个点,现在,我们再使用一个新的API transform,它可以直接设置矩阵变换的操作,这样就非常方便我们,详细的解释你依然可以参考W3school,现在我们要做的事情就是把顶点变换转换成矩阵变换,最后把矩阵变换的值通过这个transform直接输入到绘图系统里,这里是很关键的一个部分,我们如果要对一个三角形变形是需要2组坐标的,或者你可以理解为A组坐标变换到B组坐标的矩阵变换,而且A组坐标永远是初始化坐标,不然你执行的每一次变换都会累加,所以,你现在可以创建2组坐标,一组是绘制三角形的初始化坐标,另外一组是实际三角形变换的坐标,比如这样:
1
2
3
4
5
6
7
8
9
|
//绘制顶点
this
.drawPoint1=
null
;
this
.drawPoint2=
null
;
this
.drawPoint3=
null
;
//移动顶点
this
.movePoint1=
null
;
this
.movePoint2=
null
;
this
.movePoint3=
null
;
|
然后你需要一个变换的公式,通过这2组坐标求出变换出的矩阵,比如这样,这个方法接受一个数组,前三个元素为第一组坐标,后三个元素为第二组坐标,然后通过他们计算出之间的变化矩阵
1
2
3
4
5
6
7
8
9
10
11
12
|
//通过2组坐标点,参数接受一个数组,前三个元素为第一组坐标,后三个元素为第二组坐标,然后计算出他们之间的变换矩阵
function
drawMatrix(array) {
var
matrixList = [
((array[2].y - array[0].y)*(array[4].x - array[3].x) - (array[1].y-array[0].y)*(array[5].x-array[3].x))/((array[2].y-array[0].y)*(array[1].x-array[0].x)-(array[2].x-array[0].x)*(array[1].y-array[0].y)),
((array[2].y - array[0].y)*(array[4].y - array[3].y) - (array[1].y-array[0].y)*(array[5].y-array[3].y))/((array[2].y-array[0].y)*(array[1].x-array[0].x)-(array[2].x-array[0].x)*(array[1].y-array[0].y)),
((array[2].x - array[0].x)*(array[4].x - array[3].x) - (array[1].x-array[0].x)*(array[5].x-array[3].x))/((array[1].y-array[0].y)*(array[2].x-array[0].x)-(array[2].y-array[0].y)*(array[1].x-array[0].x)),
((array[2].x - array[0].x)*(array[4].y - array[3].y) - (array[1].x-array[0].x)*(array[5].y-array[3].y))/((array[1].y-array[0].y)*(array[2].x-array[0].x)-(array[2].y-array[0].y)*(array[1].x-array[0].x)),
((array[4].x*array[0].x - array[3].x*array[1].x)*(array[2].y*array[0].x-array[0].y*array[2].x)-(array[5].x*array[0].x-array[3].x*array[2].x)*(array[1].y*array[0].x-array[0].y*array[1].x))/((array[0].x-array[1].x)*(array[2].y*array[0].x-array[0].y*array[2].x)-(array[0].x-array[2].x)*(array[1].y*array[0].x-array[0].y*array[1].x)),
((array[4].y*array[0].x - array[3].y*array[1].x)*(array[2].y*array[0].x-array[0].y*array[2].x)-(array[5].y*array[0].x-array[3].y*array[2].x)*(array[1].y*array[0].x-array[0].y*array[1].x))/((array[0].x-array[1].x)*(array[2].y*array[0].x-array[0].y*array[2].x)-(array[0].x-array[2].x)*(array[1].y*array[0].x-array[0].y*array[1].x))
];
return
matrixList;
}
|
现在打开你的DisplayObject.js把上面3个部分加入到系统里去,先加入新的成员变量:
1
2
3
4
5
6
7
8
9
10
11
12
|
//图片填充样式句柄
this
.imgPattern=
null
;
//绘制顶点
this
.drawPoint1=
null
;
this
.drawPoint2=
null
;
this
.drawPoint3=
null
;
//移动顶点
this
.movePoint1=
null
;
this
.movePoint2=
null
;
this
.movePoint3=
null
;
|
然后是一个点对象,用来存储我们的坐标点:
1
2
3
4
5
6
|
//用于存储坐标点的数据类
function
Point(x,y)
{
this
.x=x;
this
.y=y;
}
|
还记得isPlay参数吗,之前我们扩展到2,现在要加入到3了,比如这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
|
//动画类的重绘函数
this
.paint=
function
()
{
if
(
this
.visible)
{
this
.upFrameData();
//保存画布句柄状态
context.save();
//加入混色功能
context.globalCompositeOperation=
this
.blend;
//更改画布句柄的透明度,从这以后绘制的图像都会依据这个透明度
context.globalAlpha=
this
.alpha;
//设置画布句柄的位置,实际上是设置的图像的位置
context.translate(
this
.x,
this
.y);
//设置画布旋转角度实际上是设置了图像的角度,参数是弧度,所以我们必须把角度转换为弧度
context.rotate(
this
.rotation*Math.PI/180);
//设置画布句柄的比例,实际上是设置了图像的比例
context.scale(
this
.scaleX,
this
.scaleY);
switch
(
this
.isPlay)
{
case
1:
context.drawImage(img,
this
.mcX,
this
.mcY,
this
.frameWidth,
this
.frameHeight,-
this
.frameWidth/2,-
this
.frameHeight/2,
this
.frameWidth,
this
.frameHeight);
break
;
case
2:
//增加了帧信息的偏移量,和最后一段的动画实际宽度和高度
context.drawImage(img,
this
.mcX,
this
.mcY,
this
.width,
this
.height,
-(
this
.frameX)-
this
.frameWidth/2,
-(
this
.frameY)-
this
.frameHeight/2
,
this
.width,
this
.height);
break
case
3:
//把初始点也就是绘制点填入第一组坐标,目标点填入第二组坐标,并且存储矩阵变换的参数
var
list=drawMatrix([
this
.drawPoint1,
this
.drawPoint2,
this
.drawPoint3,
this
.movePoint1,
this
.movePoint2,
this
.movePoint3
]);
//按照循序输入矩阵变换的循序
context.transform(
list[0],
list[1],
list[2],
list[3],
list[4],
list[5]);
//获取图片填充的句柄
if
(
this
.imgPattern==
null
)
this
.imgPattern = context.createPattern(img,
"repeat-y"
);
//重置路径
context.beginPath();
//设置绘制的句柄
context.fillStyle =
this
.imgPattern;
//绘制路线,第一个点
context.moveTo(
this
.drawPoint1.x,
this
.drawPoint1.y);
//绘制路线第二个点
context.lineTo(
this
.drawPoint2.x,
this
.drawPoint2.y);
//绘制路线第三个点
context.lineTo(
this
.drawPoint3.x,
this
.drawPoint3.y);
//结束路径
context.closePath();
//开始填充
context.fill();
break
;
default
:
context.drawImage(img,
this
.mcX,
this
.mcY,
this
.frameWidth,
this
.frameHeight,-
this
.frameWidth/2,-
this
.frameHeight/2,
this
.frameWidth,
this
.frameHeight);
break
;
}
//最后返回画布句柄的状态,因为画布句柄是唯一的,它的状态也是唯一的,当我们使用之后方便其他地方使用所以
//需要返回上一次保存的状态,就好像什么事情都没有发生过
context.restore();
}
}
|
好了,系统的扩展结束了,让我们回到Main.js看看如何使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
//创建一个绘制对象
drawMc=
new
MovieClip2D(imageStart[2]);
//绘制模式为3
drawMc.isPlay=3;
//初始化绘制点坐标
drawMc.drawPoint1=
new
Point(300,0);
drawMc.drawPoint2=
new
Point(300+300,300);
drawMc.drawPoint3=
new
Point(0,300);
//让三角形目标点等于3个代理的点,之后只要我们对这3个代理点进行移动就改变这个图形
drawMc.movePoint1=mc_1;
drawMc.movePoint2=mc_2;
drawMc.movePoint3=mc_3;
//显示它
stage2d.addChild(drawMc);
|
为了偷点懒,我就利用了上一章节的结束素材当作背景和拖拽点了,然后注册鼠标的相关事件,Main.js完整代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
|
//需要加载的图像资源
var
imageAddress=[
"bg.jpg"
,
"eff.png"
,
"test.jpg"
];
//需要加载的XML对象池
var
xmlAddress=[
"eff.xml"
];
//已经加载好的图像资源池
var
imageStart=[];
//XML已经加载好的对象池
var
xmlStart=[];
//当前加载资源的ID
var
imageId=0;
//XML加载ID
var
xmlid=0;
//定义场景管理器
var
stage2d;
//全局定义3个拖拽点
var
mc_1;
var
mc_2;
var
mc_3;
//全局定义个三角形绘制对象
var
drawMc;
//定义一个当前点击的对象
var
targetMc;
//加载回调函数
this
.loadImag=
function
()
{
var
imageObj=
new
Image();
imageObj.src=imageAddress[imageId];
imageObj.onload=
function
(){
if
(imageObj.complete==
true
){
imageStart.push(imageObj);
imageId++;
if
(imageId<imageAddress.length)
{
loadImag();
}
if
(imageId==imageAddress.length)
{
loadXml();
}
}
}
}
this
.loadXml=
function
()
{
var
load=
new
AnimationXML();
this
.initer=
function
(e)
{
xmlStart.push(load);
xmlid++;
if
(xmlid<xmlAddress.length)
{
loadXml();
}
if
(xmlid==xmlAddress.length)
{
init();
}
}
load.addEventListener(
this
.initer);
load.createURL(xmlAddress[xmlid]);
}
window.onload=
function
(){
this
.loadImag();
}
//初始化函数
function
init () {
//创建场景管理器
stage2d=
new
Stage2D();
//启用场景逻辑
stage2d.init();
var
bc=
new
MovieClip2D(imageStart[0]);
bc.isPlay=1;
bc.x=1024/2;
bc.y=768/2;
bc.frameWidth=imageStart[0].width;
bc.frameHeight=imageStart[0].height;
stage2d.addChild(bc);
mc_1=
new
MovieClip2D(imageStart[1],xmlStart[0].quadDataList);
mc_1.isPlay=2;
mc_1.x=1024/2;
mc_1.y=768/2-200;
mc_1.scene(
"eff_5"
)
mc_1.blend=
"lighter"
;
stage2d.addChild(mc_1);
mc_2=
new
MovieClip2D(imageStart[1],xmlStart[0].quadDataList);
mc_2.isPlay=2;
mc_2.x=1024/2+300;
mc_2.y=768/2+300-200;
mc_2.scene(
"eff_5"
)
mc_2.blend=
"lighter"
;
stage2d.addChild(mc_2);
mc_3=
new
MovieClip2D(imageStart[1],xmlStart[0].quadDataList);
mc_3.isPlay=2;
mc_3.x=1024/2-300
mc_3.y=768/2+300-200;
mc_3.scene(
"eff_5"
)
mc_3.blend=
"lighter"
;
stage2d.addChild(mc_3);
//创建一个绘制对象
drawMc=
new
MovieClip2D(imageStart[2]);
//绘制模式为3
drawMc.isPlay=3;
//初始化绘制点坐标
drawMc.drawPoint1=
new
Point(300,0);
drawMc.drawPoint2=
new
Point(300+300,300);
drawMc.drawPoint3=
new
Point(0,300);
//让三角形目标点等于3个代理的点,之后只要我们对这3个代理点进行移动就改变这个图形
drawMc.movePoint1=mc_1;
drawMc.movePoint2=mc_2;
drawMc.movePoint3=mc_3;
//显示它
stage2d.addChild(drawMc);
//注册鼠标按下侦听器
stage2d.addEventListener(
new
Event2D(EVENT_MOUSE_DOWN,downFun));
//注册鼠标移动侦听器
stage2d.addEventListener(
new
Event2D(EVENT_MOUSE_MOVE,moveFun));
//注册鼠标松开侦听器
stage2d.addEventListener(
new
Event2D(EVENT_MOUSE_UP,upFun));
}
function
upFun(e)
{
targetMc=
null
}
function
moveFun(e)
{
//如果移动鼠标时点击对象不为空,说明我们正在拖拽一个对象
if
(targetMc!=
null
)
{
//让这个对象的XY等于鼠标的XY
targetMc.x=stage2d.mouseX;
targetMc.y=stage2d.mouseY;
}
//然后设置绘制图形的目标点,这里是一个小技巧,这是其他语言做不到的,因为我们并没有定义类型,所以可以直接把MovieClip2D对象当作Point对象设置,因为他们都有共同的属性xy
drawMc.movePoint1=mc_1;
drawMc.movePoint2=mc_2;
drawMc.movePoint3=mc_3;
}
function
downFun(e)
{
//获取当前点击的对象
targetMc=e;
}
|
如果没有报错,你运行项目看到的内容应该是这样了,拖拽画面上的任意火球可以控制图像的其中一个顶点
转载请注明:HTML5游戏开发者社区 » HTML5 2D游戏引擎研发系列 第八章