【Godot4.2】Godot中的贝塞尔曲线

news2024/12/27 14:02:00

概述

通过指定平面上的多个点,然后顺次连接,我们可以得到折线段,如果闭合图形,就可以获得多边形。通过向量旋转我们可以获得圆等特殊图形。

但是对于任意曲线,我们无法使用简单的方式来获取其顶点,好在计算机大神们已经发明了贝塞尔曲线这样的算法。

本篇就介绍如何在Godot中绘制贝塞尔曲线,并通过设定控制点来精确控制曲线的走向。

(原文写于2024年4月,内容持续改进和扩充中)

基础原理

在实际上手绘制之前,让我们先来理解一下贝塞尔曲线的求点原理与本质——向量插值。

二次贝塞尔曲线

1161912546.jpg
在平面上有三个点ABC

  • AB相连,形成一个向量 A B ⃗ \vec{AB} AB ,BC相连,形成另一个向量 B C ⃗ \vec{BC} BC
  • A B ⃗ \vec{AB} AB B C ⃗ \vec{BC} BC 同步进行0.01.0插值,设插值变量为t
  • 在插值的每一时刻,会从 A B ⃗ \vec{AB} AB B C ⃗ \vec{BC} BC 上各获得一个点DE
    1161914558.jpg
  • 连接DE,对 D E ⃗ \vec{DE} DE 进行0.01.0插值,而且插值与此时的t一致。则获得一个点F
  • 也就是说,同步对 A B ⃗ \vec{AB} AB B C ⃗ \vec{BC} BC D E ⃗ \vec{DE} DE 进行0.01.0插值。

1161916361.jpg
D E ⃗ \vec{DE} DE 插值获取的所有点F连起来就是一条由A到B的贝塞尔曲线。整个插值过程也就是官方文档中的这张动图:

2121793576.gif

所以二次贝塞尔曲线是同时进行三个向量插值获得的点的集合。

三次贝塞尔曲线

平面上四个点A、B、C、D:

  • 分别组成三个向量 A B ⃗ \vec{AB} AB B C ⃗ \vec{BC} BC C D ⃗ \vec{CD} CD
  • 在三个向量上同步插值获得三个点E、F、G
  • EF和FG相连,组成向量 E F ⃗ \vec{EF} EF F G ⃗ \vec{FG} FG
  • E F ⃗ \vec{EF} EF F G ⃗ \vec{FG} FG 上同步插值获得点H和I
  • E F ⃗ \vec{EF} EF 上同步插值获得点J
  • 整个同步插值过程获得的点J的集合,顺序相连,绘制处的就是三次贝塞尔曲线
    在这里插入图片描述

动态过程如下(也就是官方文档的动图):

-1227113256.gif

在Godot中实际绘制贝塞尔曲线

在Godot中实际上并不需要我们编写自己的贝塞尔曲线插值求点函数,Vector2类型的bezier_interpolate()方法可以让我们轻松的获取相应顶点和控制点设置下的贝塞尔曲线点。它的定义如下:

bezier_interpolate(control_1: Vector2, control_2: Vector2, end: Vector2, t: float) -> Vector2
  • control_1control_2分别为控制点1控制点2
  • end可以理解为第二个点
  • t0.01.0的插值,也可以理解为一个百分比或偏移量

bezier_interpolate()的用法就是:

p1.bezier_interpolate(c1,c2,p2,t)

其中:

  • p1是贝塞尔曲线起点,p2是贝塞尔曲线终点
  • c1,c2分别为控制点1控制点2
  • t是百分比

所以我们想要求一段贝塞尔曲线,就需要指定4个点,其中2个是起止点,另外2个是控制点。并使用一个for循环来进行插值,求取整个过程中的点。
最后再使用Godot内置的绘图函数draw_polyline()来绘制。

我们看一个实例:

extends Node2D

var p1 = Vector2(100,100)   # 起点
var p2 = Vector2(200,200)   # 终点
var ctl_1 = Vector2(100,0)  # 控制点1
var ctl_2 = Vector2(100,0)  # 控制点2

var points:PackedVector2Array = []   # 曲线点集合
var steps = 100;                     # 点的数目,越多曲线越平滑

