【讨论】Graphics 画线纹理的一种简单实现!(新增绳子纹理效果)

【本文参与征文活动】

cc.Graphics 画线也能加纹理了?文末附送完整代码。

初探精灵中的网格渲染模式 ! 中简单分析了 Sprite 组件的渲染模式 Mesh

这次,我们应用 Sprite 渲染模式 Meshcc.Graphics ,实现画线纹理的操作。

先看看效果如何。

先在场景创建一个 cc.Graphics 节点。并添加一个子节点 cc.Sprite ,渲染模式改为 Mesh

因为 Mesh 中计算的坐标是从左上角,而 Graphics 画图是从中心开始画的。

所以 cc.Sprite 节点, Scale 调整为 (1,-1)Anchor 调整为 (0,1)

为了使纹理超出边界后可以重复填充,这个纹理大小得是 2n 次方,并设为 Repeat

画纹理肯定需要坐标位置信息。

来一起看看,Graphicswebgl 实现。

Graphics 中有一个 _impl 变量。

这个 _impl 里有一个 _paths 变量,记录了所有画线路径,和对应的画线的点。

lineTomoveTo 都会往 _paths 塞入画线的点数据。

对于 circlearc 以及 rect 等接口,最终还是调用 lineTomoveTo

所以有了这个 _paths 我们画纹理的时候,可以先把点遍历出来。

for (let index = 0; index < _impl._paths.length; index++) {
    const path = _impl._paths[index];
    const pathPoints = path.points;
    if (pathPoints.length < 2) continue;
    for (let index2 = 1; index2 < pathPoints.length; index2++) {
        // 当前点
        const p = cc.v2(pathPoints[index2].x, pathPoints[index2].y);
        // 上一个点
        const p_pre = cc.v2(pathPoints[index2 - 1].x, pathPoints[index2 - 1].y);
    }
}

如何画纹理呢?

先考虑相邻的两个点,再根据线宽 w 画一个长方形。长方形有四个点,我们要求出这四个点的坐标。

先算出这两个点的方向。

const dir = p.sub(p_pre); //方向

接着求出一个垂直方向的向量(根据向量内积为0求出),长度为线宽一半。

const cross_dir = (dir.y == 0 ? cc.v2(0, 1) : cc.v2(1, -dir.x / dir.y).normalize()).mulSelf(w / 2); //垂直方向

根据两个点和垂直方向可以求出这个长方形的四个顶点。

const p_r_t = p.add(cross_dir); //右上
const p_r_b = p.sub(cross_dir); // 右下
const p_l_t = p_pre.add(cross_dir); // 左上
const p_l_b = p_pre.sub(cross_dir); // 左下

最后根据四个点填充 sprite.spriteFrame 中的数据 vertices ,如果不理解的话,可以参考初探精灵中的网格渲染模式 !

对于 uv 纹理坐标,这边就直接使用顶点坐标的缩放一个系数。参考代码如下。

const uv_mul = 50;
const i_offset = vertices.x.length;
vertices.x.push(p_r_t.x, p_r_b.x, p_l_t.x, p_l_b.x);
vertices.y.push(p_r_t.y, p_r_b.y, p_l_t.y, p_l_b.y);
vertices.nu.push(p_r_t.x / uv_mul, p_r_b.x / uv_mul, p_l_t.x / uv_mul, p_l_b.x / uv_mul);
vertices.nv.push(p_r_t.y / uv_mul, p_r_b.y / uv_mul, p_l_t.y / uv_mul, p_l_b.y / uv_mul);

vertices.triangles.push(i_offset + 0);
vertices.triangles.push(i_offset + 1);
vertices.triangles.push(i_offset + 2);
vertices.triangles.push(i_offset + 1);
vertices.triangles.push(i_offset + 2);
vertices.triangles.push(i_offset + 3);

这么画长方形存在一个问题,对于画圆弧,如果分隔太大,或者线宽比较大,会出现分割的问题。

怎么解决这个问题呢?

一是可以参考源码,把连接处补上。

另一种方式是直接用 GraphicsAssembler 中的 buffers 数据,重新组织一下。

当然,这些等我研究出来再分享给大家(日常挖坑)!

以上为白玉无冰使用 Cocos Creator v2.3.3 关于 "画线纹理的一种简单实现!"的技术分享。如果对你有点帮助,欢迎分享给身边的朋友。



