谁不想拥有自己的登山赛车呢?可编辑曲线纹理组件(CurveTexture)实现原理小分享

CurveTexture2D 可编辑曲线纹理组件 实现原理小分享

1. 插件简介

CurveTexture2D 组件插件的开发来源于经典的 2D 横版赛车游戏《登山赛车》(Hill Climb Racing),以及独立游戏《Old Man’s Journey》。在这类游戏中,地形通常由平滑的曲线构成,玩家驾驶车辆或操控角色在各种起伏的地形上探索。正好 CocosCreater 发布了体验大幅提升的 3.8.6 版本,那就实现一个吧。

登山赛车

回忆之旅/Old man’s journey

提前体验功能: Cocos Creator | flashfin_CurveTexture2D

B 站简单使用视频: https://www.bilibili.com/video/BV1Xb5KzZEQD

插件商店地址(3.x 版本): Cocos Store

插件商店地址(2.4.x 版本):Cocos Store

2. 主要功能与应用场景

主要功能

  • 曲线地形编辑:通过可视化控制点,快速绘制和调整地形轮廓。

  • 自动曲线细分:采用 Catmull-Rom 样条算法,实现地形曲线的平滑插值。

  • Mesh 自动生成:根据曲线自动生成可用于渲染的 Mesh 数据,支持不同渲染模式(竖直/切线)。

  • UV 映射与纹理支持:支持自定义地形纹理,自动计算 UV 坐标,实现地形贴图平铺或拉伸。

  • 物理碰撞体同步:可一键生成与地形形状一致的 2D PolygonCollider2D,方便物理交互。

  • 编辑器可视化扩展:支持在 CocosCreator 编辑器中实时预览和调整地形,提升开发效率。

  • 多节点同步:支持地形数据在多个节点间同步,便于复杂场景的地形复用。

可以使用在

  • 2D 横版赛车、摩托、滑雪等需要曲线地形的游戏。

  • 2D 跑酷、冒险、动作等需要自定义地形的项目。

  • 需要动态生成或编辑地形的关卡编辑器工具。

  • 其他任何需要高自由度 2D 曲线地形的 CocosCreator 项目。

3. 组件属性详解

CurveTexture2D 组件暴露了丰富的属性,方便使用者灵活配置数据。以下为主要属性说明:

  • 同步模式 (syncOnOff, syncTarget) 是否自动实时同步另一个节点的细分数据,实现多节点地形复用。

  • 渲染属性

    • 渲染模式 (renderMode) 支持竖直模式(VERTICAL_MODE)和切线模式(TANGENT_MODE),影响地形厚度和贴图方式。

    • 渲染纹理 (spFrame) 地形使用的主纹理,支持自定义贴图。

    • 渲染厚度 (thickness) 地形的厚度,0 表示自动使用纹理高度。仅竖直模式下可调。

    • 渲染偏移 (offset) 地形整体的渲染偏移量。

    • 纹理上下反转 (updownFix) 贴图渲染方向是否上下反转。

    • 透明度阈值 (alphaThreshold) 处理带透明通道纹理时的透明度阈值。

    • 细分点数量 (smoothness) 控制每两控制点之间的曲线细分数量,数值越大曲线越平滑。

  • 控制点属性

    • 控制点 (controlPoints) 编辑地形轮廓的关键点,可在编辑器中可视化拖动。

    • 控制点显示/隐藏 (gizmoVisibleOnOff) 是否在编辑器中显示控制点辅助图形。

    • 控制点纹理 (gizmoTag) 控制点的可视化 SpriteFrame 资源。

    • 控制点颜色 (gizmoColor) 控制点 Gizmo 的颜色。

    • 运行时编辑 (editWhenRun) 是否允许在运行时编辑控制点。

    • 是否在 x 轴方向排序(sortInXDirection) 是否在 x 方向上自动排序。

  • 物理属性相关

    • 同步生成物理属性 (physcisOnOff) 是否自动生成与地形一致的 PolygonCollider2D。

    • 物理碰撞器厚度 (physicsThickness) 物理碰撞体的厚度,0 表示自动使用纹理高度。

    • 物理碰撞器偏移 (offsetCollider) 物理碰撞体的偏移量。