var curve_color:= Color.WHITE       # 曲线绘制颜色
var ctl_color:= Color.AQUAMARINE    # 控制点和连线绘制颜色

func _ready() -> void:
	# 求曲线点集
	for i in range(steps+1):
		var p = p1.bezier_interpolate(p1+ctl_1,p2-ctl_2,p2,i/float(steps))
		points.append(p)


func _draw() -> void:
	# 绘制控制点
	draw_arc(p1+ctl_1,2,0,TAU,10,ctl_color,1)
	draw_arc(p2-ctl_2,2,0,TAU,10,ctl_color,1)
	# 绘制曲线端点与控制点的连线
	draw_line(p1,p1+ctl_1-Vector2(1,0),ctl_color,1)
	draw_line(p2,p2-ctl_1+Vector2(1,0),ctl_color,1)
	# 绘制贝塞尔曲线
	draw_polyline(points,curve_color,1)

上面的代码中:

  • 我们首先声明变量保存起点、终点和两个控制点的坐标
  • 然后申明变量points用于存储插值获取的贝塞尔曲线上的点
  • steps变量用于存储总共插值的步数,也就是获得的曲线上点的个数,步数越多,求得的点越多,最终绘制的曲线越平滑
  • 申明两个变量来分别存储曲线和控制点的颜色。
  • _ready()中我们执行一个for循环来插值steps次,来获取指定的起点、终点、控制点下的贝塞尔曲线上的点,并存储到变量points
  • _draw()在场景运行时会被自动调用,用来实际的绘制出曲线和控制点

最终绘制结果如下:

在这里插入图片描述

导数

Vector2类型提供了一个名叫bezier_derivative()的方法,用来求贝塞尔曲线上t处的“导数”。

经过实际测试,这个所谓的“导数”是一个点,连接贝塞尔曲线上t处的点与该点,刚好是一个切线段

我们以下面的代码进行测试:

extends Node2D

var p1 = Vector2(100,100)   # 起点
var p2 = Vector2(200,200)   # 终点
var ctl_1 = Vector2(50,0)  # 控制点1
var ctl_2 = Vector2(50,0)  # 控制点2

var points:PackedVector2Array = []   # 曲线点集合
var ds:PackedVector2Array = []       # 曲线点导数集合
var steps = 100;                     # 点的数目,越多曲线越平滑

var curve_color:= Color.WHITE       # 曲线绘制颜色
var ctl_color:= Color.AQUAMARINE    # 控制点和连线绘制颜色

func _ready() -> void:
	# 求曲线点集
	for i in range(steps+1):
		var p = p1.bezier_interpolate(p1+ctl_1,p2-ctl_2,p2,i/float(steps))
		points.append(p)
		var d = p1.bezier_derivative(p1+ctl_1,p2-ctl_2,p2,i/float(steps))
		ds.append(d)


func _draw() -> void:
	draw_polyline(points,curve_color,1)
	var i = 0
	draw_line(points[i],points[i]+ds[i],ctl_color,1)
	print(points[i]," ",points[i]+ds[i])

其中i是指曲线上点的索引,不同的i可以从points[i]中获取代表在i/float(steps)处的点。

以下是一些i值下对应点与“导数”点连线的情况:

在这里插入图片描述

将极坐标点函数运用于贝塞尔控制点

# 极坐标点函数 - 通过角度和长度定义一个点
func pVector2(angle:float = 0.0,length:float =0.0) -> Vector2:
	var dir = Vector2.RIGHT.rotated(deg_to_rad(angle))
	return dir * length

Vector2很难直观的表达方向和距离信息,pVector2则可以,所以在设定贝塞尔控制点时,可以使用极坐标点函数。

extends Node2D

var p1 = Vector2(100,100)   # 起点
var p2 = Vector2(200,200)   # 终点
var ctl_1 = pVector2(0,50)  # 控制点1
var ctl_2 = pVector2(180,50)  # 控制点2

var points:PackedVector2Array = []   # 曲线点集合
var steps = 100;                     # 点的数目,越多曲线越平滑

var curve_color:= Color.WHITE       # 曲线绘制颜色
var ctl_color:= Color.AQUAMARINE    # 控制点和连线绘制颜色