原文链接

完整代码
https://github.com/baiyuwubing/cocos-creator-examples

原创文章导航点我(持续更新)



画线纹理之连接优化

对转角处加一层处理,就可以更加平滑了。。。。

先看看效果。

画线纹理的一种简单实现! 中介绍了可以使用 Sprite 渲染模式 Meshcc.Graphics ,实现画线纹理。

不过在连接处存在缝隙。

那么怎么处理这个缝隙呢?

只需要在连接点画一个圆,这样缝隙就能去掉了。

那么怎么画圆呢?可以把圆看成是正多边形,根据半径和圆心的关系,可以确认位置坐标。可参考 shader 动画之 loading 特效!这篇文章。

半径刚好就是画线宽度的一半,某个圆上的坐标转成代码如下。

// 角度
const r = 2 * Math.PI * index3 / count;
// 先算方向向量,在加上圆心坐标就是,圆上的点。
const pos_circle = cc.v2(w / 2 * Math.cos(r), w / 2 * Math.sin(r)).addSelf(p);

怎么确定顶点索引呢?

其实可以按照圆心走,画一个个三角形就行啦。

当然这是其中一种索引方式,转成代码如下。

//画圆
const count = 12;
i_offset = vertices.x.length;
vertices.x.push(p.x);
vertices.y.push(p.y);
vertices.nu.push(p.x / uv_mul);
vertices.nv.push(p.y / uv_mul);
for (let index3 = 0; index3 < count; index3++) {
    const r = 2 * Math.PI * index3 / count;
    const pos_circle = cc.v2(w / 2 * Math.cos(r), w / 2 * Math.sin(r)).addSelf(p);
    vertices.x.push(pos_circle.x);
    vertices.y.push(pos_circle.y);
    vertices.nu.push(pos_circle.x / uv_mul);
    vertices.nv.push(pos_circle.y / uv_mul);
    if (index3 === 0) {
        // 0 - count -1
        vertices.triangles.push(i_offset, i_offset + 1 + index3, i_offset + count);
    } else {
        // 0 - index3 - (index3-1)
        vertices.triangles.push(i_offset, i_offset + 1 + index3, i_offset + index3);
    }
}

以上只是实现简单画线纹理的效果,如果要实现绳子这种效果,那就需要重新计算纹理坐标,和位置/方向/长度等有关系。

这个暂时还没想好,留给大家讨论吧哈哈~

以上为白玉无冰使用 Cocos Creator v2.3.3 关于 "画线纹理之连接优化!" 的技术分享。如果对你有点帮助,欢迎分享给身边的朋友。


画线纹理之绳子

为绳子任意方向的拖动添加纹理~

效果预览

前置教程

这次的纹理是使用 Sprite 组件的渲染模式 Mesh ,前文 初探精灵中的网格渲染模式 ! 介绍了这个用法。

绘制的数据要用到 _poins 来画长方形,前文 画线纹理之简单实现 中有介绍。

在连接处画个圆达到平衡效果,前文 画线纹理之连接优化 中有讲到处理方法。

回顾一下这三篇文章有助于本文的理解哦~

实现原理

前几篇已经实现了画线纹理,这次主要的目标是计算正确的 uv 坐标。

因为这个线有方向,有长度,都会影响纹理坐标的计算。

这里想到的一个思路是,把所有的线段拉成一条直线,并放到一个方向

为了使这个纹理能从尾部带动头部的效果,拉直后,最后一个点作为纹理的起始点。

所以遍历这个点的时候,要从尾部开始,并记录一下每节的长度。

纹理坐标 v 的两个点是 01 。 纹理坐标 u (水平方向) 根据绳子的长度去推算。

// 从最后一点开始计算
for (let index2 = pathPoints.length - 1; index2 > 0; index2--) {
	// 省略部分代码
	vertices.x.push(p_r_t.x, p_r_b.x, p_l_t.x, p_l_b.x);
	vertices.y.push(p_r_t.y, p_r_b.y, p_l_t.y, p_l_b.y);
    // 计算uv
	vertices.nu.push(offsetX.x * uv_mul, offsetX.x * uv_mul, (offsetX.x + dirLen) * uv_mul, (offsetX.x + dirLen) * uv_mul);
	vertices.nv.push(1, 0, 1, 0);
	// 省略部分代码
	offsetX.addSelf(cc.v2(dirLen, 0)); // 记录已经画的长度长度
}