通过合理配置上述属性,可以满足绝大多数 2D 曲线地形的制作需求。

4. 用到的引擎自带的组件

  • MeshRenderer:负责将 Mesh(网格)渲染到场景中。通过设置 Mesh 数据和材质,可以实现自定义的地形外观。(官方介绍)

  • Mesh:包含顶点、UV、法线、索引等信息的数据结构,是地形渲染的基础。

  • PolygonCollider2D:用于同步生成与地形形状一致的碰撞体,实现物理交互。

    使出了我仅有的绘画功底,简单描述了这个组件(我)做了什么(绿色部分):

5. 控制点编辑与曲线平滑细分

控制点编辑

  • 在编辑器中,每个控制点以可视化 Gizmo 节点的形式展示,支持拖拽、增删和排序。

  • 支持 X 轴方向自动排序,避免控制点交叉导致地形异常。

  • 控制点的颜色、纹理可自定义,提升编辑体验。

曲线平滑细分功能

  • 插件采用 Catmull-Rom 样条曲线对控制点进行插值,生成平滑的地形轮廓。

  • 每两控制点之间可自定义细分点数量(smoothness),数值越大曲线越平滑。

  • 对于仅有两个控制点的情况,采用线性插值,保证地形连续性。

我的绘画还是有点功底的

核心代码片段:控制点细分与插值


private _generateSegmentVertex(seg_count: number = 8) {

  this._vertexesSegment.length = 0;

  const points = this._controlPoints;

  const pointCount = points.length;

  if (pointCount < 2) {

    if (pointCount === 1) {

      this._vertexesSegment.push(points[0].clone());

    }

    return;

  }

  if (pointCount === 2) {

    const p0 = points[0];

    const p1 = points[1];

    const dist = p0.clone().subtract(p1).length();

    let seg = seg_count;

    while (seg > 1 && dist / seg < this._minSegmentLength) {

      seg = Math.floor(seg / 2);

    }

    this._vertexesSegment.push(p0.clone());

    const step = 1 / seg;

    for (let i = 1; i < seg; i++) {

      const t = i * step;

      this._vertexesSegment.push(p0.clone().lerp(p1, t));

    }

    this._vertexesSegment.push(p1.clone());

    return;

  }

  // 使用Catmull-Rom样条平滑插值

  const paddedPoints: Vec3[] = [];

  paddedPoints.push(points[0].clone().add(points[0].clone().subtract(points[1])));

  paddedPoints.push(...points);

  paddedPoints.push(points[pointCount - 1].clone().add(points[pointCount - 1].clone().subtract(points[pointCount - 2])));

  for (let i = 0; i < pointCount - 1; i++) {

    const p0 = paddedPoints[i];

    const p1 = paddedPoints[i + 1];

    const p2 = paddedPoints[i + 2];

    const p3 = paddedPoints[i + 3];

    if (i === 0) {

      this._vertexesSegment.push(p1.clone());

    }

    const dist = p1.clone().subtract(p2).length();

    let seg = seg_count;

    while (seg > 1 && dist / seg < this._minSegmentLength) {

      seg = Math.floor(seg / 2);

    }

    const step = 1 / seg;

    for (let j = 1; j <= seg; j++) {

      const t = j * step;

      const interpolatedPoint = this._catmullRom(p0, p1, p2, p3, t);

      this._vertexesSegment.push(interpolatedPoint);

    }

  }

}

细分点去重与异常处理

  • 插值后自动去除重复或异常点,保证地形数据稳定。

  • 过滤掉非有限数值和零点,避免渲染和物理异常。