func _ready() -> void:
	# 求曲线点集
	for i in range(steps+1):
		var p = p1.bezier_interpolate(p1+ctl_1,p2+ctl_2,p2,i/float(steps))
		points.append(p)


func _draw() -> void:
	# 绘制控制点
	draw_arc(p1+ctl_1,2,0,TAU,10,ctl_color,1)
	draw_arc(p2+ctl_2,2,0,TAU,10,ctl_color,1)
	# 绘制曲线端点与控制点的连线
	draw_line(p1,p1+ctl_1-Vector2(1,0),ctl_color,1)
	draw_line(p2,p2-ctl_1+Vector2(1,0),ctl_color,1)
	# 绘制贝塞尔曲线
	draw_polyline(points,curve_color,1)

绘制效果如下:

在这里插入图片描述
可以看到,我们可以更直观的设定控制点在起点或终点的哪个方向,以及多长。

贝塞尔曲线函数

我们可以将贝塞尔曲线上点的求取过程封装为一个函数,这样就可以直接调用。

# 求两点之间的贝塞尔曲线
func bezier_curve(p1:Vector2,p2:Vector2,ctl_1:=Vector2(),ctl_2:=Vector2(),points_count:=10) -> PackedVector2Array:
	var points:PackedVector2Array = []
	# 求曲线点集
	for i in range(points_count+1):
		var p = p1.bezier_interpolate(p1+ctl_1,p2+ctl_2,p2,i/float(points_count))
		points.append(p)
	return points

同样我们可以编写一个贝塞尔曲线绘制函数,用来直接在CanvasItem上调用和绘制:

# 绘制贝塞尔曲线
func draw_bezier_curve(canvas:CanvasItem,p1:Vector2,p2:Vector2,ctl_1:=Vector2(),ctl_2:=Vector2(),points_count:=10):
	var points:PackedVector2Array = []   # 曲线点集合
	points.append_array(bezier_curve(p1,p2,ctl_1,ctl_2,points_count))
	# 绘制控制点
	draw_arc(p1+ctl_1,2,0,TAU,10,ctl_color,1)
	draw_arc(p2+ctl_2,2,0,TAU,10,ctl_color,1)
	# 绘制曲线端点与控制点的连线
	draw_line(p1,p1+ctl_1-Vector2(1,0),ctl_color,1)
	draw_line(p2,p2+ctl_2-Vector2(1,0),ctl_color,1)
	# 绘制贝塞尔曲线
	draw_polyline(points,curve_color,1)

测试代码:

extends Node2D

var p1 = Vector2(100,100)   # 点1
var p2 = Vector2(200,200)   # 点2
var p3 = Vector2(400,300)   # 点3
var ctl_1 = pVector2(-90,100)  # 控制点1
var ctl_2 = pVector2(-45,100)  # 控制点2
var ctl_3 = pVector2(135,100)  # 控制点3
var ctl_4 = pVector2(45,100)  # 控制点4


var steps = 100;                     # 点的数目,越多曲线越平滑

var curve_color:= Color.WHITE       # 曲线绘制颜色
var ctl_color:= Color.AQUAMARINE    # 控制点和连线绘制颜色


func _draw() -> void:
	draw_bezier_curve(self,p1,p2,ctl_1,ctl_2,50)
	draw_bezier_curve(self,p2,p3,ctl_3,ctl_4,50)

image.png
可以看到:

  • 通过给定连续的点和控制点,可以创建连续的贝塞尔曲线
  • 在连接处,通过使用完全反向的控制点,可以让贝塞尔曲线连接处更丝滑

多点连续贝塞尔曲线绘制函数

通过以PackedVector2Array形式传入多个关键点和控制点,我们便可以更轻松的绘制多点连续贝塞尔曲线。

函数如下:

# 绘制由多个点和控制点顺序组成的贝塞尔曲线
func draw_points_bezier_curve(canvas:CanvasItem,points:PackedVector2Array,ctls:PackedVector2Array,points_count:=10):
	# 求所有点之间的贝塞尔曲线点
	for i in range(points.size() -1):
		var seg = [points[i],points[i+1]]        # 线段
		var ctl = [ctls[i * 2],ctls[i * 2 + 1]]  # 控制点
		draw_bezier_curve(canvas,seg[0],seg[1],ctl[0],ctl[1],points_count)