这么倒着遍历会出现一个问题,就是尾巴的纹理会被头覆盖。

所以计算长方形的顶点索引后,要整体反转,让他从头开始画。主要代码如下。

let trianglesCache: number[][] = [];
for (let index2 = pathPoints.length - 1; index2 > 0; index2--) {
    // 省略部分代码
    triangles.push(i_offset + 0);
    triangles.push(i_offset + 1);
    triangles.push(i_offset + 2);
    triangles.push(i_offset + 1);
    triangles.push(i_offset + 2);
    triangles.push(i_offset + 3);
    trianglesCache.push(triangles);
}	
trianglesCache.reverse(); // 顶点索引反转
trianglesCache.forEach(v => {
	// 真正的顶点索引顺序
    vertices.triangles.push(...v)
})

反转后,绳子的纹理就正确了。

对于连接处画圆(实际是多边形),需要注意每个点都要旋转,这样才能让圆的纹理方向正确。

参考代码如下。

//画圆
const dir_angle = dir.signAngle(cc.v2(-1, 0));//与x轴的负方向的夹角
const count = 12;
i_offset = vertices.x.length;
// 这里是圆心
vertices.x.push(p.x);
vertices.y.push(p.y);
vertices.nu.push(offsetX.x * uv_mul);
vertices.nv.push(0.5);
for (let index3 = 0; index3 < count; index3++) {
    const r = 2 * Math.PI * index3 / count;
    // 圆心到各个边的向量
    const pos_circle = cc.v2(w / 2 * Math.cos(r), w / 2 * Math.sin(r));
    vertices.x.push(pos_circle.add(p).x);
    vertices.y.push(pos_circle.add(p).y);
    // 对于圆的uv需要旋转
    vertices.nu.push((pos_circle.rotate(dir_angle).x + offsetX.x) * uv_mul);
    vertices.nv.push(pos_circle.rotate(dir_angle).y / w + 0.5);
    if (index3 === 0) {
        triangles.push(i_offset, i_offset + 1 + index3, i_offset + count);
    } else {
        triangles.push(i_offset, i_offset + 1 + index3, i_offset + index3);
    }
}

最后,给大家画个星吧~

小结

把弯的掰直!

这个绳子纹理的整个思路就是把所有弯的线,都转化成直的后,再计算纹理坐标。

以上为白玉无冰使用 Cocos Creator v2.3.3 关于 "画线纹理之绳子!" 的技术分享。如果对你有点帮助,欢迎分享给身边的朋友。


白玉无冰的微信公众号~




渐变色效果



画线纹理1
画线纹理2
画线纹理3
完整代码(详细见readme)
原创文章导航

44赞

太深的技术没人看

mark…

把相邻的两个矩形的两个顶点合并成一个点就可以消除缝隙了。

:laughing: 强!:grin:学到了

支持一下 等待后续

:smile: 最终决定在交接处画一个圆

详见:

https://mp.weixin.qq.com/s/xniwz-a_FI0snWqqPd2NOg

沙雕小游戏也上线了,欢迎愉快的玩耍~

新增绳子纹理效果~
https://mp.weixin.qq.com/s/QvJ2DHFhUxO3doNviCqBIg

原创文章导航

3赞

马克~~~

mark 我是卤煮的坚实读者

mark1111111

mark~~~~

mark!!

mark~~~~

感谢大佬分享

mark mark

有大佬用这个画过两条线么?我怎么画完一条之后再画另一条然后回过头重新改之前画的那条结果画线的位置不一样了,效果如下图

尝试了下画线的时候重新设置this.sprite.spriteFrame[‘vertices’] = this._vertices;就不会这样,但是如果在同一帧两条线都同时绘制就还是这样的。

NewProject.zip (837.8 KB)做了个简单的demo,求大佬们帮忙看看,救救可怜的孩子吧

1赞

做了下实验,画几根绳子就用几张图就不会有问题了,先这样用着吧,还好图片不大

已经解决了,画多根线的时候不要把图片拖到预制体里面,要新建一个spriteFrame赋值给精灵

1赞