核心代码片段:去重与过滤


for (let i = this._vertexesSegment.length - 1; i > 0; i--) {

  if (isCloseToVec3(this._vertexesSegment[i], this._vertexesSegment[i - 1], 2)) {

    this._vertexesSegment.splice(i, 1);

  }

}

this._vertexesSegment = this._vertexesSegment.filter((v) => isFinite(v.x) && isFinite(v.y) && (v.x !== 0 || v.y !== 0));

6. Mesh 生成与 UV 映射

Mesh 顶点与三角形索引生成

  • 根据细分后的曲线点,结合渲染模式(竖直/切线),自动生成 Mesh 顶点。

  • 竖直模式下,每个曲线点向下生成厚度顶点,切线模式下则沿法线方向生成厚度顶点。

  • 顶点按三角形顺序组装,形成完整的地形 Mesh。

代码片段:Mesh 顶点生成(竖直模式)


if (this._renderMode == TerrainRenderMode.VERTICAL_MODE) {

  for (let index = 0; index < temp.length - 1; index++) {

    const item1 = temp[index];

    const item2 = temp[index + 1];

    const item1_bottom = item1.clone().add(v3(0, -this._curve_height_result, 0));

    const item2_bottom = item2.clone().add(v3(0, -this._curve_height_result, 0));

    this._vertexes.push(item1);

    this._vertexes.push(item1_bottom);

    this._vertexes.push(item2);

    this._vertexes.push(item1_bottom);

    this._vertexes.push(item2_bottom);

    this._vertexes.push(item2);

  }

}

代码片段:Mesh 顶点生成(切线模式)


if (this._renderMode == TerrainRenderMode.TANGENT_MODE) {

  for (let index = 0; index < temp.length; index++) {

    const t_current = temp[index];

    if (index == temp.length - 1) {

      this._vertexes.push(t_current);

      break;

    }

    const t_next = temp[index + 1];

    const len = t_current.clone().subtract(t_next).length();

    const sinValue = (t_next.y - t_current.y) / len;

    const cosValue = (t_next.x - t_current.x) / len;

    const CX = t_current.x + this._curve_height * sinValue;

    const CY = t_current.y - this._curve_height * cosValue;

    const first_vertical_pos = v2(CX, CY);

    const second_vertical_pos = v2(CX, CY).add(t_next.clone().subtract(t_current).toVec2());

    if (index === 0) {

      this._vertexes.push(t_current);

      this._vertexes.push(first_vertical_pos.toVec3());

      this._vertexes.push(t_next);

      this._vertexes.push(second_vertical_pos.toVec3());

    } else {

      // 内凹/外凸等连接点处理略,详见组件详细代码

      // 这里只展示主要的 Mesh 顶点生成逻辑

      this._vertexes.push(t_next);

      this._vertexes.push(second_vertical_pos.toVec3());

    }

  }

}

核心代码片段:三角形顶点索引生成


const indices: number[] = [];

const vertexCount = this._vertexes.length;

for (let i = 0; i < vertexCount - 2; i++) {

  if (this.node.scale.x * this.node.scale.y > 0) {

    indices.push(i);

    indices.push(i % 2 == 0 ? i + 1 : i + 2);

    indices.push(i % 2 == 0 ? i + 2 : i + 1);

  } else {

    indices.push(i);

    indices.push(i % 2 == 0 ? i + 2 : i + 1);

    indices.push(i % 2 == 0 ? i + 1 : i + 2);

  }

}

UV 坐标计算

  • 插件自动为每个顶点计算 UV 坐标,实现地形纹理的平铺或拉伸。

  • 支持上下反转、不同渲染模式下的 UV 计算方式。

为了更直观看到这两种模式的 uv 区别,我制作了一张 uv 测试纹理效果图:

代码片段:UV 计算(竖直模式)