测试代码:

extends Node2D

# 曲线关键点
var points:PackedVector2Array = [
	Vector2(100,100),
	Vector2(200,200),
	Vector2(400,300)
]
# 控制点
var ctls:PackedVector2Array = [
	pVector2(-90,100),
	pVector2(-45,100),
	pVector2(135,100),
	pVector2(45,100)
]

var curve_color:= Color.WHITE       # 曲线绘制颜色
var ctl_color:= Color.AQUAMARINE    # 控制点和连线绘制颜色


func _draw() -> void:
	draw_points_bezier_curve(self,points,ctls,50)

绘制效果:

image.png

绘制心形曲线

通过利用上面的多点连续贝塞尔曲线绘制函数,我们便可以通过一系列顶点和控制点数据,绘制处一个简单的心形曲线。

extends Node2D

# 曲线关键点
var points:PackedVector2Array = [
	Vector2(100,100),
	Vector2(100,200),
	Vector2(100,100),
]
# 控制点
var ctls:PackedVector2Array = [
	pVector2(-38,120),
	pVector2(-25,100),
	pVector2(-155,100),
	pVector2(-142,120),
]

绘制效果:

image.png

基于贝塞尔曲线的特殊图形参数化函数

一些复杂但常见的图形比如心形等,起始可以用几个坐标点和控制点数据描述和复现。
因此完全可以基于基础的图形绘制函数结合贝塞尔曲线,来生成复杂的图形。
甚至可以编写相应的函数来快速生成某种图形。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1887438.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

X-ObjectMount: 对象存储访问接入的新选择

XEOS 自 2017 年发布面世以来,历经 7 年的研发迭代,上个月正式发布了 XSKY SDS 6.4 版本,包含了最新的多站点统一命名空间能力,也标志了 XEOS 在对象存储领域的全方面优势和领先市场地位。 在 XSKY 过去对象存储服务历程里&#…

mysql 命令 —— 查看表信息(show table status)

查询表信息,如整个表的数据量大小、表的索引占用空间大小等 1、查询某个库下面的所有表信息: SHOW TABLE STATUS FROM your_database_name;2、查询指定的表信息: SHOW TABLE STATUS LIKE your_table_name;如:Data_length 显示表…

openGauss真的比PostgreSQL差了10年?

前不久写了MogDB针对PostgreSQL的兼容性文章,我在文中提到针对PostgreSQL而言,MogDB兼容性还是不错的,其中也给出了其中一个能源客户之前POC的迁移报告数据。 But很快我发现总有人回留言喷我,而且我发现每次喷的这帮人是根本不看文…

Python基础003

Python流程控制基础 1.条件语句 内置函数input a input("请输入一段内容:") print(a) print(type(a))代码执行的时候遇到input函数,就会等键盘输入结果,已回车为结束标志,也就时说输入回车后代码才会执行 2.顺序执行…

【问题记录】如何在xftp上查看隐藏文件。

显示隐藏的文件夹 用xftp连接到服务器后,发现有些隐藏的文件夹并未显示出来,通过以下配置,即可使隐藏的文件夹给显示出来。 1.点击菜单栏的"小齿轮"按钮: 2.勾选显示隐藏的文件夹: 3.点击确定即可。

古韵流光:探秘五代耀州窑青瓷提梁倒灌壶的奇妙设计

在陕西历史博物馆的静谧展厅中,一件千年前的瓷器静静陈列,它不仅承载着历史的沉淀,更凝聚了古代匠人的非凡智慧。这便是五代时期的耀州窑青瓷提梁倒灌壶,一件巧夺天工的艺术品,其独特的设计至今仍让人叹为观止。 一、倒…

算法mq 交互通用校验模块设计

背景 当前与算法交互均通过rocketMQ异步交互,绝大部分场景一条请求mq消息应对应一条返回mq,但由于各种原因(消息积压、程序bug),可能会导致返回mq超时未返回或者消息丢失。工程侧针对一些重要场景 case by case的通过…

【web3】分享一个web入门学习平台-HackQuest

前言 一直想进入web3行业,但是没有什么途径,偶然在电鸭平台看到HackQuest的共学营,发现真的不错,并且还接触到了黑客松这种形式。 链接地址:HackQuest 平台功能 学习路径:平台有完整的学习路径&#xff…

