HTML5 2D游戏引擎研发系列 第八章

HTML5 2D游戏引擎研发系列 第八章 <Canvas技术篇-基于顶点绘制与变形>

~\(≥▽≤)/~HTML5游戏开发者社区(群号:326492427)

 

            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();

这段代码是针对原图的坐标开始填充图形,如果没有问题,这段代码执行的效果应该是这样

QQ截图20130924235444

 

好了,很简单,第一步就这样完成了,第二步就是变形了,其实我们让三角形做某个顶点的拉升实际上并不是真的对顶点拉升了,而是针对这整个图形的一个变换操作,这个概念很重要,为了验证这个理论,你可以打开你电脑上的任何绘图软件,然后绘制一个三角形,然后再任意创建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;
  
}

如果没有报错,你运行项目看到的内容应该是这样了,拖拽画面上的任意火球可以控制图像的其中一个顶点

QQ截图20130925003302

QQ截图20130925003321

QQ截图20130925003332

 

在线演示 源码下载

转载请注明:HTML5游戏开发者社区 » HTML5 2D游戏引擎研发系列 第八章

 

CSDN海神之光上传的代码均可运行,亲测可用,直接替换数据即可,适合小白; 1、代码压缩包内容 主函数:main.m; 调用函数:其他m文件;无需运行 运行结果效果图; 2、代码运行版本 Matlab 2019b或2023b;若运行有误,根据提示修改;若不会,私信博主; 3、运行操作步骤 步骤一:将所有文件放到Matlab的当前文件夹中; 步骤二:双击打开main.m文件; 步骤三:点击运行,等程序运行完得到结果; 4、仿真咨询 如需其他服务,可私信博主或扫描博客文章底部QQ名片; 4.1 博客或资源的完整代码提供 4.2 期刊或参考文献复现 4.3 Matlab程序定制 4.4 科研合作 功率谱估计: 故障诊断分析: 雷达通信:雷达LFM、MIMO、成像、定位、干扰、检测、信号分析、脉冲压缩 滤波估计:SOC估计 目标定位:WSN定位、滤波跟踪、目标定位 生物电信号:肌电信号EMG、脑电信号EEG、心电信号ECG 通信系统:DOA估计、编码译码、变分模态分解、管道泄漏、滤波器、数字信号处理+传输+分析+去噪(CEEMDAN)、数字信号调制、误码率、信号估计、DTMF、信号检测识别融合、LEACH协议、信号检测、水声通信 1. EMD(经验模态分解,Empirical Mode Decomposition) 2. TVF-EMD(时变滤波的经验模态分解,Time-Varying Filtered Empirical Mode Decomposition) 3. EEMD(集成经验模态分解,Ensemble Empirical Mode Decomposition) 4. VMD(变分模态分解,Variational Mode Decomposition) 5. CEEMDAN(完全自适应噪声集合经验模态分解,Complementary Ensemble Empirical Mode Decomposition with Adaptive Noise) 6. LMD(局部均值分解,Local Mean Decomposition) 7. RLMD(鲁棒局部均值分解, Robust Local Mean Decomposition) 8. ITD(固有时间尺度分解,Intrinsic Time Decomposition) 9. SVMD(逐次变分模态分解,Sequential Variational Mode Decomposition) 10. ICEEMDAN(改进的完全自适应噪声集合经验模态分解,Improved Complementary Ensemble Empirical Mode Decomposition with Adaptive Noise) 11. FMD(特征模式分解,Feature Mode Decomposition) 12. REMD(鲁棒经验模态分解,Robust Empirical Mode Decomposition) 13. SGMD(辛几何模态分解,Spectral-Grouping-based Mode Decomposition) 14. RLMD(鲁棒局部均值分解,Robust Intrinsic Time Decomposition) 15. ESMD(极点对称模态分解, extreme-point symmetric mode decomposition) 16. CEEMD(互补集合经验模态分解,Complementary Ensemble Empirical Mode Decomposition) 17. SSA(奇异谱分析,Singular Spectrum Analysis) 18. SWD(群分解,Swarm Decomposition) 19. RPSEMD(再生相移正弦辅助经验模态分解,Regenerated Phase-shifted Sinusoids assisted Empirical Mode Decomposition) 20. EWT(经验小波变换,Empirical Wavelet Transform) 21. DWT(离散小波变换,Discraete wavelet transform) 22. TDD(时域分解,Time Domain Decomposition) 23. MODWT(最大重叠离散小波变换,Maximal Overlap Discrete Wavelet Transform) 24. MEMD(多元经验模态分解,Multivariate Empirical Mode Decomposition) 25. MVMD(多元变分模态分解,Multivariate Variational Mode Decomposition)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值