for (let i = 0; i < this._vertexes.length; i++) {

  const d = this._vertexes[i];

  const u = d.x / this._curve_width;

  let v = 1.0 - d.y / this._curve_height;

  if (this._updownFix) {

    v = d.y / this._curve_height;

  }

  uvs.push(u, v);

}

核心代码片段:UV 计算(切线模式)


let index = 0;

let last_pos_up: Vec3 | null = null;

let length_all_up = 0;

let current_top_u = 0;

for (const pt of this._vertexes) {

  let u = 0;

  let v = 0;

  if (index % 2 == 0) {

    v = this._updownFix ? 1 : 0;

    if (last_pos_up != null) {

      length_all_up += pt.clone().subtract(last_pos_up).length();

      u = length_all_up / this._curve_width;

    } else {

      u = 0;

    }

    current_top_u = u;

    last_pos_up = pt;

  } else {

    v = this._updownFix ? 0 : 1;

    u = current_top_u;

  }

  uvs.push(u, v);

  index += 1;

}

数据都准备好了,就交给 Mesh 去组装吧

  • 通过 utils.MeshUtils.createMesh 创建 Mesh,并赋值给 MeshRenderer 组件,实现地形渲染。

核心代码片段:Mesh 生成与赋值


let mesh = utils.MeshUtils.createMesh({

  positions: positions,

  uvs: uvs,

  indices: indices,

  normals: normals,

});

if (this.renderer!.mesh) {

  this.renderer!.mesh.destroy();

}

this.renderer!.mesh = mesh;

设置材质属性


  private _applySpriteFrame() {

    if (this._spFrame && this.renderer) {

      this._curve_height_result = this._thickness == 0 ? this._curve_height : this._thickness;

      this._curve_collider_thickness = this._physicsThickness == 0 ? this._curve_height : this._physicsThickness;

      let texture = this._spFrame.texture;

      texture.setWrapMode(Texture2D.WrapMode.REPEAT, Texture2D.WrapMode.REPEAT);

      let mat = this.renderer.getMaterialInstance(0) as Material;

      if (!mat) {

        mat = new Material();

        mat.initialize({

          effectAsset: EffectAsset.get('builtin-unlit'),

          defines: {

            USE_TEXTURE: true,

            USE_ALPHA_TEST: true,

          },

        });

        this.renderer.setMaterialInstance(mat, 0);

      }

      mat.setProperty('mainTexture', texture, 0);

      mat.setProperty('alphaThreshold', this._alphaThreshold, 0);

     }

    }

至此,渲染结束了。

7. 物理碰撞体同步生成

主要流程

  1. 根据当前地形的细分点,自动生成碰撞体的顶点轮廓。

  2. 竖直模式下,碰撞体为上下边界闭合多边形;切线模式下,碰撞体为法线方向扩展后的闭合多边形。

  3. 支持物理碰撞体厚度、偏移等参数调整,适配不同物理需求。

  4. 自动挂载 PolygonCollider2D 组件到指定子节点,并同步更新数据。

效果图

核心代码片段:碰撞体轮廓生成


private _extractPolygonOutline(indata: Vec3[]) {

  if (this._renderMode == TerrainRenderMode.VERTICAL_MODE) {

    const points: Vec2[] = [];

    let d = this._vertexesSegment.map((item) => item.clone().add(this._offsetCollider).toVec2());

    points.push(...d);

    let down = d.map((item) => item.clone().add(v2(0, -this._curve_collider_thickness))).reverse();

    points.push(...down);

    return points;

  }

  // 切线模式下的法线扩展略,大同小异

  // ...具体处理见源码...

}

核心代码片段:同步生成 PolygonCollider2D 组件


private generatePhysicData() {

    ...

  this._physicsCollider.points = polygonPoints;

  this._physicsCollider.apply();

    ...

}

8. 编辑器扩展与可视化