VS2022+Qt+OpenCV Debug模式下,循环中格式转换引起的内存异常问题 debug_heap.cpp

文章目录 前言一、问题二、报错1.提示图片2.提示堆栈3.反汇编位置 三、解决办法总结 前言 最近在使用VS2022,C,OpenCV,Qt开发时,遇到了一个疑难杂症-在循环中执行字符串格式转换会触发内存异常,经过痛苦的排查过程&am…

Ubuntu下反弹shell的思考

目录 Ubuntu的命令执行环境 bash (Bourne Again SHell): sh (Bourne SHell): dash (Debian Almquist SHell): 它们之间的关系: 可能遇到的问题 一、脚本权限问题 二、命令执行环境(shell解释器)问题 如何解决? 1.修改/bin/sh软连接的指向为bas…

C++字体库开发

建议根据字体需求,多个组合使用。高度定制可基于freeTypeharfbuzz基础库完成。 GitHub - GNOME/pango: Read-only mirror of https://gitlab.gnome.org/GNOME/pango GitHub - googlefonts/fontview: Demo app that displays fonts with a free/libre/open-source …

Java_多线程:线程和死锁

一、线程 1、线程的状态流转 新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t new MyThread();就绪状态(Runnable):当调用线程对象的start()方法&…

JAVA极简图书管理系统,初识springboot后端项目

前提条件: 具备基础的springboot 知识 Java基础 废话不多说! 创建项目 配置所需环境 将application.properties>application.yml 配置以下环境 数据库连接MySQL 自己创建的数据库名称为book_test server:port: 8080 spring:datasource:url:…

搜索型数据库的技术发展历程与趋势前瞻

概述 随着数字科技的飞速发展和信息量的爆炸性增长,搜索引擎已成为我们获取信息的首选途径之一,典型的代表厂商如 Google。然而,随着用户需求的不断演变,传统的搜索技术已经无法满足人们对信息的实时性、个性化和多样性的需求。 …

C++基础知识-编译相关

记录C语言相关的基础知识 1 C源码到可执行文件的四个阶段 预处理(.i)、编译(.s)、汇编(.obj)、链接。 1.1 预处理 预处理阶段,主要完成宏替换、文件展开、注释删除、条件编译展开、添加行号和文件名标识,输出.i/.ii预处理文件。 宏替换,…

AI的价值——不再那么“废”人,保险行业用AI人员流失减少20%

最近有个热点挺让人唏嘘的,某咖啡店员工对顾客泼咖啡粉,我们对于这个事件不予评价。但是对员工这种情绪崩溃,我们所接触的行业中也有不少例子,比如保险行业,相信大家经常会被打保险电话,这类电话很容易就被…

K8s 的最后一片拼图:dbPaaS

K8s 的发展使得私有云跟公共云之间的技术差不断的缩小,不管是在私有云还是公共云,大家今天都在基于 K8s 去开发 PaaS 系统。而 K8s 作为构建 PaaS 的基础,其全景图里还缺最后一块“拼图”——dbPaaS。作为一个云数据库行业干了十几年的资深从…

Swin Transformer:最佳论文,准确率和性能双佳的视觉Transformer | ICCV 2021

论文提出了经典的Vision Transormer模型Swin Transformer,能够构建层级特征提高任务准确率,而且其计算复杂度经过各种加速设计,能够与输入图片大小成线性关系。从实验结果来看,Swin Transormer在各视觉任务上都有很不错的准确率&a…

哈尔滨如何选择合适的等保测评机构?

选择合适的等保测评机构确实需要细致考虑,您提到的八个方面已经非常全面,涵盖了资质、专业能力、服务质量和合规性等多个关键点。为了进一步确保所选机构的可靠性,还可以考虑以下几点: 1.技术创新与工具:了解测评机构是…

UE5的安装与基本操作(一)

文章目录 前言安装UE5新建第一个游戏项目基本游览方式对目标进行变换各种变换对齐 快速定位目标 总结 前言 Unreal Engine 5 (UE5) 是一款由 Epic Games 开发的实时 3D 创作平台,用于制作游戏、电影、动画、建筑可视化和其他类型的交互式体验。UE5 提供了一系列强大…