编辑器交互方式

  • 拖拽控制点即可实时修改地形轮廓,地形 Mesh 与碰撞体自动同步更新。

  • 支持通过 Inspector 面板调整属性(如厚度、细分数、偏移、物理参数等),实时反映到场景中。

  • 控制点可通过 Inspector 面板直接编辑坐标,也可在场景中拖动。

  • 支持运行时编辑模式(editWhenRun),便于调试和动态关卡编辑。

  • 支持多节点同步(syncOnOff/syncTarget),便于复杂场景的地形复用与批量编辑。

可继续优化增加的功能

  • 可设定多种不同类型的控制点规则,在编辑器中一键应用,减少重复工作。

  • 对于无限长度地图的需求,肯定不可能在编辑器中制作一条无限长度的地形,需要根据噪声数据程序化生成。

一个被忽略的属性

如果去掉这个默认勾选,就可以任意拖动控制点

9. 使用可能会遇到的问题

  • Q: 地形渲染后出现锯齿或断裂?

    A: 检查控制点是否过于稀疏或重叠,适当增加细分点数量(smoothness),并确保控制点按 X 轴递增排列。

  • Q: Mesh 没有正常显示或贴图错乱?

    A: 检查纹理资源是否为 2 的幂次方,且已正确设置为 Repeat 模式。确认 UV 计算逻辑与渲染模式一致。

  • Q: 物理碰撞体与地形不重合?

    A: 检查物理厚度、偏移参数设置,确保物理同步开关已开启。必要时手动调整物理参数。

  • Q: 控制点 Gizmo 不显示或无法拖拽?

    A: 检查 gizmoVisibleOnOff 是否开启,确保已指定 gizmoTag 资源,且未被其他 UI 组件遮挡。

  • Q: 跟随模式下地形不同步?

    A: 检查 syncOnOff 和 syncTarget 设置,避免循环依赖(不可能)或目标节点未挂载 CurveTexture 组件。

优化建议

  • 控制点数量不宜过多,建议合理分布,避免性能浪费。

  • 纹理资源建议使用无缝平铺贴图,提升地形视觉效果。

  • 物理碰撞体同步时可适当降低细分点数量,减少物理计算压力。

  • 可自定义编辑器工具,批量操作控制点、自动平滑等,提升编辑效率。

  • 如需大地图拼接,建议分段管理地形节点,避免单节点数据过大。

如遇问题可参考组件 CurveTexture.ts 源码或联系作者获取支持。
邮箱:flashfin@foxmail.com
微信:soida3

19赞

牛逼 牛逼

very niu b,这可太有生活了

image

谁说不是呢

实在是太牛了

牛~~~~

大佬牛逼!

markk

做的真棒,之前我也自己实现了一个版本,思路和大佬的如出一辙,不过我看你这个在处理锐角的情况会有bug,当初我也是遇到了这个问题,还有就是我看uv的分布不均匀,出现了拉伸的情况。不过做的已经非常牛逼了。

太强了!!

真强啊!!

牛逼!!!!!

准备更新一个新版本:

  1. 加入首尾固定,中间重复的渲染效果,类似九宫图的规则。
  2. 添加一个赛车场景,包括赛车。

我仿佛看到了商机,把Unity插件往Creator移植简直是降维打击。不过用户群体太小,不知能不能卖得动?

为爱发电呢. :smile:

别人都全职做Cocosstore了,做得好比上班强

这价格回本都难说,cc插件是不是也像unity,插件副本随便用?做防盗措施了吗

快去买一个看看就知道. :slight_smile:

你做得完成度非常高了,非常赞,不过我已经6年没用过cocos了。十年前用cocos2d-x的做过2d terrain Cocos2d-x利用Unity轻松快速设计复杂2D地形Cocos2d-x利用Unity轻松快速设计复杂2D地形_cocos2d 生成2d动态地图-CSDN博客
最近看论坛大家都热衷做插件,想了解调研一下,取取经

原来是大佬. 看了你以前的工具,也很棒!!