mirror of
https://github.com/Pomax/BezierInfo-2.git
synced 2025-08-22 08:13:51 +02:00
10188 lines
472 KiB
HTML
Generated
10188 lines
472 KiB
HTML
Generated
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>贝塞尔曲线底漆</title>
|
||
|
||
<base href=".." />
|
||
|
||
<link rel="icon" href="images/favicon.png" type="image/png" />
|
||
|
||
<link rel="alternate" type="application/rss+xml" title="RSS" href="news/rss.xml" />
|
||
|
||
<!-- page styling -->
|
||
<link rel="preload" href="images/paper.png" as="image" />
|
||
<style>
|
||
:root[lang="zh-CN"] {
|
||
font-family: "华文细黑", "STXihei", "PingFang TC", "微软雅黑体", "Microsoft YaHei New", "微软雅黑", "Microsoft Yahei", "宋体", SimSun,
|
||
"Helvetica Neue", Helvetica, Arial, sans-serif;
|
||
font-size: 16.7px;
|
||
}
|
||
</style>
|
||
|
||
<link rel="stylesheet" href="style.css" />
|
||
|
||
<!-- And a slew of SEO related meta elements, because being discoverable is important -->
|
||
<meta
|
||
name="description"
|
||
content="A detailed explanation of Bézier curves, and how to do the many things that we commonly want to do with them."
|
||
/>
|
||
|
||
<!-- opengraph information -->
|
||
<meta property="og:title" content="贝塞尔曲线底漆" />
|
||
<meta property="og:image" content="https://pomax.github.io/bezierinfo/images/og-image.png" />
|
||
<meta property="og:type" content="text" />
|
||
<meta property="og:url" content="https://pomax.github.io/bezierinfo/zh-CN" />
|
||
<meta
|
||
property="og:description"
|
||
content="A detailed explanation of Bézier curves, and how to do the many things that we commonly want to do with them."
|
||
/>
|
||
<meta property="og:locale" content="zh-CN" />
|
||
<meta property="og:type" content="article" />
|
||
<meta property="og:published_time" content="2013-06-13T12:00:00+00:00" />
|
||
<meta property="og:updated_time" content="2022-01-04T19:49:37+00:00" />
|
||
<meta property="og:author" content="Mike 'Pomax' Kamermans" />
|
||
<meta property="og:section" content="Bézier Curves" />
|
||
<meta property="og:tag" content="Bézier Curves" />
|
||
|
||
<!-- twitter card information -->
|
||
<meta name="twitter:card" content="summary" />
|
||
<meta name="twitter:site" content="@TheRealPomax" />
|
||
<meta name="twitter:creator" content="@TheRealPomax" />
|
||
<meta name="twitter:image" content="https://pomax.github.io/bezierinfo/images/og-image.png" />
|
||
<meta name="twitter:url" content="https://pomax.github.io/bezierinfo/zh-CN" />
|
||
<meta
|
||
name="twitter:description"
|
||
content="A detailed explanation of Bézier curves, and how to do the many things that we commonly want to do with them."
|
||
/>
|
||
|
||
<!-- my own referral/page hit tracker, because Google knows enough -->
|
||
<script src="./js/site/referrer.js" type="module" async></script>
|
||
|
||
<!--
|
||
The part that makes interactive graphics work: an HTML5 <graphics-element> custom element.
|
||
Note that we're not defering this: we just want it to kick in as soon as possible, and
|
||
given how much HTML there is, that means this can, and thus should, kick in before the
|
||
document is done even transferring.
|
||
-->
|
||
<script src="./js/graphics-element/graphics-element.js" type="module" async></script>
|
||
<link rel="stylesheet" href="./js/graphics-element/graphics-element.css" />
|
||
|
||
<!-- make images lazy load much earlier -->
|
||
<script src="./js/site/better-lazy-loading.js" type="module" async defer></script>
|
||
</head>
|
||
|
||
<body>
|
||
<div class="dev" style="display: none;">
|
||
DEV PREVIEW ONLY
|
||
<script>
|
||
(function () {
|
||
var loc = window.location.toString();
|
||
if (loc.includes("localhost") || loc.includes("BezierInfo-2")) {
|
||
var e = document.querySelector("div.dev");
|
||
e.removeAttribute("style");
|
||
}
|
||
})();
|
||
</script>
|
||
</div>
|
||
|
||
<div class="github">
|
||
<img src="images/ribbon.png" alt="This page on GitHub" style="border: none;" usemap="#githubmap" width="200" height="149" />
|
||
<map name="githubmap">
|
||
<area shape="poly" coords="30,0, 200,0, 200,114" href="http://github.com/pomax/BezierInfo-2" alt="This page on GitHub" />
|
||
</map>
|
||
</div>
|
||
|
||
<div class="notforprint scl">
|
||
<img src="images/icons.gif" usemap="#rhtimap" title="Share this on social media" />
|
||
<map name="rhtimap">
|
||
<area
|
||
class="sclnk-rdt"
|
||
shape="rect"
|
||
coords="0, 0, 19, 15"
|
||
href="https://www.reddit.com/submit?url=https://pomax.github.io/bezierinfo&title=A Primer on Bézier Curves&text=A free, online book for when you really need to know how to do Bézier things."
|
||
alt="submit to reddit"
|
||
title="submit to reddit"
|
||
/>
|
||
<area
|
||
class="sclnk-hn"
|
||
shape="rect"
|
||
coords="0, 20, 19, 35"
|
||
href="https://news.ycombinator.com/submitlink?u=https://pomax.github.io/bezierinfo&t=A Primer on Bézier Curves"
|
||
alt="submit to hacker news"
|
||
title="submit to hacker news"
|
||
/>
|
||
<area
|
||
class="sclnk-twt"
|
||
shape="rect"
|
||
coords="0, 40, 19, 55"
|
||
href="https://twitter.com/intent/tweet?hashtags=bezier,curves,maths&original_referer=https://pomax.github.io/bezierinfo&text=Reading “A Primer on Bezier Curves” by @TheRealPomax over on https://pomax.github.io/bezierinfo"
|
||
alt="tweet your read"
|
||
title="tweet your read"
|
||
/>
|
||
</map>
|
||
</div>
|
||
|
||
<script src="./js/site/social-updater.js" async defer></script>
|
||
|
||
<header>
|
||
<h1>
|
||
贝塞尔曲线底漆<a class="rss-link" href="news/rss.xml"><img src="images/rss.png" /></a>
|
||
</h1>
|
||
<h2>A free, online book for when you really need to know how to do Bézier things.</h2>
|
||
<div>
|
||
<span>Read this in your own language:</span>
|
||
<ul class="lang-switcher">
|
||
<li><a href="./index.html">English</a> </li>
|
||
<li><a href="./ja-JP/index.html">日本語</a> <span class="localisation-progress">(24%)</span></li>
|
||
<li><a href="./zh-CN/index.html">中文</a> <span class="localisation-progress">(22%)</span></li>
|
||
<li><a href="./ru-RU/index.html">Русский</a> <span class="localisation-progress">(24%)</span></li>
|
||
<li><a href="./uk-UA/index.html">Українська</a> <span class="localisation-progress">(2%)</span></li>
|
||
<li><a href="./ko-KR/index.html">한국어</a> <span class="localisation-progress">(2%)</span></li>
|
||
</ul>
|
||
<p>
|
||
(Don't see your language listed, or want to see it reach 100%?
|
||
<a href="https://github.com/Pomax/BezierInfo-2/wiki/help-localize-the-primer-on-bezier-curves">Help translate this content!</a>)
|
||
</p>
|
||
</div>
|
||
|
||
<p>
|
||
Welcome to the Primer on Bezier Curves. This is a free website/ebook dealing with both the maths and programming aspects of Bezier Curves,
|
||
covering a wide range of topics relating to drawing and working with that curve that seems to pop up everywhere, from Photoshop paths to CSS
|
||
easing functions to Font outline descriptions.
|
||
</p>
|
||
<p>
|
||
If this is your first time here: welcome! Let me know if you were looking for anything in particular that the primer doesn't cover over on the
|
||
<a href="https://github.com/Pomax/BezierInfo-2/issues">issue tracker</a>!
|
||
</p>
|
||
|
||
<h2>Donations and sponsorship</h2>
|
||
|
||
<p>
|
||
If this is a resource that you're using for research, as work reference, or even writing your own software, please consider
|
||
<a href="https://www.paypal.com/donate/?cmd=_s-xclick&hosted_button_id=3BNHGHZAS3DP6&locale.x=en_CA">donating</a> (any amount helps) or
|
||
signing up as <a href="https://www.patreon.com/bezierinfo">a patron on Patreon</a>. I don't get paid to work on this, so if you find this site
|
||
valuable, and you'd like it to stick around for a long time to come, a lot of coffee went into writing this over the years, and a lot more
|
||
coffee will need to go into it yet: if you can spare a coffee, you'd be helping keep a resource alive and well.
|
||
</p>
|
||
<p>
|
||
Also, if you are a company and your staff uses this book as a resource, or you use it as an onboarding resource, then please: consider
|
||
sponsoring the site! I am more than happy to work with your finance department on sponsorship invoicing and recognition.
|
||
</p>
|
||
|
||
<!--
|
||
<div class="btcfh">
|
||
<div>
|
||
<h3>
|
||
Bitcoin donations:
|
||
</h3>
|
||
<p>
|
||
If you prefer to donate via Bitcoin, you can donate either directly to
|
||
<a class="btclk" href="bitcoin:3GY1HbQ2cH9V4xBLnRYdEfc42Nd1ZyjLZu?label=Primer%20on%20Bezier%20Curves">3GY1HbQ2cH9V4xBLnRYdEfc42Nd1ZyjLZu</a>
|
||
or use the QR code on the right, if that's the kind of convenience you prefer =)
|
||
</p>
|
||
|
||
</div>
|
||
<div class="btcqr">
|
||
<a href="bitcoin:3GY1HbQ2cH9V4xBLnRYdEfc42Nd1ZyjLZu?label=Primer%20on%20Bezier%20Curves">
|
||
<img src="./images/3GY1HbQ2cH9V4xBLnRYdEfc42Nd1ZyjLZu.PNG">
|
||
</a>
|
||
</div>
|
||
</div>
|
||
-->
|
||
|
||
<br style="clear: both;" />
|
||
|
||
<p>— <a href="https://twitter.com/TheRealPomax">Pomax</a></p>
|
||
<noscript>
|
||
<div class="note">
|
||
<header>
|
||
<h2>This site (obviously) works best with JS enabled</h2>
|
||
<h3>But it's not required.</h3>
|
||
</header>
|
||
|
||
<p>
|
||
If you're reading this text block, then you have scripts disabled: thankfully, that's perfectly fine, and this site is not going to punish
|
||
you for making smart choices around privacy and security in your browser. All the content will show just fine, you can still read the
|
||
text, navigate to sections, and see the graphics that are used to illustrate the concepts that individual sections talk about.
|
||
</p>
|
||
|
||
<p>
|
||
<strong>However</strong>, a big part of this primer's experience is the fact that all graphics are interactive, and for that to work, HTML
|
||
Custom Elements need to work, which requires Javascript to be enabled. If anything, you'll probably want to allow scripts to run just for
|
||
this site, and keep blocking everything else. Although that does mean you won't see comments, which use Disqus's comment system, and you
|
||
won't get convenient "share a link to the section you're reading right now" buttons, if that's something you like to do.
|
||
</p>
|
||
</div>
|
||
</noscript>
|
||
<nav aria-labelledby="toc">
|
||
<h1 id="toc">目录</h1>
|
||
<h4>前言</h4>
|
||
<ol class="preamble">
|
||
<li><a href="zh-CN/index.html#preface">序言 </a></li>
|
||
<li><a href="zh-CN/index.html#changelog">What's new</a></li>
|
||
</ol>
|
||
<h4>Main content</h4>
|
||
<ol>
|
||
<li><a href="zh-CN/index.html#introduction">简单介绍</a></li>
|
||
<li><a href="zh-CN/index.html#whatis">什么构成了贝塞尔曲线?</a></li>
|
||
<li><a href="zh-CN/index.html#explanation">贝塞尔曲线的数学原理</a></li>
|
||
<li><a href="zh-CN/index.html#control">控制贝塞尔的曲率</a></li>
|
||
<li><a href="zh-CN/index.html#weightcontrol">Controlling Bézier curvatures, part 2: Rational Béziers</a></li>
|
||
<li><a href="zh-CN/index.html#extended">贝塞尔区间[0,1]</a></li>
|
||
<li><a href="zh-CN/index.html#matrix">用矩阵运算来表示贝塞尔曲率</a></li>
|
||
<li><a href="zh-CN/index.html#decasteljau">de Casteljau's 算法</a></li>
|
||
<li><a href="zh-CN/index.html#flattening">简化绘图</a></li>
|
||
<li><a href="zh-CN/index.html#splitting">分割曲线</a></li>
|
||
<li><a href="zh-CN/index.html#matrixsplit">Splitting curves using matrices</a></li>
|
||
<li><a href="zh-CN/index.html#reordering">Lowering and elevating curve order</a></li>
|
||
<li><a href="zh-CN/index.html#derivatives">Derivatives</a></li>
|
||
<li><a href="zh-CN/index.html#pointvectors">Tangents and normals</a></li>
|
||
<li><a href="zh-CN/index.html#pointvectors3d">Working with 3D normals</a></li>
|
||
<li><a href="zh-CN/index.html#components">Component functions</a></li>
|
||
<li><a href="zh-CN/index.html#extremities">Finding extremities: root finding</a></li>
|
||
<li><a href="zh-CN/index.html#boundingbox">Bounding boxes</a></li>
|
||
<li><a href="zh-CN/index.html#aligning">Aligning curves</a></li>
|
||
<li><a href="zh-CN/index.html#tightbounds">Tight bounding boxes</a></li>
|
||
<li><a href="zh-CN/index.html#inflections">Curve inflections</a></li>
|
||
<li><a href="zh-CN/index.html#canonical">The canonical form (for cubic curves)</a></li>
|
||
<li><a href="zh-CN/index.html#yforx">Finding Y, given X</a></li>
|
||
<li><a href="zh-CN/index.html#arclength">Arc length</a></li>
|
||
<li><a href="zh-CN/index.html#arclengthapprox">Approximated arc length</a></li>
|
||
<li><a href="zh-CN/index.html#curvature">Curvature of a curve</a></li>
|
||
<li><a href="zh-CN/index.html#tracing">Tracing a curve at fixed distance intervals</a></li>
|
||
<li><a href="zh-CN/index.html#intersections">Intersections</a></li>
|
||
<li><a href="zh-CN/index.html#curveintersection">Curve/curve intersection</a></li>
|
||
<li><a href="zh-CN/index.html#abc">The projection identity</a></li>
|
||
<li><a href="zh-CN/index.html#pointcurves">Creating a curve from three points</a></li>
|
||
<li><a href="zh-CN/index.html#projections">Projecting a point onto a Bézier curve</a></li>
|
||
<li><a href="zh-CN/index.html#circleintersection">Intersections with a circle</a></li>
|
||
<li><a href="zh-CN/index.html#molding">Molding a curve</a></li>
|
||
<li><a href="zh-CN/index.html#curvefitting">Curve fitting</a></li>
|
||
<li><a href="zh-CN/index.html#catmullconv">Bézier curves and Catmull-Rom curves</a></li>
|
||
<li><a href="zh-CN/index.html#catmullfitting">Creating a Catmull-Rom curve from three points</a></li>
|
||
<li><a href="zh-CN/index.html#polybezier">Forming poly-Bézier curves</a></li>
|
||
<li><a href="zh-CN/index.html#offsetting">Curve offsetting</a></li>
|
||
<li><a href="zh-CN/index.html#graduatedoffset">Graduated curve offsetting</a></li>
|
||
<li><a href="zh-CN/index.html#circles">Circles and quadratic Bézier curves</a></li>
|
||
<li><a href="zh-CN/index.html#circles_cubic">Circular arcs and cubic Béziers</a></li>
|
||
<li><a href="zh-CN/index.html#arcapproximation">Approximating Bézier curves with circular arcs</a></li>
|
||
<li><a href="zh-CN/index.html#bsplines">B-Splines</a></li>
|
||
<li><a href="zh-CN/index.html#comments">Comments and questions</a></li>
|
||
</ol>
|
||
</nav>
|
||
</header>
|
||
|
||
<main>
|
||
<section id="preface">
|
||
<h1>序言</h1>
|
||
<p>
|
||
我们通常用线条来绘制2D图形,大致分为两种线条:直线和曲线。不论我们动手还是用电脑,都能很容易地画出第一种线条。只要给电脑起点和终点,砰!直线就画出来了。没什么好疑问的。
|
||
</p>
|
||
<p>
|
||
然而,绘制曲线却是个大问题。虽然我们可以很容易地徒手画出曲线,但除非给出描述曲线的数学函数,不然计算机无法画出曲线。实际上,画直线时也需要数学函数,但画直线所需的方程式很简单,我们在这里不去考虑。在计算机看来,所有线条都是“函数”,不管它们是直线还是曲线。然而,这就表示我们需要找到能在计算机上表现良好的曲线方程。这样的曲线有很多种,在本文我们主要关注一类特殊的、备受关注的函数,基本上任何画曲线的地方都会用到它:贝塞尔曲线。
|
||
</p>
|
||
<p>
|
||
它们是以<a href="https://en.wikipedia.org/wiki/Pierre_B%C3%A9zier">Pierre Bézier</a
|
||
>命名的,尽管他并不是第一个,或者说唯一“发明”了这种曲线的人,但他让世界知道了这种曲线十分适合设计工作(在1962年为Renault工作并发表了他的研究)。有人也许会说数学家<a
|
||
href="https://en.wikipedia.org/wiki/Paul_de_Casteljau"
|
||
>Paul de Casteljau</a
|
||
>是第一个发现这类曲线特性的人,在Citroën工作时,他提出了一种很优雅的方法来画这些曲线。然而,de
|
||
Casteljau没有发表他的工作,这使得“谁先发现”这一问题很难有一个确切的答案。 贝塞尔曲线本质上是伯恩斯坦多项式,这是<a
|
||
href="https://en.wikipedia.org/wiki/Sergei_Natanovich_Bernstein"
|
||
>Sergei Natanovich Bernstein</a
|
||
>研究的一种数学函数,关于它们的出版物至少可以追溯到1912年。无论如何,这些都只是一些冷知识,你可能更在意的是这些曲线很方便:你可以连接多条贝塞尔曲线,并且连接起来的曲线看起来就像是一条曲线。甚至,在你在Photoshop中画“路径”或使用一些像Flash、Illustrator和Inkscape这样的矢量绘图程序时,所画的曲线都是贝塞尔曲线。
|
||
</p>
|
||
<p>
|
||
那么,要是你自己想编程实现它们呢?有哪些陷阱?你怎么画它们?包围盒是怎么样的,怎么确定交点,怎么拉伸曲线,简单来说:你怎么对曲线做一切你想做的事?这就是这篇文章想说的。准备好学习一些数学吧!
|
||
</p>
|
||
<div class="note">
|
||
<h2>注意:几乎所有的贝塞尔图形都是可交互的。</h2>
|
||
<p>这个页面使用了基于<a href="https://pomax.github.io/bezierjs/">Bezier.js</a> 的可交互例子。</p>
|
||
<!-- The following is no longer true
|
||
,还有一些用[MathJax](https://MathJax.org) 排版的“真正的”数学(LaTeX形式)。这个页面是用Webpack离线生成的React应用,这便让加入“查看源码”选项更具挑战性了。我仍然试图将它们添加回来,但跟前几年的版本相比,不觉得它能够支撑部署这个更新。
|
||
-->
|
||
|
||
<h2>这本书是开源的。</h2>
|
||
<p>
|
||
这本书是开源的软件项目,现有两个github仓库。第一个<a href="https://github.com/pomax/bezierinfo">https://github.com/pomax/bezierinfo</a
|
||
>,它是你现在在看的这个,纯粹用来展示的版本。另外一个<a href="https://github.com/pomax/BezierInfo-2"
|
||
>https://github.com/pomax/BezierInfo-2</a
|
||
>,是带有所有html, javascript和css的开发版本。你可以fork任意一个,随便做些什么,当然除了把它当作自己的作品来商用。 =)
|
||
</p>
|
||
<h2>用到的数学将有多复杂?</h2>
|
||
<p>
|
||
这份入门读物用到的大部分数学知识都是高中所学的。如果你理解基本的计算并能看懂英文的话,就能上手这份材料。有时候会用到<em>复杂</em>一点的数学,但如果你不想深究它们,可以选择跳过段落里的“详解”部分,或者直接跳到章节末尾,避开那些看起来很深入的数学。章节的末尾往往会列出一些结论,因此你可以直接利用这些结论。
|
||
</p>
|
||
<h2>问题,评论:</h2>
|
||
<p>
|
||
如果你有对于新章节的一些建议,点击
|
||
<a href="https://github.com/pomax/BezierInfo-2/issues">Github issue tracker</a>
|
||
(也可以点右上角的repo链接)。如果你有关于材料的一些问题,由于我现在在做改写工作,目前没有评论功能,但你可以用issue跟踪来发表评论。一旦完成重写工作,我会把评论功能加上,或者会有“选择文字段落,点击‘问题’按钮来提问”的系统。到时候我们看看。
|
||
</p>
|
||
<h2>给我买杯咖啡?</h2>
|
||
<p>
|
||
如果你很喜欢这本书,或发现它对你要做的事很有帮助,或者你想知道怎么表达自己对这本书的感激,你可以
|
||
<a href="https://www.paypal.com/donate/?cmd=_s-xclick&hosted_button_id=3BNHGHZAS3DP6&locale.x=en_CA">给我买杯咖啡</a>
|
||
,所少钱取决于你。这份工作持续了很多年,从一份小小的简要到70多页关于贝塞尔曲线的读物,在完成它的过程中倾注了很多咖啡。我从未后悔花在这上面的每一分钟,但如果有更多咖啡的话,我可以坚持写下去!
|
||
</p>
|
||
</div>
|
||
</section>
|
||
<section id="changelog">
|
||
<h1>What's new?</h1>
|
||
<p>
|
||
This primer is a living document, and so depending on when you last look at it, there may be new content. Click the following link to expand
|
||
this section to have a look at what got added, when, or click through to the <a href="./news">News posts</a> for more detailed updates. (<a
|
||
href="./news/rss.xml"
|
||
>RSS feed</a
|
||
>
|
||
available)
|
||
</p>
|
||
<!-- non-JS content reveals are nice -->
|
||
<label for="changelogtoggle">Toggle changes</label>
|
||
<input type="checkbox" id="changelogtoggle" />
|
||
<section>
|
||
<h2>November 2020</h2>
|
||
<ul>
|
||
<li><p>Added a section on finding curve/circle intersections</p></li>
|
||
</ul>
|
||
<h2>October 2020</h2>
|
||
<ul>
|
||
<li><p>Added the Ukranian locale! Help out in getting its localization to 100%!</p></li>
|
||
</ul>
|
||
<h2>August-September 2020</h2>
|
||
<ul>
|
||
<li>
|
||
<p>
|
||
Completely overhauled the site: the Primer is now a normal web page that works fine with JS disabled, but obviously better with JS
|
||
turned on.
|
||
</p>
|
||
</li>
|
||
</ul>
|
||
<h2>June 2020</h2>
|
||
<ul>
|
||
<li><p>Added automatic CI/CD using Github Actions</p></li>
|
||
</ul>
|
||
<h2>January 2020</h2>
|
||
<ul>
|
||
<li><p>Added reset buttons to all graphics</p></li>
|
||
<li><p>Updated to preface to correctly describe the on-page maths</p></li>
|
||
<li><p>Fixed the Catmull-Rom section because it had glaring maths errors</p></li>
|
||
</ul>
|
||
<h2>August 2019</h2>
|
||
<ul>
|
||
<li><p>Added a section on (plain) rational Bezier curves</p></li>
|
||
<li><p>Improved the Graphic component to allow for sliders</p></li>
|
||
</ul>
|
||
<h2>December 2018</h2>
|
||
<ul>
|
||
<li><p>Added a section on curvature and calculating kappa.</p></li>
|
||
<li>
|
||
<p>
|
||
Added a Patreon page! Head on over to <a href="https://www.patreon.com/bezierinfo">patreon.com/bezierinfo</a> to help support this
|
||
site!
|
||
</p>
|
||
</li>
|
||
</ul>
|
||
<h2>August 2018</h2>
|
||
<ul>
|
||
<li><p>Added a section on finding a curve's y, if all you have is the x coordinate.</p></li>
|
||
</ul>
|
||
<h2>July 2018</h2>
|
||
<ul>
|
||
<li><p>Rewrote the 3D normals section, implementing and explaining Rotation Minimising Frames.</p></li>
|
||
<li><p>Updated the section on curve order raising/lowering, showing how to get a least-squares optimized lower order curve.</p></li>
|
||
<li>
|
||
<p>(Finally) updated 'npm test' so that it automatically rebuilds when files are changed while the dev server is running.</p>
|
||
</li>
|
||
</ul>
|
||
<h2>June 2018</h2>
|
||
<ul>
|
||
<li><p>Added a section on direct curve fitting.</p></li>
|
||
<li><p>Added source links for all graphics.</p></li>
|
||
<li><p>Added this "What's new?" section.</p></li>
|
||
</ul>
|
||
<h2>April 2017</h2>
|
||
<ul>
|
||
<li><p>Added a section on 3d normals.</p></li>
|
||
<li><p>Added live-updating for the social link buttons, so they always link to the specific section you're reading.</p></li>
|
||
</ul>
|
||
<h2>February 2017</h2>
|
||
<ul>
|
||
<li><p>Finished rewriting the entire codebase for localization.</p></li>
|
||
</ul>
|
||
<h2>January 2016</h2>
|
||
<ul>
|
||
<li><p>Added a section to explain the Bezier interval.</p></li>
|
||
<li><p>Rewrote the Primer as a React application.</p></li>
|
||
</ul>
|
||
<h2>December 2015</h2>
|
||
<ul>
|
||
<li><p>Set up the split repository between BezierInfo-2 as development repository, and bezierinfo as live page.</p></li>
|
||
<li>
|
||
<p>
|
||
Removed the need for client-side LaTeX parsing entirely, so the site doesn't take a full minute or more to load all the graphics.
|
||
</p>
|
||
</li>
|
||
</ul>
|
||
<h2>May 2015</h2>
|
||
<ul>
|
||
<li><p>Switched over to pure JS rather than Processing-through-Processing.js</p></li>
|
||
<li><p>Added Cardano's algorithm for finding the roots of a cubic polynomial.</p></li>
|
||
</ul>
|
||
<h2>April 2015</h2>
|
||
<ul>
|
||
<li><p>Added a section on arc length approximations.</p></li>
|
||
</ul>
|
||
<h2>February 2015</h2>
|
||
<ul>
|
||
<li><p>Added a section on the canonical cubic Bezier form.</p></li>
|
||
</ul>
|
||
<h2>November 2014</h2>
|
||
<ul>
|
||
<li><p>Switched to HTTPS.</p></li>
|
||
</ul>
|
||
<h2>July 2014</h2>
|
||
<ul>
|
||
<li><p>Added the section on arc approximation.</p></li>
|
||
</ul>
|
||
<h2>April 2014</h2>
|
||
<ul>
|
||
<li><p>Added the section on Catmull-Rom fitting.</p></li>
|
||
</ul>
|
||
<h2>November 2013</h2>
|
||
<ul>
|
||
<li><p>Added the section on Catmull-Rom / Bezier conversion.</p></li>
|
||
<li><p>Added the section on Bezier cuves as matrices.</p></li>
|
||
</ul>
|
||
<h2>April 2013</h2>
|
||
<ul>
|
||
<li><p>Added a section on poly-Beziers.</p></li>
|
||
<li><p>Added a section on boolean shape operations.</p></li>
|
||
</ul>
|
||
<h2>March 2013</h2>
|
||
<ul>
|
||
<li><p>First drastic rewrite.</p></li>
|
||
<li><p>Added sections on circle approximations.</p></li>
|
||
<li><p>Added a section on projecting a point onto a curve.</p></li>
|
||
<li><p>Added a section on tangents and normals.</p></li>
|
||
<li><p>Added Legendre-Gauss numerical data tables.</p></li>
|
||
</ul>
|
||
<h2>October 2011</h2>
|
||
<ul>
|
||
<li>
|
||
<p>
|
||
First commit for the <a href="https://pomax.github.io/bezierinfo/">bezierinfo</a> site, based on the pre-Primer webpage that covered
|
||
the basics of Bezier curves in HTML with Processing.js examples.
|
||
</p>
|
||
</li>
|
||
</ul>
|
||
</section>
|
||
</section>
|
||
<section id="chapters">
|
||
<section id="introduction">
|
||
<h1>
|
||
<div class="nav"><a href="#toc">目录</a><a href="zh-CN/index.html#whatis">下</a></div>
|
||
<a href="zh-CN/index.html#introduction">简单介绍</a>
|
||
</h1>
|
||
<p>
|
||
让我们有个好的开始:当我们在谈论贝塞尔曲线的时候,所指的就是你在如下图像看到的东西。它们从某些起点开始,到终点结束,并且受到一个或多个的“中间”控制点的影响。本页面上的图形都是可交互的,你可以拖动这些点,看看这些形状在你的操作下会怎么变化。
|
||
</p>
|
||
<div class="figure">
|
||
<graphics-element
|
||
title="二次贝塞尔曲线"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/introduction/quadratic.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/introduction/54e9ec0600ac436b0e6f0c6b5005cf03.png" loading="lazy" />
|
||
<label>二次贝塞尔曲线</label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
<graphics-element
|
||
title="三次贝塞尔曲线"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/introduction/cubic.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/introduction/8d158a13e9a86969b99c64057644cbc6.png" loading="lazy" />
|
||
<label>三次贝塞尔曲线</label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
</div>
|
||
|
||
<p>
|
||
这些曲线在计算机辅助设计和计算机辅助制造应用(CAD/CAM)中用的很多。在图形设计软件中也常用到,像Adobe Illustrator, Photoshop, Inkscape,
|
||
Gimp等等。还可以应用在一些图形技术中,像矢量图形(SVG)和OpenType字体(ttf/otf)。许多东西都用到贝塞尔曲线,如果你想更了解它们...准备好继续往下学吧!
|
||
</p>
|
||
</section>
|
||
<section id="whatis">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#introduction">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#explanation">下</a></div>
|
||
<a href="zh-CN/index.html#whatis">什么构成了贝塞尔曲线?</a>
|
||
</h1>
|
||
<p>
|
||
操作点的移动,看看曲线的变化,可能让你感受到了贝塞尔曲线是如何表现的。但贝塞尔曲线究竟<em>是</em>什么呢?有两种方式来解释贝塞尔曲线,并且可以证明它们完全相等,但是其中一种用到了复杂的数学,另外一种比较简单。所以...我们先从简单的开始吧:
|
||
</p>
|
||
<p>
|
||
贝塞尔曲线是<a href="https://zh.wikipedia.org/wiki/%E7%BA%BF%E6%80%A7%E6%8F%92%E5%80%BC">线性插值</a
|
||
>的结果。这听起来很复杂,但你在很小的时候就做过线性插值:当你指向两个物体中的另外一个物体时,你就用到了线性插值。它就是很简单的“选出两点之间的一个点”。
|
||
</p>
|
||
<p>如果我们知道两点之间的距离,并想找出离第一个点20%间距的一个新的点(也就是离第二个点80%的间距),我们可以通过简单的计算来得到:</p>
|
||
<!--
|
||
|
||
╭ p = some point ╮
|
||
│ 1 │
|
||
│ p = some other point │
|
||
│ 2 │
|
||
Given │ distance= (p - p ) │, our new point = p + distance · ratio
|
||
│ 2 1 │ 1
|
||
│ percentage │
|
||
│ ratio= ─────────── │
|
||
╰ 100 ╯
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/whatis/c7f8cdd755d744412476b87230d0400d.svg" width="493px" height="103px" loading="lazy" />
|
||
<p>
|
||
让我们来通过实际操作看一下:下面的图形都是可交互的,因此你可以通过上下键来增加或减少插值距离,来观察图形的变化。我们从三个点构成的两条线段开始。通过对各条线段进行线性插值得到两个点,对点之间的线段再进行线性插值,产生一个新的点。最终这些点——所有的点都可以通过选取不同的距离插值产生——构成了贝塞尔曲线
|
||
:
|
||
<graphics-element
|
||
title="Linear Interpolation leading to Bézier curves"
|
||
width="825"
|
||
height="275"
|
||
src="./chapters/whatis/interpolation.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="825px" height="275px" src="./images/chapters/whatis/524dd296e96c0fe2281fb95146f8ea65.png" loading="lazy" />
|
||
<label></label>
|
||
</fallback-image>
|
||
<input type="range" min="10" max="90" step="1" value="25" class="slide-control" />
|
||
</graphics-element>
|
||
</p>
|
||
|
||
<p>这为我们引出了复杂的数学:微积分。</p>
|
||
<p>
|
||
虽然我们刚才好像没有用到这个,我们实际上只是逐步地画了一条二次曲线,而不是一次画好。贝塞尔曲线的一个很棒的特性就是它们可以通过多项式方程表示,也可以用很简单的插值形式表示。因此,反过来说,我们可以基于“真正的数学”(检查方程式,导数之类的东西),也可以通过观察曲线的“机械”构成(比如说,可以得知曲线永远不会延伸超过我们用来构造它的点),来看看这些曲线能够做什么。
|
||
</p>
|
||
<p>让我们从更深的层次来观察贝塞尔曲线。看看它们的数学表达式,从这些表达式衍生得到的属性,以及我们可以对贝塞尔曲线做的事。</p>
|
||
</section>
|
||
<section id="explanation">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#whatis">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#control">下</a></div>
|
||
<a href="zh-CN/index.html#explanation">贝塞尔曲线的数学原理</a>
|
||
</h1>
|
||
<p>
|
||
贝塞尔曲线是“参数”方程的一种形式。从数学上讲,参数方程作弊了:“方程”实际上是一个从输入到<strong>唯一</strong>输出的、良好定义的映射关系。几个输入进来,一个输出返回。改变输入变量,还是只有一个输出值。参数方程在这里作弊了。它们基本上干了这么件事,“好吧,我们想要更多的输出值,所以我们用了多个方程”。举个例子:假如我们有一个方程,通过一些计算,将假设为<i>x</i>的一些值映射到另外的值:
|
||
</p>
|
||
<!--
|
||
|
||
f(x) = cos (x)
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/explanation/0cc876c56200446c60114c1b0eeeb2cc.svg" width="96px" height="17px" loading="lazy" />
|
||
<p>
|
||
记号<i>f(x)</i>是表示函数的标准方式(为了方便起见,如果只有一个的话,我们称函数为<i>f</i>),函数的输出根据一个变量(本例中是<i>x</i>)变化。改变<i>x</i>,<i>f(x)</i>的输出值也会变。
|
||
</p>
|
||
<p>到目前没什么问题。现在,让我们来看一下参数方程,以及它们是怎么作弊的。我们取以下两个方程:</p>
|
||
<!--
|
||
|
||
f(a) = cos (a)
|
||
f(b) = sin (b)
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/explanation/a2891980850ddbb27d308ac112d69f74.svg" width="93px" height="36px" loading="lazy" />
|
||
<p>
|
||
这俩方程没什么让人印象深刻的,只不过是正弦函数和余弦函数,但正如你所见,输入变量有两个不同的名字。如果我们改变了<i>a</i>的值,<i>f(b)</i>的输出不会有变化,因为这个方程没有用到<i>a</i>。参数方程通过改变这点来作弊。在参数方程中,所有不同的方程共用一个变量,如下所示:
|
||
</p>
|
||
<!--
|
||
|
||
╭ f (t) = cos (t)
|
||
╡ a
|
||
│ f (t) = sin (t)
|
||
╰ b
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/explanation/7acc94ec70f053fd10dab69d424b02a6.svg"
|
||
width="100px"
|
||
height="40px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
多个方程,但只有一个变量。如果我们改变了<i>t</i>的值,<i>f<sub>a</sub>(t)</i>和<i>f<sub>b</sub>(t)</i>的输出都会发生变化。你可能会好奇这有什么用,答案其实很简单:对于参数曲线,如果我们用常用的标记来替代<i>f<sub>a</sub>(t)</i>和<i>f<sub>b</sub>(t)</i>,看起来就有些明朗了:
|
||
</p>
|
||
<!--
|
||
|
||
{ x = cos (t)
|
||
y = sin (t)
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/explanation/6914ba615733c387251682db7a3db045.svg" width="77px" height="40px" loading="lazy" />
|
||
<p>好了,通过一些神秘的<i>t</i>值将<i>x</i>/<i>y</i>坐标系联系起来。</p>
|
||
<p>
|
||
所以,参数曲线不像一般函数那样,通过<i>x</i>坐标来定义<i>y</i>坐标,而是用一个“控制”变量将它们连接起来。如果改变<i>t</i>的值,每次变化时我们都能得到<strong>两个</strong>值,这可以作为图形中的(<i>x</i>,<i>y</i>)坐标。比如上面的方程组,生成位于一个圆上的点:我们可以使<i>t</i>在正负极值间变化,得到的输出(<i>x</i>,<i>y</i>)都会位于一个以原点(0,0)为中心且半径为1的圆上。如果我们画出<i>t</i>从0到5时的值,将得到如下图像:
|
||
</p>
|
||
<graphics-element
|
||
title="(一部分的)圆: x=sin(t), y=cos(t)"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/explanation/circle.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/explanation/959762e39ae32407e914a687d804ff3a.png" loading="lazy" />
|
||
<label>(一部分的)圆: x=sin(t), y=cos(t)</label>
|
||
</fallback-image>
|
||
<input type="range" min="0" max="10" step="0.1" value="5" class="slide-control" />
|
||
</graphics-element>
|
||
<p>
|
||
贝塞尔曲线是(一种)参数方程,并在它的多个维度上使用相同的基本方程。在上述的例子中<i>x</i>值和<i>y</i>值使用了不同的方程,与此不同的是,贝塞尔曲线的<i>x</i>和<i>y</i>都用了“二项多项式”。那什么是二项多项式呢?
|
||
</p>
|
||
<p>你可能记得高中所学的多项式,看起来像这样:</p>
|
||
<!--
|
||
|
||
3 2
|
||
f(x) = a · x + b · x + c · x + d
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/explanation/855a34c7f72733be6529c3fb33fa1a23.svg"
|
||
width="213px"
|
||
height="20px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
如果它的最高次项是<i>x³</i>就称为“三次”多项式,如果最高次项是<i>x²</i>,称为“二次”多项式,如果只含有<i>x</i>的项,它就是一条线(不过不含任何<i>x</i>的项它就不是一个多项式!)
|
||
</p>
|
||
<p>
|
||
贝塞尔曲线不是x的多项式,它是<i>t</i>的多项式,<i>t</i>的值被限制在0和1之间,并且含有<i>a</i>,<i>b</i>等参数。它采用了二次项的形式,听起来很神奇但实际上就是混合不同值的简单描述:
|
||
</p>
|
||
<!--
|
||
|
||
linear= (1-t) + t
|
||
2 2
|
||
square= (1-t) + 2 · (1-t) · t + t
|
||
3 2 2 3
|
||
cubic= (1-t) + 3 · (1-t) · t + 3 · (1-t) · t + t
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/explanation/7f74178029422a35267fd033b392fe4c.svg"
|
||
width="367px"
|
||
height="64px"
|
||
loading="lazy"
|
||
/>
|
||
<p>我明白你在想什么:这看起来并不简单,但如果我们拿掉<i>t</i>并让系数乘以1,事情就会立马简单很多,看看这些二次项:</p>
|
||
<!--
|
||
|
||
linear= 1 + 1
|
||
square= 1 + 2 + 1
|
||
cubic= 1 + 3 + 3 + 1
|
||
quartic= 1 + 4 + 6 + 4 + 1
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/explanation/af40980136c291814e8970dc2a3d8e63.svg"
|
||
width="183px"
|
||
height="87px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
需要注意的是,2与1+1相同,3相当于2+1或1+2,6相当于3+3...如你所见,每次我们增加一个维度,只要简单地将头尾置为1,中间的操作都是“将上面的两个数字相加”。现在就能很容易地记住了。
|
||
</p>
|
||
<p>
|
||
还有一个简单的办法可以弄清参数项怎么工作的:如果我们将<i>(1-t)</i>重命名为<i>a</i>,将<i>t</i>重命名为<i>b</i>,暂时把权重删掉,可以得到这个:
|
||
</p>
|
||
<!--
|
||
|
||
linear= \colorblue a + \colorred b
|
||
square= \colorblue a · \colorblue a + \colorblue a · \colorred b + \colorred b · \colorred b
|
||
cubic= \colorblue a · \colorblue a · \colorblue a + \colorblue a · \colorblue a · \colorred b + \colorblue a · \colorred b · \colorred b + \co
|
||
|
||
|
||
lorred b · \colorred b · \colorred b
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/explanation/4319b2e361960c842a4308a610a35048.svg"
|
||
width="300px"
|
||
height="60px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
基本上它就是“每个<i>a</i>和<i>b</i>结合项”的和,在每个加号后面逐步的将<i>a</i>换成<i>b</i>。因此这也很简单。现在你已经知道了二次多项式,为了叙述的完整性,我将给出一般方程:
|
||
</p>
|
||
<!--
|
||
|
||
__ n n-i i
|
||
Bézier(n,t) = ❯ \underset binomial term\underbrace\binomni · \ \underset polynomial term\underbrace(1-t) · t
|
||
‾‾ i=0
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/explanation/39d33ea94e7527ed221a809ca6054174.svg"
|
||
width="289px"
|
||
height="55px"
|
||
loading="lazy"
|
||
/>
|
||
<p>这就是贝塞尔曲线完整的描述。在这个函数中的Σ表示了这是一系列的加法(用Σ下面的变量,从...=<值>开始,直到Σ上面的数字结束)。</p>
|
||
<div class="howtocode">
|
||
<h3>如何实现基本方程</h3>
|
||
<p>我们可以用之前说过的方程,来简单地实现基本方程作为数学构造,如下:</p>
|
||
|
||
<table class="code">
|
||
<tr>
|
||
<td>1</td>
|
||
<td rowspan="5">
|
||
<textarea disabled rows="5" role="doc-example">
|
||
function Bezier(n,t):
|
||
sum = 0
|
||
for(k=0; k<n; k++):
|
||
sum += n!/(k!*(n-k)!) * (1-t)^(n-k) * t^(k)
|
||
return sum</textarea
|
||
>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p>
|
||
我说我们“可以用”是因为我们不会这么去做:因为阶乘函数开销<em>非常大</em>。并且,正如我们在上面所看到的,我们不用阶乘也能够很容易地构造出帕斯卡三角形:一开始是[1],接着是[1,2,1],然后是[1,3,3,1]等等。下一行都比上一行多一个数,首尾都为1,中间的数字是上一行两边元素的和。
|
||
</p>
|
||
<p>我们可以很快的生成这个列表,并在之后使用这个查找表而不用再计算二次多项式的系数:</p>
|
||
|
||
<table class="code">
|
||
<tr>
|
||
<td>1</td>
|
||
<td rowspan="18">
|
||
<textarea disabled rows="18" role="doc-example">
|
||
lut = [ [1], // n=0
|
||
[1,1], // n=1
|
||
[1,2,1], // n=2
|
||
[1,3,3,1], // n=3
|
||
[1,4,6,4,1], // n=4
|
||
[1,5,10,10,5,1], // n=5
|
||
[1,6,15,20,15,6,1]] // n=6
|
||
|
||
binomial(n,k):
|
||
while(n >= lut.length):
|
||
s = lut.length
|
||
nextRow = new array(size=s+1)
|
||
nextRow[0] = 1
|
||
for(i=1, prev=s-1; i<s; i++):
|
||
nextRow[i] = lut[prev][i-1] + lut[prev][i]
|
||
nextRow[s] = 1
|
||
lut.add(nextRow)
|
||
return lut[n][k]</textarea
|
||
>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>7</td>
|
||
</tr>
|
||
<tr>
|
||
<td>8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>9</td>
|
||
</tr>
|
||
<tr>
|
||
<td>10</td>
|
||
</tr>
|
||
<tr>
|
||
<td>11</td>
|
||
</tr>
|
||
<tr>
|
||
<td>12</td>
|
||
</tr>
|
||
<tr>
|
||
<td>13</td>
|
||
</tr>
|
||
<tr>
|
||
<td>14</td>
|
||
</tr>
|
||
<tr>
|
||
<td>15</td>
|
||
</tr>
|
||
<tr>
|
||
<td>16</td>
|
||
</tr>
|
||
<tr>
|
||
<td>17</td>
|
||
</tr>
|
||
<tr>
|
||
<td>18</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p>
|
||
这里做了些什么?首先,我们声明了一个足够大的查找表。然后,我们声明了一个函数来获取我们想要的值,并且确保当一个请求的n/k对不在LUT查找表中时,先将表扩大。我们的基本函数如下所示:
|
||
</p>
|
||
|
||
<table class="code">
|
||
<tr>
|
||
<td>1</td>
|
||
<td rowspan="5">
|
||
<textarea disabled rows="5" role="doc-example">
|
||
function Bezier(n,t):
|
||
sum = 0
|
||
for(k=0; k<=n; k++):
|
||
sum += binomial(n,k) * (1-t)^(n-k) * t^(k)
|
||
return sum</textarea
|
||
>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p>
|
||
完美。当然我们可以进一步优化。为了大部分的计算机图形学目的,我们不需要任意的曲线。我们需要二次曲线和三次曲线(实际上这篇文章没有涉及任意次的曲线,因此你会在其他地方看到与这些类似的代码),这说明我们可以彻底简化代码:
|
||
</p>
|
||
|
||
<table class="code">
|
||
<tr>
|
||
<td>1</td>
|
||
<td rowspan="13">
|
||
<textarea disabled rows="13" role="doc-example">
|
||
function Bezier(2,t):
|
||
t2 = t * t
|
||
mt = 1-t
|
||
mt2 = mt * mt
|
||
return mt2 + 2*mt*t + t2
|
||
|
||
function Bezier(3,t):
|
||
t2 = t * t
|
||
t3 = t2 * t
|
||
mt = 1-t
|
||
mt2 = mt * mt
|
||
mt3 = mt2 * mt
|
||
return mt3 + 3*mt2*t + 3*mt*t2 + t3</textarea
|
||
>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>7</td>
|
||
</tr>
|
||
<tr>
|
||
<td>8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>9</td>
|
||
</tr>
|
||
<tr>
|
||
<td>10</td>
|
||
</tr>
|
||
<tr>
|
||
<td>11</td>
|
||
</tr>
|
||
<tr>
|
||
<td>12</td>
|
||
</tr>
|
||
<tr>
|
||
<td>13</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p>现在我们知道如何代用码实现基本方程了。很好。</p>
|
||
</div>
|
||
|
||
<p>既然我们已经知道基本函数的样子,是时候添加一些魔法来使贝塞尔曲线变得特殊了:控制点。</p>
|
||
</section>
|
||
<section id="control">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#explanation">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#weightcontrol">下</a></div>
|
||
<a href="zh-CN/index.html#control">控制贝塞尔的曲率</a>
|
||
</h1>
|
||
<p>
|
||
贝塞尔曲线是插值方程(就像所有曲线一样),这表示它们取一系列的点,生成一些处于这些点之间的值。(一个推论就是你永远无法生成一个位于这些控制点轮廓线外面的点,更普遍是称为曲线的外壳。这信息很有用!)实际上,我们可以将每个点对方程产生的曲线做出的贡献进行可视化,因此可以看出曲线上哪些点是重要的,它们处于什么位置。
|
||
</p>
|
||
<p>
|
||
下面的图形显示了二次曲线和三次曲线的差值方程,“S”代表了点对贝塞尔方程总和的贡献。点击拖动点来看看在特定的<i>t</i>值时,每个曲线定义的点的插值百分比。
|
||
</p>
|
||
<div class="figure">
|
||
<graphics-element
|
||
title="二次插值"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/control/lerp.js"
|
||
data-degree="3"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/control/8332e5d34b7344bbee2a2e1f4521ce46.png" loading="lazy" />
|
||
<label>二次插值</label>
|
||
</fallback-image>
|
||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control" />
|
||
</graphics-element>
|
||
<graphics-element
|
||
title="三次插值"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/control/lerp.js"
|
||
data-degree="4"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/control/1b8c5e574dc67bfb0afc3fb0a8727378.png" loading="lazy" />
|
||
<label>三次插值</label>
|
||
</fallback-image>
|
||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control" />
|
||
</graphics-element>
|
||
<graphics-element
|
||
title="15次插值"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/control/lerp.js"
|
||
data-degree="15"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/control/c26d2655e8741ef7e2eeb4f6554fc7a5.png" loading="lazy" />
|
||
<label>15次插值</label>
|
||
</fallback-image>
|
||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control" />
|
||
</graphics-element>
|
||
</div>
|
||
|
||
<p>上面有一张是15<sup>th</sup>阶的插值方程。如你所见,在所有控制点中,起点和终点对曲线形状的贡献比其他点更大些。</p>
|
||
<p>
|
||
如果我们要改变曲线,就需要改变每个点的权重,有效地改变插值。可以很直接地做到这个:只要用一个值乘以每个点,来改变它的强度。这个值照惯例称为“权重”,我们可以将它加入我们原始的贝塞尔函数:
|
||
</p>
|
||
<!--
|
||
|
||
__ n n-i i
|
||
Bézier(n,t) = ❯ \underset binomial term\underbrace\binomni · \ \underset polynomial term\underbrace(1-t) · t · \ \underset
|
||
‾‾ i=0
|
||
weight\underbracew
|
||
i
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/control/c2f2fe0ef5d0089d9dd8e5e3999405cb.svg" width="336px" height="55px" loading="lazy" />
|
||
<p>
|
||
看起来很复杂,但实际上“权重”只是我们想让曲线所拥有的坐标值:对于一条n<sup>th</sup>阶曲线,w<sup>0</sup>是起始坐标,w<sup>n</sup>是终点坐标,中间的所有点都是控制点坐标。假设说一条曲线的起点为(110,150),终点为(210,30),并受点(25,190)和点(210,250)的控制,贝塞尔曲线方程就为:
|
||
</p>
|
||
<!--
|
||
|
||
╭ 3 2 2 3
|
||
╡ x = \colordarkred 110 · (1-t) + \colordarkgreen 25 · 3 · (1-t) · t + \colordarkblue 210 · 3 · (1-t) · t + \coloramber 210 · t
|
||
│ 3 2 2 3
|
||
╰ y = \colordarkred 150 · (1-t) + \colordarkgreen 190 · 3 · (1-t) · t + \colordarkblue 250 · 3 · (1-t) · t + \coloramber 30 · t
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/control/be73034ac382b54863c7a18c2932cbbc.svg" width="473px" height="40px" loading="lazy" />
|
||
<p>这就是我们在文章开头看到的曲线:</p>
|
||
<Graphic title="我们的三次贝塞尔曲线" setup="{this.drawCubic}" draw="{this.drawCurve}" />
|
||
|
||
<p>我们还能对贝塞尔曲线做些什么?实际上还有很多。文章接下来涉及到我们可能运用到的一系列操作和算法,以及它们可以完成的任务。</p>
|
||
<div class="howtocode">
|
||
<h3>如何实现权重基本函数</h3>
|
||
<p>鉴于我们已经知道怎样实现基本函数,在其加入控制点是非常简单的:</p>
|
||
|
||
<table class="code">
|
||
<tr>
|
||
<td>1</td>
|
||
<td rowspan="5">
|
||
<textarea disabled rows="5" role="doc-example">
|
||
function Bezier(n,t,w[]):
|
||
sum = 0
|
||
for(k=0; k<n; k++):
|
||
sum += w[k] * binomial(n,k) * (1-t)^(n-k) * t^(k)
|
||
return sum</textarea
|
||
>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p>下面是优化过的版本:</p>
|
||
|
||
<table class="code">
|
||
<tr>
|
||
<td>1</td>
|
||
<td rowspan="13">
|
||
<textarea disabled rows="13" role="doc-example">
|
||
function Bezier(2,t,w[]):
|
||
t2 = t * t
|
||
mt = 1-t
|
||
mt2 = mt * mt
|
||
return w[0]*mt2 + w[1]*2*mt*t + w[2]*t2
|
||
|
||
function Bezier(3,t,w[]):
|
||
t2 = t * t
|
||
t3 = t2 * t
|
||
mt = 1-t
|
||
mt2 = mt * mt
|
||
mt3 = mt2 * mt
|
||
return w[0]*mt3 + 3*w[1]*mt2*t + 3*w[2]*mt*t2 + w[3]*t3</textarea
|
||
>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>7</td>
|
||
</tr>
|
||
<tr>
|
||
<td>8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>9</td>
|
||
</tr>
|
||
<tr>
|
||
<td>10</td>
|
||
</tr>
|
||
<tr>
|
||
<td>11</td>
|
||
</tr>
|
||
<tr>
|
||
<td>12</td>
|
||
</tr>
|
||
<tr>
|
||
<td>13</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p>现在我们知道如何编程实现基本权重函数了。</p>
|
||
</div>
|
||
</section>
|
||
<section id="weightcontrol">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#control">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#extended">下</a></div>
|
||
<a href="zh-CN/index.html#weightcontrol">Controlling Bézier curvatures, part 2: Rational Béziers</a>
|
||
</h1>
|
||
<p>
|
||
We can further control Bézier curves by "rationalising" them: that is, adding a "ratio" value in addition to the weight value discussed in
|
||
the previous section, thereby gaining control over "how strongly" each coordinate influences the curve.
|
||
</p>
|
||
<p>Adding these ratio values to the regular Bézier curve function is fairly easy. Where the regular function is the following:</p>
|
||
<!--
|
||
|
||
__ n n-i i
|
||
Bézier(n,t) = ❯ \binomni · (1-t) · t · w
|
||
‾‾ i=0 i
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/weightcontrol/ceac4259d2aed0767c7765d2237ca1a3.svg"
|
||
width="276px"
|
||
height="41px"
|
||
loading="lazy"
|
||
/>
|
||
<p>The function for rational Bézier curves has two more terms:</p>
|
||
<!--
|
||
|
||
__ n n-i i
|
||
❯ \binomni · (1-t) · t · w · \colorblue ratio
|
||
‾‾ i=0 i i
|
||
Rational Bézier(n,t) = ────────────────────────────────────────────────────────────
|
||
__ n n-i i
|
||
\colorblue ❯ \binomni · (1-t) · t · ratio
|
||
‾‾ i=0 i
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/weightcontrol/942e3b3cacc7f403ad95fcd4acce7d19.svg"
|
||
width="408px"
|
||
height="48px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
In this, the first new term represents an additional weight for each coordinate. For example, if our ratio values are [1, 0.5, 0.5, 1]
|
||
then <code>ratio<sub>0</sub> = 1</code>, <code>ratio<sub>1</sub> = 0.5</code>, and so on, and is effectively identical as if we were just
|
||
using different weight. So far, nothing too special.
|
||
</p>
|
||
<p>
|
||
However, the second new term is what makes the difference: every point on the curve isn't just a "double weighted" point, it is a
|
||
<em>fraction</em> of the "doubly weighted" value we compute by introducing that ratio. When computing points on the curve, we compute the
|
||
"normal" Bézier value and then <em>divide</em> that by the Bézier value for the curve that only uses ratios, not weights.
|
||
</p>
|
||
<p>
|
||
This does something unexpected: it turns our polynomial into something that <em>isn't</em> a polynomial anymore. It is now a kind of curve
|
||
that is a super class of the polynomials, and can do some really cool things that Bézier curves can't do "on their own", such as perfectly
|
||
describing circles (which we'll see in a later section is literally impossible using standard Bézier curves).
|
||
</p>
|
||
<p>
|
||
But the best way to show what this does is to do literally that: let's look at the effect of "rationalising" our Bézier curves using an
|
||
interactive graphic for a rationalised curves. The following graphic shows the Bézier curve from the previous section, "enriched" with
|
||
ratio factors for each coordinate. The closer to zero we set one or more terms, the less relative influence the associated coordinate
|
||
exerts on the curve (and of course the higher we set them, the more influence they have). Try to change the values and see how it affects
|
||
what gets drawn:
|
||
</p>
|
||
<graphics-element
|
||
title="Our rational cubic Bézier curve"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/weightcontrol/rational.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/weightcontrol/3d71e2b9373684eebcb0dc8563f70b18.png" loading="lazy" />
|
||
<label>Our rational cubic Bézier curve</label>
|
||
</fallback-image>
|
||
<input type="range" min="0.01" max="2" value="1" step="0.01" class="ratio-1" />
|
||
<input type="range" min="0.01" max="2" value="1" step="0.01" class="ratio-2" />
|
||
<input type="range" min="0.01" max="2" value="1" step="0.01" class="ratio-3" />
|
||
<input type="range" min="0.01" max="2" value="1" step="0.01" class="ratio-4" />
|
||
</graphics-element>
|
||
<p>
|
||
You can think of the ratio values as each coordinate's "gravity": the higher the gravity, the closer to that coordinate the curve will
|
||
want to be. You'll also notice that if you simply increase or decrease all the ratios by the same amount, nothing changes... much like
|
||
with gravity, if the relative strengths stay the same, nothing really changes. The values define each coordinate's influence
|
||
<em>relative to all other points</em>.
|
||
</p>
|
||
<div class="howtocode">
|
||
<h3>How to implement rational curves</h3>
|
||
<p>Extending the code of the previous section to include ratios is almost trivial:</p>
|
||
|
||
<table class="code">
|
||
<tr>
|
||
<td>1</td>
|
||
<td rowspan="26">
|
||
<textarea disabled rows="26" role="doc-example">
|
||
function RationalBezier(2,t,w[],r[]):
|
||
t2 = t * t
|
||
mt = 1-t
|
||
mt2 = mt * mt
|
||
f = [
|
||
r[0] * mt2,
|
||
2 * r[1] * mt * t,
|
||
r[2] * t2
|
||
]
|
||
basis = f[0] + f[1] + f[2]
|
||
return (f[0] * w[0] + f[1] * w[1] + f[2] * w[2])/basis
|
||
|
||
function RationalBezier(3,t,w[],r[]):
|
||
t2 = t * t
|
||
t3 = t2 * t
|
||
mt = 1-t
|
||
mt2 = mt * mt
|
||
mt3 = mt2 * mt
|
||
f = [
|
||
r[0] * mt3,
|
||
3 * r[1] * mt2 * t,
|
||
3 * r[2] * mt * t2,
|
||
r[3] * t3
|
||
]
|
||
basis = f[0] + f[1] + f[2] + f[3]
|
||
return (f[0] * w[0] + f[1] * w[1] + f[2] * w[2] + f[3] * w[3])/basis</textarea
|
||
>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>7</td>
|
||
</tr>
|
||
<tr>
|
||
<td>8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>9</td>
|
||
</tr>
|
||
<tr>
|
||
<td>10</td>
|
||
</tr>
|
||
<tr>
|
||
<td>11</td>
|
||
</tr>
|
||
<tr>
|
||
<td>12</td>
|
||
</tr>
|
||
<tr>
|
||
<td>13</td>
|
||
</tr>
|
||
<tr>
|
||
<td>14</td>
|
||
</tr>
|
||
<tr>
|
||
<td>15</td>
|
||
</tr>
|
||
<tr>
|
||
<td>16</td>
|
||
</tr>
|
||
<tr>
|
||
<td>17</td>
|
||
</tr>
|
||
<tr>
|
||
<td>18</td>
|
||
</tr>
|
||
<tr>
|
||
<td>19</td>
|
||
</tr>
|
||
<tr>
|
||
<td>20</td>
|
||
</tr>
|
||
<tr>
|
||
<td>21</td>
|
||
</tr>
|
||
<tr>
|
||
<td>22</td>
|
||
</tr>
|
||
<tr>
|
||
<td>23</td>
|
||
</tr>
|
||
<tr>
|
||
<td>24</td>
|
||
</tr>
|
||
<tr>
|
||
<td>25</td>
|
||
</tr>
|
||
<tr>
|
||
<td>26</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p>And that's all we have to do.</p>
|
||
</div>
|
||
</section>
|
||
<section id="extended">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#weightcontrol">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#matrix">下</a></div>
|
||
<a href="zh-CN/index.html#extended">贝塞尔区间[0,1]</a>
|
||
</h1>
|
||
<p>
|
||
既然我们知道了贝塞尔曲线背后的数学原理,你可能会注意到一件奇怪的事:它们都是从<code>t=0</code>到<code>t=1</code>。为什么是这个特殊区间?
|
||
</p>
|
||
<p>这一切都与我们如何从曲线的“起点”变化到曲线“终点”有关。如果有一个值是另外两个值的混合,一般方程如下:</p>
|
||
<!--
|
||
|
||
mixture = a · value + b · value
|
||
1 2
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/extended/dfd6ded3f0addcf43e0a1581627a2220.svg" width="212px" height="16px" loading="lazy" />
|
||
<p>
|
||
很显然,起始值需要<code>a=1, b=0</code>,混合值就为100%的value 1和0%的value 2。终点值需要<code>a=0, b=1</code>,则混合值是0%的value
|
||
1和100%的value
|
||
2。另外,我们不想让“a”和“b”是互相独立的:如果它们是互相独立的话,我们可以任意选出自己喜欢的值,并得到混合值,比如说100%的value1和100%的value2。原则上这是可以的,但是对于贝塞尔曲线来说,我们通常想要的是起始值和终点值<em>之间</em>的混合值,所以要确保我们不会设置一些“a”和"b"而导致混合值超过100%。这很简单:
|
||
</p>
|
||
<!--
|
||
|
||
m = a · value + (1 - a) · value
|
||
1 2
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/extended/4e0fa763b173e3a683587acf83733353.svg" width="213px" height="16px" loading="lazy" />
|
||
<p>
|
||
用这个式子我们可以保证相加的值永远不会超过100%。通过将<code>a</code>限制在区间[0,1],我们将会一直处于这两个值之间(包括这两个端点),并且相加为100%。
|
||
</p>
|
||
<p>但是...如果我们没有假定只使用0到1之间的数,而是用一些区间外的值呢,事情会变得很糟糕吗?好吧...不全是,我们接下来看看。</p>
|
||
<p>
|
||
对于贝塞尔曲线的例子,扩展区间只会使我们的曲线“保持延伸”。贝塞尔曲线是多项式曲线上简单的片段,如果我们选一个更大的区间,会看到曲线更多部分。它们看起来是什么样的呢?
|
||
</p>
|
||
<p>
|
||
下面两个图形给你展示了以“普通方式”来渲染的贝塞尔曲线,以及如果我们扩大<code>t</code>值时它们所“位于”的曲线。如你所见,曲线的剩余部分隐藏了很多“形状”,我们可以通过移动曲线的点来建模这部分。
|
||
</p>
|
||
<div class="figure">
|
||
<graphics-element
|
||
title="二次无限区间贝塞尔曲线"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/extended/extended.js"
|
||
data-type="quadratic"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/extended/37948bde4bf0d25bde85f172bf55b9fb.png" loading="lazy" />
|
||
<label>二次无限区间贝塞尔曲线</label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
<graphics-element
|
||
title="三次无限区间贝塞尔曲线"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/extended/extended.js"
|
||
data-type="cubic"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/extended/2d17acb381ebdd28f0ff43be00d723c4.png" loading="lazy" />
|
||
<label>三次无限区间贝塞尔曲线</label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
</div>
|
||
|
||
<p>
|
||
实际上,图形设计和计算机建模中还用了一些和贝塞尔曲线相反的曲线,这些曲线没有固定区间和自由的坐标,相反,它们固定座标但给你自由的区间。<a
|
||
href="https://levien.com/phd/phd.html"
|
||
>"Spiro"曲线</a
|
||
>就是一个很好的例子,它的构造是基于<a href="https://zh.wikipedia.org/wiki/%E7%BE%8A%E8%A7%92%E8%9E%BA%E7%BA%BF">羊角螺线,也就是欧拉螺线</a
|
||
>的一部分。这是在美学上很令人满意的曲线,你可以在一些图形包中看到它,比如<a href="https://fontforge.org/en-US/">FontForge</a>和<a
|
||
href="https://inkscape.org"
|
||
>Inkscape</a
|
||
>,它也被用在一些字体设计中(比如Inconsolata字体)。
|
||
</p>
|
||
</section>
|
||
<section id="matrix">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#extended">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#decasteljau">下</a></div>
|
||
<a href="zh-CN/index.html#matrix">用矩阵运算来表示贝塞尔曲率</a>
|
||
</h1>
|
||
<p>
|
||
通过将贝塞尔公式表示成一个多项式基本方程、系数矩阵以及实际的坐标,我们也可以用矩阵运算来表示贝塞尔。让我们看一下这对三次曲线来说有什么含义:
|
||
</p>
|
||
<!--
|
||
|
||
3 2 2 3
|
||
B(t) = P · (1-t) + P · 3 · (1-t) · t + P · 3 · (1-t) · t + P · t
|
||
1 2 3 4
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/matrix/9a9a55f5b0323d9ea88f82fc6be58ad3.svg" width="468px" height="20px" loading="lazy" />
|
||
<p>暂时不用管我们具体的坐标,现在有:</p>
|
||
<!--
|
||
|
||
3 2 2 3
|
||
B(t) = (1-t) + 3 · (1-t) · t + 3 · (1-t) · t + t
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/matrix/87cfac83cb8a4b0bee68ef006effc611.svg" width="353px" height="19px" loading="lazy" />
|
||
<p>可以将它写成四个表达式之和:</p>
|
||
<!--
|
||
|
||
3
|
||
... = (1-t)
|
||
2
|
||
+ 3 · (1-t) · t
|
||
2
|
||
+ 3 · (1-t) · t
|
||
3
|
||
+ t
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/matrix/cdd88611833f3b178df91278359a4193.svg" width="140px" height="75px" loading="lazy" />
|
||
<p>我们可以扩展这些表达式:</p>
|
||
<!--
|
||
|
||
3 2
|
||
... = (1-t) · (1-t) · (1-t) = -t + 3 · t - 3 · t + 1
|
||
3 2
|
||
+ 3 · (1-t) · (1-t) · t = 3 · t - 6 · t + 3 · t
|
||
3 2
|
||
+ 3 · (1-t) · t · t = -3 · t + 3 · t
|
||
3
|
||
+ t · t · t = t
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/matrix/ec118f296511c6e9ac8727be3703a7ce.svg" width="397px" height="75px" loading="lazy" />
|
||
<p>更进一步,我们可以加上所有的1和0系数,以便看得更清楚:</p>
|
||
<!--
|
||
|
||
3 2
|
||
... = -1 · t + 3 · t - 3 · t + 1
|
||
3 2
|
||
+ +3 · t - 6 · t + 3 · t + 0
|
||
3 2
|
||
+ -3 · t + 3 · t + 0 · t + 0
|
||
3 2
|
||
+ +1 · t + 0 · t + 0 · t + 0
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/matrix/67a5ea33d6c6558f7d954b18226f4956.svg" width="217px" height="75px" loading="lazy" />
|
||
<p><em>现在</em>,我们可以将它看作四个矩阵运算:</p>
|
||
<!--
|
||
|
||
┌ -1 ┐ ┌ 3 ┐ ┌ -3 ┐ ┌ 1 ┐
|
||
┌ 3 2 ┐ · │ 3 │ + ┌ 3 2 ┐ · │ -6 │ + ┌ 3 2 ┐ · │ 3 │ + ┌ 3 2 ┐ · │ 0 │
|
||
└ t t t 1 ┘ │ -3 │ └ t t t 1 ┘ │ 3 │ └ t t t 1 ┘ │ 0 │ └ t t t 1 ┘ │ 0 │
|
||
└ 1 ┘ └ 0 ┘ └ 0 ┘ └ 0 ┘
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/matrix/8ecff6b8a37d60385d287ea2b26876db.svg" width="607px" height="72px" loading="lazy" />
|
||
<p>如果我们将它压缩到一个矩阵操作里,就能得到:</p>
|
||
<!--
|
||
|
||
┌ -1 3 -3 1 ┐
|
||
┌ 3 2 ┐ · │ 3 -6 3 0 │
|
||
└ t t t 1 ┘ │ -3 3 0 0 │
|
||
└ 1 0 0 0 ┘
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/matrix/b9527f7d5a0f5d2d737eac118d69243e.svg" width="227px" height="72px" loading="lazy" />
|
||
<p>这种多项式表达式一般是以递增的顺序来写的,所以我们应该将<code>t</code>矩阵水平翻转,并将大的那个“混合”矩阵上下颠倒:</p>
|
||
<!--
|
||
|
||
┌ 1 0 0 0 ┐
|
||
┌ 2 3 ┐ · │ -3 3 0 0 │
|
||
└ 1 t t t ┘ │ 3 -6 3 0 │
|
||
└ -1 3 -3 1 ┘
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/matrix/1a64ed455c6dd2f8cacca5e5e12bdcc1.svg" width="227px" height="72px" loading="lazy" />
|
||
<p>最终,我们可以加入原始的坐标,作为第三个单独矩阵:</p>
|
||
<!--
|
||
|
||
┌ P ┐
|
||
│ 1 │
|
||
┌ 1 0 0 0 ┐ │ P │
|
||
B(t) = ┌ 2 3 ┐ · │ -3 3 0 0 │ · │ 2 │
|
||
└ 1 t t t ┘ │ 3 -6 3 0 │ │ P │
|
||
└ -1 3 -3 1 ┘ │ 3 │
|
||
│ P │
|
||
└ 4 ┘
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/matrix/b32cae2dfc47d5f36df0bc3defb7dfa8.svg" width="323px" height="73px" loading="lazy" />
|
||
<p>我们可以对二次曲线运用相同的技巧,可以得到:</p>
|
||
<!--
|
||
|
||
┌ P ┐
|
||
┌ 1 0 0 ┐ │ 1 │
|
||
B(t) = ┌ 2 ┐ · │ -2 2 0 │ · │ P │
|
||
└ 1 t t ┘ └ 1 -2 1 ┘ │ 2 │
|
||
│ P │
|
||
└ 3 ┘
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/matrix/1bae50fefa43210b3a6259d1984f6cbc.svg" width="263px" height="55px" loading="lazy" />
|
||
<p>如果我们代入<code>t</code>值并乘以矩阵来计算,得到的值与解原始多项式方程或用逐步线性插值计算的结果一样。</p>
|
||
<p>
|
||
<strong>因此:为什么我们要用矩阵来计算?</strong> 用矩阵形式来表达曲线可以让我们去探索函数的一些很难被发现的性质。可以证明的是曲线构成了<a
|
||
href="https://en.wikipedia.org/wiki/Triangular_matrix"
|
||
>三角矩阵</a
|
||
>,并且它与我们用在曲线中的实际坐标的求积相同。它还是可颠倒的,这说明可以满足<a
|
||
href="https://en.wikipedia.org/wiki/Invertible_matrix#The_invertible_matrix_theorem"
|
||
>大量特性</a
|
||
>。当然,主要问题是:“现在,为什么这些对我们很有用?”,答案就是这些并不是立刻就很有用,但是以后你会看到在一些例子中,曲线的一些属性可以用函数式来计算,也可以巧妙地用矩阵运算来得到,有时候矩阵方法要快得多。
|
||
</p>
|
||
<p>所以,现在只要记着我们可以用这种形式来表示曲线,让我们接着往下看看。</p>
|
||
</section>
|
||
<section id="decasteljau">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#matrix">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#flattening">下</a></div>
|
||
<a href="zh-CN/index.html#decasteljau">de Casteljau's 算法</a>
|
||
</h1>
|
||
<p>
|
||
要绘制贝塞尔曲线,我们可以从<code>0</code>到<code>1</code>遍历<code>t</code>的所有值,计算权重函数,得到需要画的<code>x/y</code>值。但曲线越复杂,计算量也变得越大。我们可以利用“de
|
||
Casteljau算法",这是一种几何画法,并且易于实现。实际上,你可以轻易地用笔和尺画出曲线。
|
||
</p>
|
||
<p>我们用以下步骤来替代用<code>t</code>计算<code>x/y</code>的微积分算法:</p>
|
||
<ul>
|
||
<li>把<code>t</code>看做一个比例(实际上它就是),<code>t=0</code>代表线段的0%,<code>t=1</code>代表线段的100%。</li>
|
||
<li>画出所有点的连线,对<code>n</code>阶曲线来说可以画出<code>n</code>条线。</li>
|
||
<li>在每条线的<code>t</code>处做一个记号。比如<code>t</code>是0.2,就在离起点20%(离终点80%)的地方做个记号。</li>
|
||
<li>连接<code>这些</code>点,得到<code>n-1</code>条线。</li>
|
||
<li>在这些新得到的线上同样用<code>t</code>为比例标记。</li>
|
||
<li>把相邻的<code>那些</code>点连线,得到<code>n-2</code>条线。</li>
|
||
<li>取记号,连线,取记号,等等。</li>
|
||
<li>重复这些步骤,直到剩下一条线。这条线段上的<code>t</code>点就是原始曲线在<code>t</code>处的点。</li>
|
||
</ul>
|
||
<p>
|
||
我们通过实际操作来观察这个过程。在以下的图表中,移动鼠标来改变用de
|
||
Casteljau算法计算得到的曲线点,左右移动鼠标,可以实时看到曲线是如何生成的。
|
||
</p>
|
||
<graphics-element
|
||
title="用de Casteljau算法来遍历曲线"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/decasteljau/decasteljau.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/decasteljau/df92f529841f39decf9ad62b0967855a.png" loading="lazy" />
|
||
<label>用de Casteljau算法来遍历曲线</label>
|
||
</fallback-image>
|
||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control" />
|
||
</graphics-element>
|
||
<div class="howtocode">
|
||
<h3>如何实现de Casteljau算法</h3>
|
||
<p>让我们使用刚才描述过的算法,并实现它:</p>
|
||
|
||
<table class="code">
|
||
<tr>
|
||
<td>1</td>
|
||
<td rowspan="8">
|
||
<textarea disabled rows="8" role="doc-example">
|
||
function drawCurvePoint(points[], t):
|
||
if(points.length==1):
|
||
draw(points[0])
|
||
else:
|
||
newpoints=array(points.size-1)
|
||
for(i=0; i<newpoints.length; i++):
|
||
newpoints[i] = (1-t) * points[i] + t * points[i+1]
|
||
drawCurvePoint(newpoints, t)</textarea
|
||
>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>7</td>
|
||
</tr>
|
||
<tr>
|
||
<td>8</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p>好了,这就是算法的实现。一般来说你不能随意重载“+”操作符,因此我们给出计算<code>x</code>和<code>y</code>坐标的实现:</p>
|
||
|
||
<table class="code">
|
||
<tr>
|
||
<td>1</td>
|
||
<td rowspan="10">
|
||
<textarea disabled rows="10" role="doc-example">
|
||
function drawCurvePoint(points[], t):
|
||
if(points.length==1):
|
||
draw(points[0])
|
||
else:
|
||
newpoints=array(points.size-1)
|
||
for(i=0; i<newpoints.length; i++):
|
||
x = (1-t) * points[i].x + t * points[i+1].x
|
||
y = (1-t) * points[i].y + t * points[i+1].y
|
||
newpoints[i] = new point(x,y)
|
||
drawCurvePoint(newpoints, t)</textarea
|
||
>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>7</td>
|
||
</tr>
|
||
<tr>
|
||
<td>8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>9</td>
|
||
</tr>
|
||
<tr>
|
||
<td>10</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p>
|
||
以上算法做了什么?如果参数points列表只有一个点,
|
||
就画出一个点。如果有多个点,就生成以<i>t</i>为比例的一系列点(例如,以上算法中的"标记点"),然后为新的点列表调用绘制函数。
|
||
</p>
|
||
</div>
|
||
</section>
|
||
<section id="flattening">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#decasteljau">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#splitting">下</a></div>
|
||
<a href="zh-CN/index.html#flattening">简化绘图</a>
|
||
</h1>
|
||
<p>
|
||
我们可以简化绘制的过程,先在具体的位置“采样”曲线,然后用线段把这些点连接起来。由于我们是将曲线转换成一系列“平整的”直线,故将这个过程称之为“拉平(flattening)”。
|
||
</p>
|
||
<p>
|
||
我们可以先确定“想要X个分段”,然后在间隔的地方采样曲线,得到一定数量的分段。这种方法的优点是速度很快:比起遍历100甚至1000个曲线坐标,我们可以采样比较少的点,仍然得到看起来足够好的曲线。这么做的缺点是,我们失去了“真正的曲线”的精度,因此不能用此方法来做真实的相交检测或曲率对齐。
|
||
</p>
|
||
<div class="figure">
|
||
<graphics-element
|
||
title="拉平一条二次曲线"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/flattening/flatten.js"
|
||
data-type="quadratic"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/flattening/6813bfc608aea11df1dda444b9f18123.png" loading="lazy" />
|
||
<label>拉平一条二次曲线</label>
|
||
</fallback-image>
|
||
<input type="range" min="1" max="16" step="1" value="4" class="slide-control" />
|
||
</graphics-element>
|
||
<graphics-element
|
||
title="拉平一条三次曲线"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/flattening/flatten.js"
|
||
data-type="cubic"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/flattening/0e0e4a2ee46bd89bcfde9f75cfe43292.png" loading="lazy" />
|
||
<label>拉平一条三次曲线</label>
|
||
</fallback-image>
|
||
<input type="range" min="1" max="24" step="1" value="8" class="slide-control" />
|
||
</graphics-element>
|
||
</div>
|
||
|
||
<p>
|
||
试着点击图形,并用上下键来降低二次曲线和三次曲线的分段数量。你会发现对某些曲率来说,数量少的分段也能做的很好,但对于复杂的曲率(在三次曲线上试试),足够多的分段才能很好地满足曲率的变化。
|
||
</p>
|
||
<div class="howtocode">
|
||
<h3>如何实现曲线的拉平</h3>
|
||
<p>让我们来实现刚才简述过的算法:</p>
|
||
|
||
<table class="code">
|
||
<tr>
|
||
<td>1</td>
|
||
<td rowspan="7">
|
||
<textarea disabled rows="7" role="doc-example">
|
||
function flattenCurve(curve, segmentCount):
|
||
step = 1/segmentCount;
|
||
coordinates = [curve.getXValue(0), curve.getYValue(0)]
|
||
for(i=1; i <= segmentCount; i++):
|
||
t = i*step;
|
||
coordinates.push[curve.getXValue(t), curve.getYValue(t)]
|
||
return coordinates;</textarea
|
||
>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>7</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p>好了,这就是算法的实现。它基本上是画出一系列的线段来模拟“曲线”。</p>
|
||
|
||
<table class="code">
|
||
<tr>
|
||
<td>1</td>
|
||
<td rowspan="7">
|
||
<textarea disabled rows="7" role="doc-example">
|
||
function drawFlattenedCurve(curve, segmentCount):
|
||
coordinates = flattenCurve(curve, segmentCount)
|
||
coord = coordinates[0], _coord;
|
||
for(i=1; i < coordinates.length; i++):
|
||
_coord = coordinates[i]
|
||
line(coord, _coord)
|
||
coord = _coord</textarea
|
||
>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>7</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p>我们将第一个坐标作为参考点,然后在相邻两个点之间画线。</p>
|
||
</div>
|
||
</section>
|
||
<section id="splitting">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#flattening">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#matrixsplit">下</a></div>
|
||
<a href="zh-CN/index.html#splitting">分割曲线</a>
|
||
</h1>
|
||
<p>
|
||
使用 de Casteljau 算法我们也可以将一条贝塞尔曲线分割成两条更小的曲线,二者拼接起来即可形成原来的曲线。当采用某个 <code>t</code> 值构造 de
|
||
Casteljau 算法时,该过程会给到我们在 <code>t</code> 点分割曲线的所有点:
|
||
一条曲线包含该曲线上点之前的所有点,另一条曲线包含该曲线上点之后的所有点。
|
||
</p>
|
||
<graphics-element
|
||
title="分割一条曲线"
|
||
width="825"
|
||
height="275"
|
||
src="./chapters/splitting/splitting.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="825px" height="275px" src="./images/chapters/splitting/fce5eb16dfcd103797c5e17bd77f1437.png" loading="lazy" />
|
||
<label></label>
|
||
</fallback-image>
|
||
<input type="range" min="0" max="1" step="0.01" value="0.5" class="slide-control" />
|
||
</graphics-element>
|
||
<div class="howtocode">
|
||
<h3>分割曲线的代码实习</h3>
|
||
<p>通过在 de Casteljau 函数里插入一些额外的输出代码,我们就可以实现曲线的分割:</p>
|
||
|
||
<table class="code">
|
||
<tr>
|
||
<td>1</td>
|
||
<td rowspan="16">
|
||
<textarea disabled rows="16" role="doc-example">
|
||
left=[]
|
||
right=[]
|
||
function drawCurvePoint(points[], t):
|
||
if(points.length==1):
|
||
left.add(points[0])
|
||
right.add(points[0])
|
||
draw(points[0])
|
||
else:
|
||
newpoints=array(points.size-1)
|
||
for(i=0; i<newpoints.length; i++):
|
||
if(i==0):
|
||
left.add(points[i])
|
||
if(i==newpoints.length-1):
|
||
right.add(points[i+1])
|
||
newpoints[i] = (1-t) * points[i] + t * points[i+1]
|
||
drawCurvePoint(newpoints, t)</textarea
|
||
>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>7</td>
|
||
</tr>
|
||
<tr>
|
||
<td>8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>9</td>
|
||
</tr>
|
||
<tr>
|
||
<td>10</td>
|
||
</tr>
|
||
<tr>
|
||
<td>11</td>
|
||
</tr>
|
||
<tr>
|
||
<td>12</td>
|
||
</tr>
|
||
<tr>
|
||
<td>13</td>
|
||
</tr>
|
||
<tr>
|
||
<td>14</td>
|
||
</tr>
|
||
<tr>
|
||
<td>15</td>
|
||
</tr>
|
||
<tr>
|
||
<td>16</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p>
|
||
对某个给定 <code>t</code> 值,该函数执行后,数组 <code>left</code> 和 <code>right</code> 将包含两条曲线的所有点的坐标 --
|
||
一条是<code>t</code>值左侧的曲线,一条是<code>t</code>值右侧的曲线, 与原始曲线同序且完全重合。
|
||
</p>
|
||
</div>
|
||
</section>
|
||
<section id="matrixsplit">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#splitting">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#reordering">下</a></div>
|
||
<a href="zh-CN/index.html#matrixsplit">Splitting curves using matrices</a>
|
||
</h1>
|
||
<p>
|
||
Another way to split curves is to exploit the matrix representation of a Bézier curve. In <a href="#matrix">the section on matrices</a>,
|
||
we saw that we can represent curves as matrix multiplications. Specifically, we saw these two forms for the quadratic and cubic curves
|
||
respectively: (we'll reverse the Bézier coefficients vector for legibility)
|
||
</p>
|
||
<!--
|
||
|
||
┌ P ┐
|
||
┌ 1 0 0 ┐ │ 1 │
|
||
B(t) = ┌ 2 ┐ · │ -2 2 0 │ · │ P │
|
||
└ 1 t t ┘ └ 1 -2 1 ┘ │ 2 │
|
||
│ P │
|
||
└ 3 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/matrixsplit/1bae50fefa43210b3a6259d1984f6cbc.svg"
|
||
width="263px"
|
||
height="55px"
|
||
loading="lazy"
|
||
/>
|
||
<p>and</p>
|
||
<!--
|
||
|
||
┌ P ┐
|
||
│ 1 │
|
||
┌ 1 0 0 0 ┐ │ P │
|
||
B(t) = ┌ 2 3 ┐ · │ -3 3 0 0 │ · │ 2 │
|
||
└ 1 t t t ┘ │ 3 -6 3 0 │ │ P │
|
||
└ -1 3 -3 1 ┘ │ 3 │
|
||
│ P │
|
||
└ 4 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/matrixsplit/f690ff0502d9fd7d4697cc43d98afd5d.svg"
|
||
width="323px"
|
||
height="73px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
Let's say we want to split the curve at some point <code>t = z</code>, forming two new (obviously smaller) Bézier curves. To find the
|
||
coordinates for these two Bézier curves, we can use the matrix representation and some linear algebra. First, we separate out the actual
|
||
"point on the curve" information into a new matrix multiplication:
|
||
</p>
|
||
<!--
|
||
|
||
┌ P ┐ ┌ P ┐
|
||
┌ 1 0 0 ┐ │ 1 │ ┌ 1 0 0 ┐ ┌ 1 0 0 ┐ │ 1 │
|
||
B(t) = ┌ 2 ┐ · │ -2 2 0 │ · │ P │ = ┌ 2 ┐ · │ 0 z 0 │ · │ -2 2 0 │ · │ P │
|
||
└ 1 (z · t) (z · t) ┘ └ 1 -2 1 ┘ │ 2 │ └ 1 t t ┘ │ 2 │ └ 1 -2 1 ┘ │ 2 │
|
||
│ P │ └ 0 0 z ┘ │ P │
|
||
└ 3 ┘ └ 3 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/matrixsplit/f565e66677138927335535d009409c3d.svg"
|
||
width="648px"
|
||
height="55px"
|
||
loading="lazy"
|
||
/>
|
||
<p>and</p>
|
||
<!--
|
||
|
||
┌ P ┐ ┌ P ┐
|
||
│ 1 │ ┌ 1 0 0 0 ┐ │ 1 │
|
||
┌ 1 0 0 0 ┐ │ P │ │ 0 z 0 0 │ ┌ 1 0 0 0 ┐ │ P │
|
||
B(t) = ┌ 2 3 ┐ · │ -3 3 0 0 │ · │ 2 │ = ┌ 2 3 ┐ · │ 2 │ · │ -3 3 0 0 │ · │ 2 │
|
||
└ 1 (z · t) (z · t) (z · t) ┘ │ 3 -6 3 0 │ │ P │ └ 1 t t t ┘ │ 0 0 z 0 │ │ 3 -6 3 0 │ │ P │
|
||
└ -1 3 -3 1 ┘ │ 3 │ │ 3 │ └ -1 3 -3 1 ┘ │ 3 │
|
||
│ P │ └ 0 0 0 z ┘ │ P │
|
||
└ 4 ┘ └ 4 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/matrixsplit/ebf8d72c6056476172deeb89726b75c8.svg"
|
||
width="805px"
|
||
height="75px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
If we could compact these matrices back to the form <strong>[t values] · [Bézier matrix] · [column matrix]</strong>, with the first two
|
||
staying the same, then that column matrix on the right would be the coordinates of a new Bézier curve that describes the first segment,
|
||
from <code>t = 0</code> to <code>t = z</code>. As it turns out, we can do this quite easily, by exploiting some simple rules of linear
|
||
algebra (and if you don't care about the derivations, just skip to the end of the box for the results!).
|
||
</p>
|
||
<div class="note">
|
||
<h2>Deriving new hull coordinates</h2>
|
||
<p>
|
||
Deriving the two segments upon splitting a curve takes a few steps, and the higher the curve order, the more work it is, so let's look
|
||
at the quadratic curve first:
|
||
</p>
|
||
<!--
|
||
|
||
┌ P ┐
|
||
┌ 1 0 0 ┐ ┌ 1 0 0 ┐ │ 1 │
|
||
B(t) = ┌ 2 ┐ · │ 0 z 0 │ · │ -2 2 0 │ · │ P │
|
||
└ 1 t t ┘ │ 2 │ └ 1 -2 1 ┘ │ 2 │
|
||
└ 0 0 z ┘ │ P │
|
||
└ 3 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/matrixsplit/c32007be095224e0d157a8f71c62c90e.svg"
|
||
width="348px"
|
||
height="55px"
|
||
loading="lazy"
|
||
/>
|
||
<!--
|
||
|
||
┌ P ┐
|
||
│ 1 │
|
||
= ┌ 2 ┐ · \underset we turn this...\underbrace\kern 2.25em Z · M \kern 2.25em · │ P │
|
||
└ 1 t t ┘ │ 2 │
|
||
│ P │
|
||
└ 3 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/matrixsplit/aa17f7e82cf50498f90deb6a21a2489a.svg"
|
||
width="247px"
|
||
height="55px"
|
||
loading="lazy"
|
||
/>
|
||
<!--
|
||
|
||
┌ P ┐
|
||
-1 │ 1 │
|
||
= ┌ 2 ┐ · \underset into this...\underbrace M · M · Z · M · │ P │
|
||
└ 1 t t ┘ │ 2 │
|
||
│ P │
|
||
└ 3 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/matrixsplit/4549b95450db3c73479e8902e4939427.svg"
|
||
width="247px"
|
||
height="55px"
|
||
loading="lazy"
|
||
/>
|
||
<!--
|
||
|
||
┌ P ┐
|
||
│ 1 │
|
||
= ┌ 2 ┐ · M \underset ...to get this!\underbrace \kern 1.25em · \kern 1.25em Q \kern 1.25em · \kern 1.25em │ P │
|
||
└ 1 t t ┘ │ 2 │
|
||
│ P │
|
||
└ 3 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/matrixsplit/266b71339b55ad3a312a9f41e6bcf988.svg"
|
||
width="252px"
|
||
height="55px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
We can do this because [<em>M · M<sup>-1</sup></em
|
||
>] is the identity matrix. It's a bit like multiplying something by x/x in calculus: it doesn't do anything to the function, but it does
|
||
allow you to rewrite it to something that may be easier to work with, or can be broken up differently. In the same way, multiplying our
|
||
matrix by [<em>M · M<sup>-1</sup></em
|
||
>] has no effect on the total formula, but it does allow us to change the matrix sequence [<em>something · M</em>] to a sequence [<em
|
||
>M · something</em
|
||
>], and that makes a world of difference: if we know what [<em>M<sup>-1</sup> · Z · M</em>] is, we can apply that to our coordinates,
|
||
and be left with a proper matrix representation of a quadratic Bézier curve (which is [<em>T · M · P</em>]), with a new set of
|
||
coordinates that represent the curve from <em>t = 0</em> to <em>t = z</em>. So let's get computing:
|
||
</p>
|
||
<!--
|
||
|
||
┌ 1 0 0 ┐
|
||
-1 │ 1 │ ┌ 1 0 0 ┐ ┌ 1 0 0 ┐ ┌ 1 0 0 ┐
|
||
Q = M · Z · M = │ 1 ─ 0 │ · │ 0 z 0 │ · │ -2 2 0 │ = │ -(z-1) z 0 │
|
||
│ 2 │ │ 2 │ └ 1 -2 1 ┘ │ 2 2 │
|
||
└ 1 1 1 ┘ └ 0 0 z ┘ └ (z - 1) -2 · (z-1) · z z ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/matrixsplit/5e008143622c66bb5e9cc4d5d6a8ea62.svg"
|
||
width="627px"
|
||
height="56px"
|
||
loading="lazy"
|
||
/>
|
||
<p>Excellent! Now we can form our new quadratic curve:</p>
|
||
<!--
|
||
|
||
┌ P ┐ ╭ ┌ P ┐ ╮
|
||
│ 1 │ │ │ 1 │ │
|
||
B(t) = ┌ 2 ┐ · M · Q · │ P │ = ┌ 2 ┐ · M · │ Q · │ P │ │
|
||
└ 1 t t ┘ │ 2 │ └ 1 t t ┘ │ │ 2 │ │
|
||
│ P │ │ │ P │ │
|
||
└ 3 ┘ ╰ └ 3 ┘ ╯
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/matrixsplit/dceed84990aaf6878bcc67ddbaa8d8d9.svg"
|
||
width="417px"
|
||
height="55px"
|
||
loading="lazy"
|
||
/>
|
||
<!--
|
||
|
||
╭ ┌ P ┐ ╮
|
||
┌ 1 0 0 ┐ │ ┌ 1 0 0 ┐ │ 1 │ │
|
||
= ┌ 2 ┐ · │ -2 2 0 │ · │ │ -(z-1) z 0 │ · │ P │ │
|
||
└ 1 t t ┘ └ 1 -2 1 ┘ │ │ 2 2 │ │ 2 │ │
|
||
│ └ (z - 1) -2 · (z-1) · z z ┘ │ P │ │
|
||
╰ └ 3 ┘ ╯
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/matrixsplit/f63067c2c3042c374a58dfa7f692309e.svg"
|
||
width="479px"
|
||
height="55px"
|
||
loading="lazy"
|
||
/>
|
||
<!--
|
||
|
||
┌ P ┐
|
||
│ 1 │
|
||
┌ 1 0 0 ┐ │ z · P - (z-1) · P │
|
||
= ┌ 2 ┐ · │ -2 2 0 │ · │ 2 1 │
|
||
└ 1 t t ┘ └ 1 -2 1 ┘ │ 2 2 │
|
||
│ z · P - 2 · z · (z-1) · P + (z - 1) · P │
|
||
└ 3 2 1 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/matrixsplit/e58196b82b78f584779208cce88137f5.svg"
|
||
width="492px"
|
||
height="55px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
<strong><em>Brilliant</em></strong
|
||
>: if we want a subcurve from <code>t = 0</code> to <code>t = z</code>, we can keep the first coordinate the same (which makes sense),
|
||
our control point becomes a z-ratio mixture of the original control point and the start point, and the new end point is a mixture that
|
||
looks oddly similar to a <a href="https://en.wikipedia.org/wiki/Bernstein_polynomial">Bernstein polynomial</a> of degree two. These new
|
||
coordinates are actually really easy to compute directly!
|
||
</p>
|
||
<p>
|
||
Of course, that's only one of the two curves. Getting the section from <code>t = z</code> to <code>t = 1</code> requires doing this
|
||
again. We first observe that in the previous calculation, we actually evaluated the general interval [0,<code>z</code>]. We were able to
|
||
write it down in a more simple form because of the zero, but what we <em>actually</em> evaluated, making the zero explicit, was:
|
||
</p>
|
||
<!--
|
||
|
||
┌ P ┐
|
||
┌ 1 0 0 ┐ │ 1 │
|
||
B(t) = ┌ 2 ┐ · │ -2 2 0 │ · │ P │
|
||
└ 1 ( 0 + z · t) ( 0 + z · t) ┘ └ 1 -2 1 ┘ │ 2 │
|
||
│ P │
|
||
└ 3 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/matrixsplit/6a22184e6ca869d28f4a252b64f23eff.svg"
|
||
width="381px"
|
||
height="55px"
|
||
loading="lazy"
|
||
/>
|
||
<!--
|
||
|
||
┌ P ┐
|
||
┌ 1 0 0 ┐ ┌ 1 0 0 ┐ │ 1 │
|
||
= ┌ 2 ┐ · │ 0 z 0 │ · │ -2 2 0 │ · │ P │
|
||
└ 1 t t ┘ │ 2 │ └ 1 -2 1 ┘ │ 2 │
|
||
└ 0 0 z ┘ │ P │
|
||
└ 3 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/matrixsplit/3b5e41808b6c3bc66f3da2f40651410e.svg"
|
||
width="313px"
|
||
height="55px"
|
||
loading="lazy"
|
||
/>
|
||
<p>If we want the interval [<em>z</em>,1], we will be evaluating this instead:</p>
|
||
<!--
|
||
|
||
┌ P ┐
|
||
┌ 1 0 0 ┐ │ 1 │
|
||
B(t) = ┌ 2 ┐ · │ -2 2 0 │ · │ P │
|
||
└ 1 ( z + (1-z) · t) ( z + (1-z) · t) ┘ └ 1 -2 1 ┘ │ 2 │
|
||
│ P │
|
||
└ 3 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/matrixsplit/e079f44b56e07c8d7f83c17c8ebf1ecf.svg"
|
||
width="461px"
|
||
height="55px"
|
||
loading="lazy"
|
||
/>
|
||
<!--
|
||
|
||
┌ 2 ┐ ┌ P ┐
|
||
│ 1 z z │ ┌ 1 0 0 ┐ │ 1 │
|
||
= ┌ 2 ┐ · │ 0 1-z 2 · z · (1-z) │ · │ -2 2 0 │ · │ P │
|
||
└ 1 t t ┘ │ 2 │ └ 1 -2 1 ┘ │ 2 │
|
||
└ 0 0 (1-z) ┘ │ P │
|
||
└ 3 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/matrixsplit/4764868f43815e471bb1ea95a81e1633.svg"
|
||
width="412px"
|
||
height="57px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
We're going to do the same trick of multiplying by the identity matrix, to turn <code>[something · M]</code> into
|
||
<code>[M · something]</code>:
|
||
</p>
|
||
<!--
|
||
|
||
┌ 1 0 0 ┐ ┌ 2 ┐
|
||
-1 │ 1 │ │ 1 z z │ ┌ 1 0 0 ┐ ┌ 2 2 ┐
|
||
Q' = M · Z' · M = │ 1 ─ 0 │ · │ 0 1-z 2 · z · (1-z) │ · │ -2 2 0 │ = │ (z-1) -2 · z · (z-1) z │
|
||
│ 2 │ │ 2 │ └ 1 -2 1 ┘ │ 0 -(z-1) z │
|
||
└ 1 1 1 ┘ └ 0 0 (1-z) ┘ └ 0 0 1 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/matrixsplit/c341532f693c2c1adfd298597bbfb5b5.svg"
|
||
width="729px"
|
||
height="57px"
|
||
loading="lazy"
|
||
/>
|
||
<p>So, our final second curve looks like:</p>
|
||
<!--
|
||
|
||
┌ P ┐ ╭ ┌ P ┐ ╮
|
||
│ 1 │ │ │ 1 │ │
|
||
B(t) = ┌ 2 ┐ · M · Q · │ P │ = ┌ 2 ┐ · M · │ Q' · │ P │ │
|
||
└ 1 t t ┘ │ 2 │ └ 1 t t ┘ │ │ 2 │ │
|
||
│ P │ │ │ P │ │
|
||
└ 3 ┘ ╰ └ 3 ┘ ╯
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/matrixsplit/e2622175dadafecc015f15c79ddf3002.svg"
|
||
width="421px"
|
||
height="55px"
|
||
loading="lazy"
|
||
/>
|
||
<!--
|
||
|
||
╭ ┌ P ┐ ╮
|
||
┌ 1 0 0 ┐ │ ┌ 2 2 ┐ │ 1 │ │
|
||
= ┌ 2 ┐ · │ -2 2 0 │ · │ │ (z-1) -2 · z · (z-1) z │ · │ P │ │
|
||
└ 1 t t ┘ └ 1 -2 1 ┘ │ │ 0 -(z-1) z │ │ 2 │ │
|
||
│ └ 0 0 1 ┘ │ P │ │
|
||
╰ └ 3 ┘ ╯
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/matrixsplit/4ce218bc968cbd98da0ca6ab66d415ed.svg"
|
||
width="473px"
|
||
height="57px"
|
||
loading="lazy"
|
||
/>
|
||
<!--
|
||
|
||
┌ 2 2 ┐
|
||
│ z · P - 2 · z · (z-1) · P + (z-1) · P │
|
||
┌ 1 0 0 ┐ │ 3 2 1 │
|
||
= ┌ 2 ┐ · │ -2 2 0 │ · │ z · P - (z-1) · P │
|
||
└ 1 t t ┘ └ 1 -2 1 ┘ │ 3 2 │
|
||
│ P │
|
||
└ 3 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/matrixsplit/9a4899b69e03cd4ad02c5eedffaa6a2f.svg"
|
||
width="492px"
|
||
height="57px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
<strong><em>Nice</em></strong
|
||
>. We see the same thing as before: we can keep the last coordinate the same (which makes sense); our control point becomes a z-ratio
|
||
mixture of the original control point and the end point, and the new start point is a mixture that looks oddly similar to a bernstein
|
||
polynomial of degree two, except this time it uses (z-1) rather than (1-z). These new coordinates are <em>also</em> really easy to
|
||
compute directly!
|
||
</p>
|
||
</div>
|
||
|
||
<p>
|
||
So, using linear algebra rather than de Casteljau's algorithm, we have determined that, for any quadratic curve split at some value
|
||
<code>t = z</code>, we get two subcurves that are described as Bézier curves with simple-to-derive coordinates:
|
||
</p>
|
||
<!--
|
||
|
||
┌ P ┐
|
||
┌ P ┐ │ 1 │
|
||
┌ 1 0 0 ┐ │ 1 │ │ z · P - (z-1) · P │
|
||
│ -(z-1) z 0 │ · │ P │ = │ 2 1 │
|
||
│ 2 2 │ │ 2 │ │ 2 2 │
|
||
└ (z - 1) -2 · (z-1) · z z ┘ │ P │ │ z · P - 2 · z · (z-1) · P + (z - 1) · P │
|
||
└ 3 ┘ └ 3 2 1 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/matrixsplit/480ebd0234e2fe1adc94926e8ed4339c.svg"
|
||
width="576px"
|
||
height="55px"
|
||
loading="lazy"
|
||
/>
|
||
<p>and</p>
|
||
<!--
|
||
|
||
┌ 2 2 ┐
|
||
┌ P ┐ │ z · P - 2 · z · (z-1) · P + (z-1) · P │
|
||
┌ 2 2 ┐ │ 1 │ │ 3 2 1 │
|
||
│ (z-1) -2 · z · (z-1) z │ · │ P │ = │ z · P - (z-1) · P │
|
||
│ 0 -(z-1) z │ │ 2 │ │ 3 2 │
|
||
└ 0 0 1 ┘ │ P │ │ P │
|
||
└ 3 ┘ └ 3 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/matrixsplit/17e308aa6d459b1d06d3160cc8e2e786.svg"
|
||
width="571px"
|
||
height="57px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
We can do the same for cubic curves. However, I'll spare you the actual derivation (don't let that stop you from writing that out
|
||
yourself, though) and simply show you the resulting new coordinate sets:
|
||
</p>
|
||
<!--
|
||
|
||
┌ P ┐
|
||
┌ P ┐ │ 1 │
|
||
┌ 1 0 0 0 ┐ │ 1 │ │ z · P - (z-1) · P │
|
||
│ -(z-1) z 0 0 │ │ P │ │ 2 1 │
|
||
│ 2 2 │ · │ 2 │ = │ 2 2 │
|
||
│ (z-1) -2 · (z-1) · z z 0 │ │ P │ │ z · P - 2 · z · (z-1) · P + (z-1) · P │
|
||
│ 3 2 2 3 │ │ 3 │ │ 3 2 1 │
|
||
└ -(z-1) 3 · (z-1) · z -3 · (z-1) · z z ┘ │ P │ │ 3 2 2 3 │
|
||
└ 4 ┘ │ z · P - 3 · z · (z-1) · P + 3 · z · (z-1) · P - (z-1) · P │
|
||
└ 4 3 2 1 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/matrixsplit/11505e0215ef026f2e49383ebb4a1abb.svg"
|
||
width="841px"
|
||
height="75px"
|
||
loading="lazy"
|
||
/>
|
||
<p>and</p>
|
||
<!--
|
||
|
||
┌ 3 2 2 3 ┐
|
||
┌ P ┐ │ z · P - 3 · z · (z-1) · P + 3 · z · (z-1) · P - (z-1) · P │
|
||
┌ 3 2 2 3 ┐ │ 1 │ │ 4 3 2 1 │
|
||
│ -(z-1) 3 · (z-1) · z -3 · (z-1) · z z │ │ P │ │ 2 2 │
|
||
│ 2 2 │ · │ 2 │ = │ z · P - 2 · z · (z-1) · P + (z-1) · P │
|
||
│ 0 (z-1) -2 · (z-1) · z z │ │ P │ │ 4 3 2 │
|
||
│ 0 0 -(z-1) z │ │ 3 │ │ z · P - (z-1) · P │
|
||
└ 0 0 0 1 ┘ │ P │ │ 4 3 │
|
||
└ 4 ┘ │ P │
|
||
└ 4 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/matrixsplit/a899891096d82b7fdb23a90e6106b6df.svg"
|
||
width="837px"
|
||
height="77px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
So, looking at our matrices, did we really need to compute the second segment matrix? No, we didn't. Actually having one segment's matrix
|
||
means we implicitly have the other: push the values of each row in the matrix <strong><em>Q</em></strong> to the right, with zeroes
|
||
getting pushed off the right edge and appearing back on the left, and then flip the matrix vertically. Presto, you just "calculated"
|
||
<strong><em>Q'</em></strong
|
||
>.
|
||
</p>
|
||
<p>
|
||
Implementing curve splitting this way requires less recursion, and is just straight arithmetic with cached values, so can be cheaper on
|
||
systems where recursion is expensive. If you're doing computation with devices that are good at matrix multiplication, chopping up a
|
||
Bézier curve with this method will be a lot faster than applying de Casteljau.
|
||
</p>
|
||
</section>
|
||
<section id="reordering">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#matrixsplit">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#derivatives">下</a></div>
|
||
<a href="zh-CN/index.html#reordering">Lowering and elevating curve order</a>
|
||
</h1>
|
||
<p>
|
||
One interesting property of Bézier curves is that an <em>n<sup>th</sup></em> order curve can always be perfectly represented by an
|
||
<em>(n+1)<sup>th</sup></em> order curve, by giving the higher-order curve specific control points.
|
||
</p>
|
||
<p>
|
||
If we have a curve with three points, then we can create a curve with four points that exactly reproduces the original curve. First, we
|
||
give it the same start and end points, and for its two control points we pick "1/3<sup>rd</sup> start + 2/3<sup>rd</sup> control" and
|
||
"2/3<sup>rd</sup> control + 1/3<sup>rd</sup> end". Now we have exactly the same curve as before, except represented as a cubic curve
|
||
rather than a quadratic curve.
|
||
</p>
|
||
<p>
|
||
The general rule for raising an <em>n<sup>th</sup></em> order curve to an <em>(n+1)<sup>th</sup></em> order curve is as follows (observing
|
||
that the start and end weights are the same as the start and end weights for the old curve):
|
||
</p>
|
||
<!--
|
||
|
||
__ k k-i i
|
||
Bézier(k,t) = ❯ \underset binomial term\underbrace\binomki · \ \underset polynomial term\underbrace(1-t) · t · \ \underset new
|
||
‾‾ i=0
|
||
╭ (k-i) · w + i · w ╮
|
||
│ i i-1 │
|
||
weights\underbrace│ ─────────────────────── │ , with k = n+1 and w =0 when i = 0
|
||
╰ k ╯ i-1
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/reordering/74e038deabd9e240606fa3f07ba98269.svg" width="755px" height="61px" loading="lazy" />
|
||
<p>
|
||
However, this rule also has as direct consequence that you <strong>cannot</strong> generally safely lower a curve from
|
||
<em>n<sup>th</sup></em> order to <em>(n-1)<sup>th</sup></em> order, because the control points cannot be "pulled apart" cleanly. We can
|
||
try to, but the resulting curve will not be identical to the original, and may in fact look completely different.
|
||
</p>
|
||
<p>
|
||
However, there is a surprisingly good way to ensure that a lower order curve looks "as close as reasonably possible" to the original
|
||
curve: we can optimise the "least-squares distance" between the original curve and the lower order curve, in a single operation (also
|
||
explained over on <a href="https://www.sirver.net/blog/2011/08/23/degree-reduction-of-bezier-curves/">Sirver's Castle</a>). However, to
|
||
use it, we'll need to do some calculus work and then switch over to linear algebra. As mentioned in the section on matrix representations,
|
||
some things can be done much more easily with matrices than with calculus functions, and this is one of those things. So... let's go!
|
||
</p>
|
||
<p>We start by taking the standard Bézier function, and condensing it a little:</p>
|
||
<!--
|
||
|
||
__ n n n n-i i
|
||
Bézier(n,t) = ❯ w B (t) , where B (t) = \binomni · (1-t) · t
|
||
‾‾ i=0 i i i
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/reordering/9fc4ecc087d389dd0111bcba165cd5d0.svg" width="408px" height="41px" loading="lazy" />
|
||
<p>
|
||
Then, we apply one of those silly (actually, super useful) calculus tricks: since our <code>t</code> value is always between zero and one
|
||
(inclusive), we know that <code>(1-t)</code> plus <code>t</code> always sums to 1. As such, we can express any value as a sum of
|
||
<code>t</code> and <code>1-t</code>:
|
||
</p>
|
||
<!--
|
||
|
||
x = 1 x = ((1-t) + t) x = (1-t) x + t x = x (1-t) + x t
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/reordering/ff224ded6bbbc94b43130f5f8eeb5d29.svg" width="379px" height="16px" loading="lazy" />
|
||
<p>
|
||
So, with that seemingly trivial observation, we rewrite that Bézier function by splitting it up into a sum of a <code>(1-t)</code> and
|
||
<code>t</code> component:
|
||
</p>
|
||
<!--
|
||
|
||
Bézier(n,t)= (1-t) B(n,t) + t B(n,t)
|
||
__ n n __ n n
|
||
= ❯ w (1 - t) B (t) + ❯ w t B (t)
|
||
‾‾ i=0 i i ‾‾ i=0 i i
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/reordering/0cf0d5f856ec204dc32e0e42691cc70a.svg" width="316px" height="67px" loading="lazy" />
|
||
<p>
|
||
So far so good. Now, to see why we did this, let's write out the <code>(1-t)</code> and <code>t</code> parts, and see what that gives us.
|
||
I promise, it's about to make sense. We start with <code>(1-t)</code>:
|
||
</p>
|
||
<!--
|
||
|
||
n n! n-i i
|
||
(1 - t) B (t)= (1-t) ──────── (1-t) t
|
||
i (n-i)!i!
|
||
n+1-i (n+1)! n+1-i i
|
||
= ───── ────────── (1-t) t
|
||
n+1 (n+1-i)!i!
|
||
k-i k! k-i i
|
||
= ─── ──────── (1-t) t , where k = n + 1
|
||
k (k-i)!i!
|
||
k-i k
|
||
= ─── B (t)
|
||
k i
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/reordering/0f5698b31598b2390e966fc5e43ab53e.svg"
|
||
width="387px"
|
||
height="160px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
So by using this seemingly silly trick, we can suddenly express part of our n<sup>th</sup> order Bézier function in terms of an (n+1)<sup
|
||
>th</sup
|
||
>
|
||
order Bézier function. And that sounds a lot like raising the curve order! Of course we need to be able to repeat that trick for the
|
||
<code>t</code> part, but that's not a problem:
|
||
</p>
|
||
<!--
|
||
|
||
n n! n-i i
|
||
t B (t)= t ──────── (1-t) t
|
||
i (n-i)!i!
|
||
i+1 (n+1)! (n+1)-(i+1) i+1
|
||
= ─── ──────────────────── (1-t) t
|
||
n+1 ((n+1)-(i+1))!(i+1)!
|
||
i+1 k! k-(i+1) i+1
|
||
= ─── ──────────────── (1-t) t , where k = n + 1
|
||
k (k-(i+1))!(i+1)!
|
||
i+1 k
|
||
= ─── B (t)
|
||
k i+1
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/reordering/ab7c087f7c070d43a42f3f03010a7427.svg"
|
||
width="471px"
|
||
height="159px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
So, with both of those changed from an order <code>n</code> expression to an order <code>(n+1)</code> expression, we can put them back
|
||
together again. Now, where the order <code>n</code> function had a summation from 0 to <code>n</code>, the order <code>n+1</code> function
|
||
uses a summation from 0 to <code>n+1</code>, but this shouldn't be a problem as long as we can add some new terms that "contribute
|
||
nothing". In the next section on derivatives, there is a discussion about why "higher terms than there is a binomial for" and "lower than
|
||
zero terms" both "contribute nothing". So as long as we can add terms that have the same form as the terms we need, we can just include
|
||
them in the summation, they'll sit there and do nothing, and the resulting function stays identical to the lower order curve.
|
||
</p>
|
||
<p>Let's do this:</p>
|
||
<!--
|
||
|
||
__ n+1 n __ n+1 n
|
||
Bézier(n,t)= ❯ w (1 - t) B (t) + ❯ w t B (t)
|
||
‾‾ i=0 i i ‾‾ i=0 i i
|
||
__ n+1 k-i k __ n+1 i+1 k
|
||
= ❯ w ─── B (t) + ❯ w ─── B (t), where k = n + 1
|
||
‾‾ i=0 i k i ‾‾ i=0 i k i+1
|
||
__ n+1 k-i k __ n+1 i k
|
||
= ❯ w ─── B (t) + ❯ p ─ B (t)
|
||
‾‾ i=0 i k i ‾‾ i=0 i-1 k i
|
||
__ n+1 ╭ k-i i ╮ k
|
||
= ❯ │ w ─── + p ─ │ B (t)
|
||
‾‾ i=0 ╰ i k i-1 k ╯ i
|
||
__ n+1 k i
|
||
= ❯ (w (1-s) + p s) B (t), where s = ─
|
||
‾‾ i=0 i i-1 i k
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/reordering/4ff41e183d60d5fd10a5d3d30dd63358.svg"
|
||
width="465px"
|
||
height="257px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
And this is where we switch over from calculus to linear algebra, and matrices: we can now express this relation between Bézier(n,t) and
|
||
Bézier(n+1,t) as a very simple matrix multiplication:
|
||
</p>
|
||
<!--
|
||
|
||
M B = B
|
||
n k
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/reordering/1f5b60d190a1c7099b3411e4cc477291.svg" width="71px" height="16px" loading="lazy" />
|
||
<p>where the matrix <strong>M</strong> is an <code>n+1</code> by <code>n</code> matrix, and looks like:</p>
|
||
<!--
|
||
|
||
┌ 1 0 . . . . . . ┐
|
||
│ 1 k-1 │
|
||
│ ─ ─── 0 . . . 0 . │
|
||
│ k k │
|
||
│ 2 k-2 │
|
||
│ 0 ─ ─── 0 . . . . │
|
||
│ k k │
|
||
│ 3 k-3 │
|
||
│ . 0 ─ ─── 0 . . . │
|
||
M = │ k k │
|
||
│ . . 0 ... ... 0 . . │
|
||
│ . . . 0 ... ... 0 . │
|
||
│ n-1 k-n+1 │
|
||
│ . . . . 0 ─── ───── 0 │
|
||
│ k k │
|
||
│ n k-n │
|
||
│ . 0 . . . 0 ─ ─── │
|
||
│ k k │
|
||
└ . . . . . . 0 1 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/reordering/056e25c397c524d80f378ce3823c7e78.svg"
|
||
width="336px"
|
||
height="187px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
That might look unwieldy, but it's really just a mostly-zeroes matrix, with a very simply fraction on the diagonal, and an even simpler
|
||
fraction to the left of it. Multiplying a list of coordinates with this matrix means we can plug the resulting transformed coordinates
|
||
into the one-order-higher function and get an identical looking curve.
|
||
</p>
|
||
<p>Not too bad!</p>
|
||
<p>
|
||
Equally interesting, though, is that with this matrix operation established, we can now use an incredibly powerful and ridiculously simple
|
||
way to find out a "best fit" way to reverse the operation, called
|
||
<a href="https://mathworld.wolfram.com/NormalEquation.html">the normal equation</a>. What it does is minimize the sum of the square
|
||
differences between one set of values and another set of values. Specifically, if we can express that as some function
|
||
<strong>A x = b</strong>, we can use it. And as it so happens, that's exactly what we're dealing with, so:
|
||
</p>
|
||
<!--
|
||
|
||
M B = B
|
||
n k
|
||
T T
|
||
(M M) B = M B
|
||
n k
|
||
T -1 T T -1 T
|
||
(M M) (M M) B = (M M) M B
|
||
n k
|
||
T -1 T
|
||
I B = (M M) M B
|
||
n k
|
||
T -1 T
|
||
B = (M M) M B
|
||
n k
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/reordering/46e64dc07502e14217ec83d755f736ee.svg"
|
||
width="272px"
|
||
height="116px"
|
||
loading="lazy"
|
||
/>
|
||
<p>The steps taken here are:</p>
|
||
<ol>
|
||
<li>We have a function in a form that the normal equation can be used with, so</li>
|
||
<li>apply the normal equation!</li>
|
||
<li>
|
||
Then, we want to end up with just B<sub>n</sub> on the left, so we start by left-multiply both sides such that we'll end up with lots of
|
||
stuff on the left that simplified to "a factor 1", which in matrix maths is the
|
||
<a href="https://en.wikipedia.org/wiki/Identity_matrix">identity matrix</a>.
|
||
</li>
|
||
<li>
|
||
In fact, by left-multiplying with the inverse of what was already there, we've effectively "nullified" (but really, one-inified) that
|
||
big, unwieldy block into the identity matrix <strong>I</strong>. So we substitute the mess with <strong>I</strong>, and then
|
||
</li>
|
||
<li>
|
||
because multiplication with the identity matrix does nothing (like multiplying by 1 does nothing in regular algebra), we just drop it.
|
||
</li>
|
||
</ol>
|
||
<p>
|
||
And we're done: we now have an expression that lets us approximate an <code>n+1</code><sup>th</sup> order curve with a lower <code>n</code
|
||
><sup>th</sup> order curve. It won't be an exact fit, but it's definitely a best approximation. So, let's implement these rules for
|
||
raising and lowering curve order to a (semi) random curve, using the following graphic. Select the sketch, which has movable control
|
||
points, and press your up and down arrow keys to raise or lower the curve order.
|
||
</p>
|
||
<graphics-element
|
||
title="A variable-order Bézier curve"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/reordering/reorder.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/reordering/c4874e1205aabe624e5504abe154eae9.png" loading="lazy" />
|
||
<label>A variable-order Bézier curve</label>
|
||
</fallback-image>
|
||
<button class="raise">raise</button>
|
||
<button class="lower">lower</button>
|
||
</graphics-element>
|
||
</section>
|
||
<section id="derivatives">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#reordering">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#pointvectors">下</a></div>
|
||
<a href="zh-CN/index.html#derivatives">Derivatives</a>
|
||
</h1>
|
||
<p>
|
||
There's a number of useful things that you can do with Bézier curves based on their derivative, and one of the more amusing observations
|
||
about Bézier curves is that their derivatives are, in fact, also Bézier curves. In fact, the differentiation of a Bézier curve is
|
||
relatively straightforward, although we do need a bit of math.
|
||
</p>
|
||
<p>First, let's look at the derivative rule for Bézier curves, which is:</p>
|
||
<!--
|
||
|
||
__ n-1
|
||
Bézier'(n,t) = n · ❯ (b -b ) · Bézier(n-1,t)
|
||
‾‾ i=0 i+1 i i
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/derivatives/a7b79877822a8f60e45552dcafc0815d.svg"
|
||
width="333px"
|
||
height="44px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
which we can also write (observing that <i>b</i> in this formula is the same as our <i>w</i> weights, and that <i>n</i> times a summation
|
||
is the same as a summation where each term is multiplied by <i>n</i>) as:
|
||
</p>
|
||
<!--
|
||
|
||
__ n-1
|
||
Bézier'(n,t) = ❯ Bézier(n-1,t) · n · (w -w )
|
||
‾‾ i=0 i i+1 i
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/derivatives/8f78fdb9ef54b1bc4dbc00f07263cc97.svg"
|
||
width="343px"
|
||
height="44px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
Or, in plain text: the derivative of an n<sup>th</sup> degree Bézier curve is an (n-1)<sup>th</sup> degree Bézier curve, with one fewer
|
||
term, and new weights w'<sub>0</sub>...w'<sub>n-1</sub> derived from the original weights as n(w<sub>i+1</sub> - w<sub>i</sub>). So for a
|
||
3<sup>rd</sup> degree curve, with four weights, the derivative has three new weights: w'<sub>0</sub> = 3(w<sub>1</sub>-w<sub>0</sub>),
|
||
w'<sub>1</sub> = 3(w<sub>2</sub>-w<sub>1</sub>) and w'<sub>2</sub> = 3(w<sub>3</sub>-w<sub>2</sub>).
|
||
</p>
|
||
<div class="note">
|
||
<h3>"Slow down, why is that true?"</h3>
|
||
<p>
|
||
Sometimes just being told "this is the derivative" is nice, but you might want to see why this is indeed the case. As such, let's have a
|
||
look at the proof for this derivative. First off, the weights are independent of the full Bézier function, so the derivative involves
|
||
only the derivative of the polynomial basis function. So, let's find that:
|
||
</p>
|
||
<!--
|
||
|
||
d ╭ n ╮ k n-k d
|
||
B (t) ── = │ │ t (1-t) ──
|
||
n,k dt ╰ k ╯ dt
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/derivatives/a992185a346518b5ca159484019b6917.svg"
|
||
width="209px"
|
||
height="36px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
Applying the <a href="https://en.wikipedia.org/wiki/Product_rule">product</a> and
|
||
<a href="https://en.wikipedia.org/wiki/Chain_rule">chain</a> rules gives us:
|
||
</p>
|
||
<!--
|
||
|
||
╭ n ╮ k-1 n-k k n-k-1
|
||
... = │ │ (k · t (1-t) + t · (1-t) · (n-k) · -1)
|
||
╰ k ╯
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/derivatives/c3ac18fe4ba0606a15bc111e52b17a9a.svg"
|
||
width="412px"
|
||
height="28px"
|
||
loading="lazy"
|
||
/>
|
||
<p>Which is hard to work with, so let's expand that properly:</p>
|
||
<!--
|
||
|
||
kn! k-1 n-k (n-k)n! k n-1-k
|
||
... = ──────── t (1-t) - ──────── t (1-t)
|
||
k!(n-k)! k!(n-k)!
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/derivatives/12fa7f83f055ef2078cc9f04e1468663.svg"
|
||
width="344px"
|
||
height="27px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
Now, the trick is to turn this expression into something that has binomial coefficients again, so we want to end up with things that
|
||
look like "x! over y!(x-y)!". If we can do that in a way that involves terms of <i>n-1</i> and <i>k-1</i>, we'll be on the right track.
|
||
</p>
|
||
<!--
|
||
|
||
n! k-1 n-k (n-k)n! k n-1-k
|
||
... = ──────────── t (1-t) - ──────── t (1-t)
|
||
(k-1)!(n-k)! k!(n-k)!
|
||
╭ (n-1)! k-1 n-k (n-k)(n-1)! k n-1-k ╮
|
||
... = n │ ──────────── t (1-t) - ─────────── t (1-t) │
|
||
╰ (k-1)!(n-k)! k!(n-k)! ╯
|
||
╭ (n-1)! (k-1) (n-1)-(k-1) (n-1)! k (n-1)-k ╮
|
||
... = n │ ──────────────────── t (1-t) - ──────────── t (1-t) │
|
||
╰ (k-1)!((n-1)-(k-1))! k!((n-1)-k)! ╯
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/derivatives/64c06c61727d0912a67c0f287a395e47.svg"
|
||
width="545px"
|
||
height="76px"
|
||
loading="lazy"
|
||
/>
|
||
<p>And that's the first part done: the two components inside the parentheses are actually regular, lower-order Bézier expressions:</p>
|
||
<!--
|
||
|
||
╭ x! y x-y x! k x-k ╮
|
||
... = n │ ──────── t (1-t) - ──────── t (1-t) │ , with x=n-1, y=k-1
|
||
╰ y!(x-y)! k!(x-k)! ╯
|
||
... = n (B (t) - B (t))
|
||
(n-1),(k-1) (n-1),k
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/derivatives/6a3672344bb571eadb72669f60a93ff4.svg"
|
||
width="533px"
|
||
height="48px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
Now to apply this to our weighted Bézier curves. We'll write out the plain curve formula that we saw earlier, and then work our way
|
||
through to its derivative:
|
||
</p>
|
||
<!--
|
||
|
||
Bézier (t) = B (t) · w + B (t) · w + B (t) · w + B (t) · w + ...
|
||
n,k n,0 0 n,1 1 n,2 2 n,3 3
|
||
d
|
||
Bézier (t) ── = n · (B (t) - B (t)) · w +
|
||
n,k dt n-1,-1 n-1,0 0
|
||
n · (B (t) - B (t)) · w +
|
||
n-1,0 n-1,1 1
|
||
n · (B (t) - B (t)) · w +
|
||
n-1,1 n-1,2 2
|
||
n · (B (t) - B (t)) · w +
|
||
n-1,2 n-1,3 3
|
||
...
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/derivatives/02cecadc92b8ff681edc8edb0ace53ce.svg"
|
||
width="527px"
|
||
height="112px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
If we expand this (with some color to show how terms line up), and reorder the terms by increasing values for <i>k</i> we see the
|
||
following:
|
||
</p>
|
||
<!--
|
||
|
||
n · B (t) · w +
|
||
n-1,-1 0
|
||
n · B (t) · w - n · B (t) · w +
|
||
n-1,\colorblue 0 1 n-1,\colorblue 0 0
|
||
n · B (t) · w - n · B (t) · w +
|
||
n-1,\colorred 1 2 n-1,\colorred 1 1
|
||
n · B (t) · w - n · B (t) · w +
|
||
n-1,\colormagenta 2 3 n-1,\colormagenta 2 2
|
||
... - n · B (t) · w +
|
||
n-1,3 3
|
||
...
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/derivatives/87403e5b0da3dcc4ceca74fae058fe69.svg"
|
||
width="300px"
|
||
height="109px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
Two of these terms fall way: the first term falls away because there is no -1<sup>st</sup> term in a summation. As such, it always
|
||
contributes "nothing", so we can safely completely ignore it for the purpose of finding the derivative function. The other term is the
|
||
very last term in this expansion: one involving <i>B<sub>n-1,n</sub></i
|
||
>. This term would have a binomial coefficient of [<i>i</i> choose <i>i+1</i>], which is a non-existent binomial coefficient. Again,
|
||
this term would contribute "nothing", so we can ignore it, too. This means we're left with:
|
||
</p>
|
||
<!--
|
||
|
||
n · B (t) · w - n · B (t) · w +
|
||
n-1,\colorblue 0 1 n-1,\colorblue 0 0
|
||
n · B (t) · w - n · B (t) · w +
|
||
n-1,\colorred 1 2 n-1,\colorred 1 1
|
||
n · B (t) · w - n · B (t) · w +
|
||
n-1,\colormagenta 2 3 n-1,\colormagenta 2 2
|
||
...
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/derivatives/f00423aaceac6ade478aba2a761664d8.svg"
|
||
width="295px"
|
||
height="71px"
|
||
loading="lazy"
|
||
/>
|
||
<p>And that's just a summation of lower order curves:</p>
|
||
<!--
|
||
|
||
d
|
||
Bézier (t) ── = n · B (t) · (w - w ) + n · B (t) · (w - w ) + n · B (t) · (w - w )
|
||
n,k dt (n-1),\colorblue 0 1 0 (n-1),\colorred 1 2 1 (n-1),\colormagenta 2 3 2
|
||
+ ...
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/derivatives/de486728b41299269cc990ed09d5d286.svg"
|
||
width="716px"
|
||
height="36px"
|
||
loading="lazy"
|
||
/>
|
||
<p>We can rewrite this as a normal summation, and we're done:</p>
|
||
<!--
|
||
|
||
d __ n-1 __ n-1
|
||
Bézier (t) ── = ❯ n · B (t) · (w - w ) = ❯ B (t) · \underset derivative weights \underbracen · (w - w )
|
||
n,k dt ‾‾ k=0 n-1,k k+1 k ‾‾ k=0 n-1,k k+1 k
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/derivatives/4eeb75f5de2d13a39f894625d3222443.svg"
|
||
width="545px"
|
||
height="51px"
|
||
loading="lazy"
|
||
/>
|
||
</div>
|
||
|
||
<p>
|
||
Let's rewrite that in a form similar to our original formula, so we can see the difference. We will first list our original formula for
|
||
Bézier curves, and then the derivative:
|
||
</p>
|
||
<!--
|
||
|
||
__ n n-i i
|
||
Bézier(n,t) = ❯ \underset binomial term\underbrace\binomni · \ \underset polynomial term\underbrace(1-t) · t · \ \underset
|
||
‾‾ i=0
|
||
weight\underbracew
|
||
i
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/derivatives/153d99ce571bd664945394a1203a9eba.svg"
|
||
width="336px"
|
||
height="55px"
|
||
loading="lazy"
|
||
/>
|
||
<!--
|
||
|
||
__ k k-i i
|
||
Bézier'(n,t) = ❯ \underset binomial term\underbrace\binomki · \ \underset polynomial term\underbrace(1-t) · t · \ \underset derivative
|
||
‾‾ i=0
|
||
weight\underbracen · (w - w ) , with k=n-1
|
||
i+1 i
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/derivatives/2368534c6e964e6d4a54904cc99b8986.svg"
|
||
width="520px"
|
||
height="59px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
What are the differences? In terms of the actual Bézier curve, virtually nothing! We lowered the order (rather than <i>n</i>, it's now
|
||
<i>n-1</i>), but it's still the same Bézier function. The only real difference is in how the weights change when we derive the curve's
|
||
function. If we have four points A, B, C, and D, then the derivative will have three points, the second derivative two, and the third
|
||
derivative one:
|
||
</p>
|
||
<!--
|
||
|
||
B(n,t), w = {A,B,C,D}
|
||
B'(n,t), n = 3, w' = {A',B',C'} = {3 · (B-A), 3 · (C-B), 3 · (D-C)}
|
||
B''(n,t), n = 2, w'' = {A'',B''} = {2 · (B'-A'), 2 · (C'-B')}
|
||
B'''(n,t), n = 1, w''' = {A'''} = {1 · (B''-A'')}
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/derivatives/2d733684f81b65a42c4cdb3f1e589c8b.svg"
|
||
width="523px"
|
||
height="73px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
We can keep performing this trick for as long as we have more than one weight. Once we have one weight left, the next step will see
|
||
<i>k = 0</i>, and the result of our "Bézier function" summation is zero, because we're not adding anything at all. As such, a quadratic
|
||
curve has no second derivative, a cubic curve has no third derivative, and generalized: an <i>n<sup>th</sup></i> order curve has
|
||
<i>n-1</i> (meaningful) derivatives, with any further derivative being zero.
|
||
</p>
|
||
</section>
|
||
<section id="pointvectors">
|
||
<h1>
|
||
<div class="nav">
|
||
<a href="zh-CN/index.html#derivatives">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#pointvectors3d">下</a>
|
||
</div>
|
||
<a href="zh-CN/index.html#pointvectors">Tangents and normals</a>
|
||
</h1>
|
||
<p>
|
||
If you want to move objects along a curve, or "away from" a curve, the two vectors you're most interested in are the tangent vector and
|
||
normal vector for curve points. These are actually really easy to find. For moving and orienting along a curve, we use the tangent, which
|
||
indicates the direction of travel at specific points, and is literally just the first derivative of our curve:
|
||
</p>
|
||
<!--
|
||
|
||
tangent (t) = B' (t)
|
||
x x
|
||
|
||
tangent (t) = B' (t)
|
||
y y
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/pointvectors/da069c7d6cbdb516c5454371dae84e7f.svg"
|
||
width="132px"
|
||
height="61px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
This gives us the directional vector we want. We can normalize it to give us uniform directional vectors (having a length of 1.0) at each
|
||
point, and then do whatever it is we want to do based on those directions:
|
||
</p>
|
||
<!--
|
||
|
||
┌─────────────────┐
|
||
│ 2 2
|
||
d = \| tangent(t)\| = │B' (t) + B' (t)
|
||
⟍│ x y
|
||
|
||
tangent (t) B' (t)
|
||
^ x x
|
||
x(t) = \| tangent (t)\| =─────────────── = ──────
|
||
x \| tangent(t)\| d
|
||
|
||
tangent (t) B' (t)
|
||
^ y y
|
||
y(t) = \| tangent (t)\| = ─────────────── = ──────
|
||
y \| tangent(t)\| d
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/pointvectors/33afd1a141ec444989c393b3e51ec9ca.svg"
|
||
width="273px"
|
||
height="121px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
The tangent is very useful for moving along a line, but what if we want to move away from the curve instead, perpendicular to the curve at
|
||
some point <i>t</i>? In that case we want the <em>normal</em> vector. This vector runs at a right angle to the direction of the curve, and
|
||
is typically of length 1.0, so all we have to do is rotate the normalized directional vector and we're done:
|
||
</p>
|
||
<!--
|
||
|
||
^ \pi ^ \pi ^
|
||
normal (t) = x(t) · cos ─── - y(t) · sin ─── = - y(t)
|
||
x 2 2
|
||
|
||
^ \pi ^ \pi ^
|
||
normal (t) = \underset quarter circle rotation \underbrace x(t) · sin ─── + y(t) · cos ─── = x(t)
|
||
y 2 2
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/pointvectors/c3d5f3506b763b718e567f90dbb78324.svg"
|
||
width="324px"
|
||
height="79px"
|
||
loading="lazy"
|
||
/>
|
||
<div class="note">
|
||
<p>
|
||
Rotating coordinates is actually very easy, if you know the rule for it. You might find it explained as "applying a
|
||
<a href="https://en.wikipedia.org/wiki/Rotation_matrix">rotation matrix</a>, which is what we'll look at here, too. Essentially, the
|
||
idea is to take the circles over which we can rotate, and simply "sliding the coordinates" over these circles by the desired angle. If
|
||
we want a quarter circle turn, we take the coordinate, slide it along the circle by a quarter turn, and done.
|
||
</p>
|
||
<p>
|
||
To turn any point <i>(x,y)</i> into a rotated point <i>(x',y')</i> (over 0,0) by some angle φ, we apply this nice and easy computation:
|
||
</p>
|
||
<!--
|
||
|
||
x' = x · cos (\phi) - y · sin (\phi)
|
||
y' = x · sin (\phi) + y · cos (\phi)
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/pointvectors/1df6c055ae8e41a46bfdebc55a4f17c0.svg"
|
||
width="183px"
|
||
height="37px"
|
||
loading="lazy"
|
||
/>
|
||
<p>Which is the "long" version of the following matrix transformation:</p>
|
||
<!--
|
||
|
||
┌ x' ┐ = ┌ cos (\phi) -sin (\phi) ┐ ┌ x ┐
|
||
└ y' ┘ └ sin (\phi) cos (\phi) ┘ └ y ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/pointvectors/f02e359a5e47667919738fff69d2625b.svg"
|
||
width="211px"
|
||
height="40px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
And that's all we need to rotate any coordinate. Note that for quarter, half, and three-quarter turns these functions become even
|
||
easier, since <em>sin</em> and <em>cos</em> for these angles are, respectively: 0 and 1, -1 and 0, and 0 and -1.
|
||
</p>
|
||
<p>
|
||
But <strong><em>why</em></strong> does this work? Why this matrix multiplication?
|
||
<a href="https://en.wikipedia.org/wiki/Rotation_matrix#Decomposition_into_shears">Wikipedia</a> (technically, Thomas Herter and Klaus
|
||
Lott) tells us that a rotation matrix can be treated as a sequence of three (elementary) shear operations. When we combine this into a
|
||
single matrix operation (because all matrix multiplications can be collapsed), we get the matrix that you see above.
|
||
<a href="https://datagenetics.com/blog/august32013/index.html">DataGenetics</a> have an excellent article about this very thing: it's
|
||
really quite cool, and I strongly recommend taking a quick break from this primer to read that article.
|
||
</p>
|
||
</div>
|
||
|
||
<p>
|
||
The following two graphics show the tangent and normal along a quadratic and cubic curve, with the direction vector coloured blue, and the
|
||
normal vector coloured red (the markers are spaced out evenly as <em>t</em>-intervals, not spaced equidistant).
|
||
</p>
|
||
<div class="figure">
|
||
<graphics-element
|
||
title="Quadratic Bézier tangents and normals"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/pointvectors/pointvectors.js"
|
||
data-type="quadratic"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/pointvectors/d0b09bb338bd42d4164ced871d1f77ba.png" loading="lazy" />
|
||
<label>Quadratic Bézier tangents and normals</label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
<graphics-element
|
||
title="Cubic Bézier tangents and normals"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/pointvectors/pointvectors.js"
|
||
data-type="cubic"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/pointvectors/b8285282b8a0ce28fc2cc0392e9b607a.png" loading="lazy" />
|
||
<label>Cubic Bézier tangents and normals</label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
</div>
|
||
</section>
|
||
<section id="pointvectors3d">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#pointvectors">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#components">下</a></div>
|
||
<a href="zh-CN/index.html#pointvectors3d">Working with 3D normals</a>
|
||
</h1>
|
||
<p>
|
||
Before we move on to the next section we need to spend a little bit of time on the difference between 2D and 3D. While for many things
|
||
this difference is irrelevant and the procedures are identical (for instance, getting the 3D tangent is just doing what we do for 2D, but
|
||
for x, y, and z, instead of just for x and y), when it comes to normals things are a little more complex, and thus more work. Mind you,
|
||
it's not "super hard", but there are more steps involved and we should have a look at those.
|
||
</p>
|
||
<p>
|
||
Getting normals in 3D is in principle the same as in 2D: we take the normalised tangent vector, and then rotate it by a quarter turn.
|
||
However, this is where things get that little more complex: we can turn in quite a few directions, since "the normal" in 3D is a plane,
|
||
not a single vector, so we basically need to define what "the" normal is in the 3D case.
|
||
</p>
|
||
<p>
|
||
The "naïve" approach is to construct what is known as the
|
||
<a href="https://en.wikipedia.org/wiki/Frenet%E2%80%93Serret_formulas">Frenet normal</a>, where we follow a simple recipe that works in
|
||
many cases (but does super bizarre things in some others). The idea is that even though there are infinitely many vectors that are
|
||
perpendicular to the tangent (i.e. make a 90 degree angle with it), the tangent itself sort of lies on its own plane already: since each
|
||
point on the curve (no matter how closely spaced) has its own tangent vector, we can say that each point lies in the same plane as the
|
||
local tangent, as well as the tangents "right next to it".
|
||
</p>
|
||
<p>
|
||
Even if that difference in tangent vectors is minute, "any difference" is all we need to find out what that plane is - or rather, what the
|
||
vector perpendicular to that plane is. Which is what we need: if we can calculate that vector, and we have the tangent vector that we know
|
||
lies on a plane, then we can rotate the tangent vector over the perpendicular, and presto. We have computed the normal using the same
|
||
logic we used for the 2D case: "just rotate it 90 degrees".
|
||
</p>
|
||
<p>So let's do that! And in a twist surprise, we can do this in four lines:</p>
|
||
<ul>
|
||
<li><strong>a</strong> = normalize(B'(t))</li>
|
||
<li><strong>b</strong> = normalize(<strong>a</strong> + B''(t))</li>
|
||
<li><strong>r</strong> = normalize(<strong>b</strong> × <strong>a</strong>)</li>
|
||
<li><strong>normal</strong> = normalize(<strong>r</strong> × <strong>a</strong>)</li>
|
||
</ul>
|
||
<p>Let's unpack that a little:</p>
|
||
<ul>
|
||
<li>
|
||
We start by taking the <a href="https://en.wikipedia.org/wiki/Unit_vector">normalized vector</a> for the derivative at some point on the
|
||
curve. We normalize it so the maths is less work. Less work is good.
|
||
</li>
|
||
<li>
|
||
Then, we compute <strong>b</strong> which represents what a next point's tangent would be if the curve stopped changing at our point and
|
||
just had the same derivative and second derivative from that point on.
|
||
</li>
|
||
<li>
|
||
This lets us find two vectors (the derivative, and the second derivative added to the derivative) that lie on the same plane, which
|
||
means we can use them to compute a vector perpendicular to that plane, using an elementary vector operation called the
|
||
<a href="https://en.wikipedia.org/wiki/Cross_product">cross product</a>. (Note that while that operation uses the × operator, it's most
|
||
definitely not a multiplication!) The result of that gives us a vector that we can use as the "axis of rotation" for turning the tangent
|
||
a quarter circle to get our normal, just like we did in the 2D case.
|
||
</li>
|
||
<li>
|
||
Since the cross product lets us find a vector that is perpendicular to some plane defined by two other vectors, and since the normal
|
||
vector should be perpendicular to the plane that the tangent and the axis of rotation lie in, we can use the cross product a second
|
||
time, and immediately get our normal vector.
|
||
</li>
|
||
</ul>
|
||
<p>
|
||
And then we're done, we found "the" normal vector for a 3D curve. Let's see what that looks like for a sample curve, shall we? You can
|
||
move your cursor across the graphic from left to right, to show the normal at a point with a t value that is based on your cursor
|
||
position: all the way on the left is 0, all the way on the right = 1, midway is t=0.5, etc:
|
||
</p>
|
||
<graphics-element
|
||
title="Some known and unknown vectors"
|
||
width="350"
|
||
height="300"
|
||
src="./chapters/pointvectors3d/frenet.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="350px" height="300px" src="./images/chapters/pointvectors3d/11c1da2357004bb51cf0c591fc492115.png" loading="lazy" />
|
||
<label></label>
|
||
</fallback-image>
|
||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control" />
|
||
</graphics-element>
|
||
<p>
|
||
However, if you've played with that graphic a bit, you might have noticed something odd. The normal seems to "suddenly twist around the
|
||
curve" between t=0.65 and t=0.75... Why is it doing that?
|
||
</p>
|
||
<p>
|
||
As it turns out, it's doing that because that's how the maths works, and that's the problem with Frenet normals: while they are
|
||
"mathematically correct", they are "practically problematic", and so for any kind of graphics work what we really want is a way to compute
|
||
normals that just... look good.
|
||
</p>
|
||
<p>Thankfully, Frenet normals are not our only option.</p>
|
||
<p>
|
||
Another option is to take a slightly more algorithmic approach and compute a form of
|
||
<a href="https://www.microsoft.com/en-us/research/wp-content/uploads/2016/12/Computation-of-rotation-minimizing-frames.pdf"
|
||
>Rotation Minimising Frame</a
|
||
>
|
||
(also known as "parallel transport frame" or "Bishop frame") instead, where a "frame" is a set made up of the tangent, the rotational
|
||
axis, and the normal vector, centered on an on-curve point.
|
||
</p>
|
||
<p>
|
||
These type of frames are computed based on "the previous frame", so we cannot simply compute these "on demand" for single points, as we
|
||
could for Frenet frames; we have to compute them for the entire curve. Thankfully, the procedure is pretty simple, and can be performed at
|
||
the same time that you're building lookup tables for your curve.
|
||
</p>
|
||
<p>
|
||
The idea is to take a starting "tangent/rotation axis/normal" frame at t=0, and then compute what the next frame "should" look like by
|
||
applying some rules that yield a good looking next frame. In the case of the RMF paper linked above, those rules are:
|
||
</p>
|
||
<ul>
|
||
<li>Take a point on the curve for which we know the RM frame already,</li>
|
||
<li>take a next point on the curve for which we don't know the RM frame yet, and</li>
|
||
<li>
|
||
reflect the known frame onto the next point, by treating the plane through the curve at the point exactly between the next and previous
|
||
points as a "mirror".
|
||
</li>
|
||
<li>
|
||
This gives the next point a tangent vector that's essentially pointing in the opposite direction of what it should be, and a normal
|
||
that's slightly off-kilter, so:
|
||
</li>
|
||
<li>
|
||
reflect the vectors of our "mirrored frame" a second time, but this time using the plane through the "next point" itself as "mirror".
|
||
</li>
|
||
<li>Done: the tangent and normal have been fixed, and we have a good looking frame to work with.</li>
|
||
</ul>
|
||
<p>So, let's write some code for that!</p>
|
||
<div class="howtocode">
|
||
<h3>Implementing Rotation Minimising Frames</h3>
|
||
<p>
|
||
We first assume we have a function for calculating the Frenet frame at a point, which we already discussed above, inn a way that it
|
||
yields a frame with properties:
|
||
</p>
|
||
|
||
<table class="code">
|
||
<tr>
|
||
<td>1</td>
|
||
<td rowspan="6">
|
||
<textarea disabled rows="6" role="doc-example">
|
||
{
|
||
o: origin of all vectors, i.e. the on-curve point,
|
||
t: tangent vector,
|
||
r: rotational axis vector,
|
||
n: normal vector
|
||
}</textarea
|
||
>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>6</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p>Then, we can write a function that generates a sequence of RM frames in the following manner:</p>
|
||
|
||
<table class="code">
|
||
<tr>
|
||
<td>1</td>
|
||
<td rowspan="36">
|
||
<textarea disabled rows="36" role="doc-example">
|
||
generateRMFrames(steps) -> frames:
|
||
step = 1.0/steps
|
||
|
||
// Start off with the standard tangent/axis/normal frame
|
||
// associated with the curve at t=0:
|
||
frames.add(getFrenetFrame(0))
|
||
|
||
// start constructing RM frames:
|
||
for t0 = 0, t0 < 1.0, t0 += step:
|
||
// start with the previous, known frame
|
||
x0 = frames.last
|
||
|
||
// get the next frame: we're going to keep its position and tangent,
|
||
// but we're going to recompute the axis and normal.
|
||
t1 = t0 + step
|
||
x1 = { o: getPoint(t1), t: getDerivative(t) }
|
||
|
||
// First we reflect x0's tangent and axis of rotation onto x1,
|
||
// through/ the plane of reflection at the point between x0 x1
|
||
v1 = x1.o - x0.o
|
||
c1 = v1 · v1
|
||
riL = x0.r - v1 * 2/c1 * v1 · x0.r
|
||
tiL = x0.t - v1 * 2/c1 * v1 · x0.t
|
||
|
||
// note that v1 is a vector, but 2/c1 and (v1 · ...) are just
|
||
// plain numbers, so we're just scaling v1 by some constant.
|
||
|
||
// Then we reflect a second time, over a plane at x1, so that
|
||
// the frame tangent is aligned with the curve tangent again:
|
||
v2 = x1.t - tiL
|
||
c2 = v2 · v2
|
||
|
||
// and we're done here:
|
||
x1.r = riL - v2 * 2/c2 * v2 · riL
|
||
x1.n = x1.r × x1.t
|
||
frames.add(x1)</textarea
|
||
>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>7</td>
|
||
</tr>
|
||
<tr>
|
||
<td>8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>9</td>
|
||
</tr>
|
||
<tr>
|
||
<td>10</td>
|
||
</tr>
|
||
<tr>
|
||
<td>11</td>
|
||
</tr>
|
||
<tr>
|
||
<td>12</td>
|
||
</tr>
|
||
<tr>
|
||
<td>13</td>
|
||
</tr>
|
||
<tr>
|
||
<td>14</td>
|
||
</tr>
|
||
<tr>
|
||
<td>15</td>
|
||
</tr>
|
||
<tr>
|
||
<td>16</td>
|
||
</tr>
|
||
<tr>
|
||
<td>17</td>
|
||
</tr>
|
||
<tr>
|
||
<td>18</td>
|
||
</tr>
|
||
<tr>
|
||
<td>19</td>
|
||
</tr>
|
||
<tr>
|
||
<td>20</td>
|
||
</tr>
|
||
<tr>
|
||
<td>21</td>
|
||
</tr>
|
||
<tr>
|
||
<td>22</td>
|
||
</tr>
|
||
<tr>
|
||
<td>23</td>
|
||
</tr>
|
||
<tr>
|
||
<td>24</td>
|
||
</tr>
|
||
<tr>
|
||
<td>25</td>
|
||
</tr>
|
||
<tr>
|
||
<td>26</td>
|
||
</tr>
|
||
<tr>
|
||
<td>27</td>
|
||
</tr>
|
||
<tr>
|
||
<td>28</td>
|
||
</tr>
|
||
<tr>
|
||
<td>29</td>
|
||
</tr>
|
||
<tr>
|
||
<td>30</td>
|
||
</tr>
|
||
<tr>
|
||
<td>31</td>
|
||
</tr>
|
||
<tr>
|
||
<td>32</td>
|
||
</tr>
|
||
<tr>
|
||
<td>33</td>
|
||
</tr>
|
||
<tr>
|
||
<td>34</td>
|
||
</tr>
|
||
<tr>
|
||
<td>35</td>
|
||
</tr>
|
||
<tr>
|
||
<td>36</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p>
|
||
Ignoring comments, this is certainly more code than when we were just computing a single Frenet frame, but it's not a crazy amount more
|
||
code to get much better looking normals.
|
||
</p>
|
||
</div>
|
||
|
||
<p>
|
||
Speaking of better looking, what does this actually look like? Let's revisit that earlier curve, but this time use rotation minimising
|
||
frames rather than Frenet frames:
|
||
</p>
|
||
<graphics-element
|
||
title="Some known and unknown vectors"
|
||
width="350"
|
||
height="300"
|
||
src="./chapters/pointvectors3d/rotation-minimizing.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="350px" height="300px" src="./images/chapters/pointvectors3d/f4a2fa1e0204c890b2bff07228ba678d.png" loading="lazy" />
|
||
<label></label>
|
||
</fallback-image>
|
||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control" />
|
||
</graphics-element>
|
||
<p>That looks so much better!</p>
|
||
<p>
|
||
For those reading along with the code: we don't even strictly speaking need a Frenet frame to start with: we could, for instance, treat
|
||
the z-axis as our initial axis of rotation, so that our initial normal is <strong>(0,0,1) × tangent</strong>, and then take things from
|
||
there, but having that initial "mathematically correct" frame so that the initial normal seems to line up based on the curve's orientation
|
||
in 3D space is just nice.
|
||
</p>
|
||
</section>
|
||
<section id="components">
|
||
<h1>
|
||
<div class="nav">
|
||
<a href="zh-CN/index.html#pointvectors3d">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#extremities">下</a>
|
||
</div>
|
||
<a href="zh-CN/index.html#components">Component functions</a>
|
||
</h1>
|
||
<p>
|
||
One of the first things people run into when they start using Bézier curves in their own programs is "I know how to draw the curve, but
|
||
how do I determine the bounding box?". It's actually reasonably straightforward to do so, but it requires having some knowledge on
|
||
exploiting math to get the values we need. For bounding boxes, we aren't actually interested in the curve itself, but only in its
|
||
"extremities": the minimum and maximum values the curve has for its x- and y-axis values. If you remember your calculus (provided you ever
|
||
took calculus, otherwise it's going to be hard to remember) we can determine function extremities using the first derivative of that
|
||
function, but this poses a problem, since our function is parametric: every axis has its own function.
|
||
</p>
|
||
<p>
|
||
The solution: compute the derivative for each axis separately, and then fit them back together in the same way we do for the original.
|
||
</p>
|
||
<p>
|
||
Let's look at how a parametric Bézier curve "splits up" into two normal functions, one for the x-axis and one for the y-axis. Note the
|
||
leftmost figure is again an interactive curve, without labeled axes (you get coordinates in the graph instead). The center and rightmost
|
||
figures are the component functions for computing the x-axis value, given a value for <i>t</i> (between 0 and 1 inclusive), and the y-axis
|
||
value, respectively.
|
||
</p>
|
||
<p>
|
||
If you move points in a curve sideways, you should only see the middle graph change; likewise, moving points vertically should only show a
|
||
change in the right graph.
|
||
</p>
|
||
<graphics-element
|
||
title="Quadratic Bézier curve components"
|
||
width="825"
|
||
height="275"
|
||
src="./chapters/components/components.js"
|
||
data-type="quadratic"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="825px" height="275px" src="./images/chapters/components/1e6e38f6403dbe4c8b80295a94fc6748.png" loading="lazy" />
|
||
<label></label> </fallback-image
|
||
></graphics-element>
|
||
<p> </p>
|
||
<graphics-element
|
||
title="Cubic Bézier curve components"
|
||
width="825"
|
||
height="275"
|
||
src="./chapters/components/components.js"
|
||
data-type="cubic"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="825px" height="275px" src="./images/chapters/components/348694339257428a260144da4bbf80fc.png" loading="lazy" />
|
||
<label></label> </fallback-image
|
||
></graphics-element>
|
||
</section>
|
||
<section id="extremities">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#components">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#boundingbox">下</a></div>
|
||
<a href="zh-CN/index.html#extremities">Finding extremities: root finding</a>
|
||
</h1>
|
||
<p>
|
||
Now that we understand (well, superficially anyway) the component functions, we can find the extremities of our Bézier curve by finding
|
||
maxima and minima on the component functions, by solving the equation B'(t) = 0. We've already seen that the derivative of a Bézier curve
|
||
is a simpler Bézier curve, but how do we solve the equality? Fairly easily, actually, until our derivatives are 4th order or higher...
|
||
then things get really hard. But let's start simple:
|
||
</p>
|
||
<h3>Quadratic curves: linear derivatives.</h3>
|
||
<p>
|
||
The derivative of a quadratic Bézier curve is a linear Bézier curve, interpolating between just two terms, which means finding the
|
||
solution for "where is this line 0" is effectively trivial by rewriting it to a function of <code>t</code> and solving. First we turn our
|
||
quadratic Bézier function into a linear one, by following the rule mentioned at the end of the
|
||
<a href="#derivatives">derivatives section</a>:
|
||
</p>
|
||
<!--
|
||
|
||
B'(t) = a(1-t) + b(t)= 0,
|
||
a - at + bt= 0,
|
||
(b-a)t + a= 0
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/extremities/55e16ef652d30face0f6586b675a6c7b.svg"
|
||
width="187px"
|
||
height="63px"
|
||
loading="lazy"
|
||
/>
|
||
<p>And then we turn this into our solution for <code>t</code> using basic arithmetics:</p>
|
||
<!--
|
||
|
||
(b-a)t + a= 0,
|
||
(b-a)t= -a,
|
||
-a
|
||
t= ───
|
||
b-a
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/extremities/1fab66c84e7df38a2edda147f939bd80.svg"
|
||
width="135px"
|
||
height="77px"
|
||
loading="lazy"
|
||
/>
|
||
<p>Done.</p>
|
||
<p>
|
||
Although with the <a href="https://en.wikipedia.org/wiki/Caveat_emptor#Caveat_lector">caveat</a> that if <code>b-a</code> is zero, there
|
||
is no solution and we probably shouldn't try to perform that division.
|
||
</p>
|
||
<h3>Cubic curves: the quadratic formula.</h3>
|
||
<p>
|
||
The derivative of a cubic Bézier curve is a quadratic Bézier curve, and finding the roots for a quadratic polynomial means we can apply
|
||
the <a href="https://en.wikipedia.org/wiki/Quadratic_formula">Quadratic formula</a>. If you've seen it before, you'll remember it, and if
|
||
you haven't, it looks like this:
|
||
</p>
|
||
<!--
|
||
|
||
┌────────┐
|
||
│ 2
|
||
2 -b ±⟍│b - 4ac
|
||
Given f(t) = at + bt + c, f(t)=0 when t = ───────────────
|
||
2a
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/extremities/3125ab785fb039994582552790a2674b.svg"
|
||
width="409px"
|
||
height="40px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
So, if we can rewrite the Bézier component function as a plain polynomial, we're done: we just plug in the values into the quadratic
|
||
formula, check if that square root is negative or not (if it is, there are no roots) and then just compute the two values that come out
|
||
(because of that plus/minus sign we get two). Any value between 0 and 1 is a root that matters for Bézier curves, anything below or above
|
||
that is irrelevant (because Bézier curves are only defined over the interval [0,1]). So, how do we convert?
|
||
</p>
|
||
<p>
|
||
First we turn our cubic Bézier function into a quadratic one, by following the rule mentioned at the end of the
|
||
<a href="#derivatives">derivatives section</a>:
|
||
</p>
|
||
<!--
|
||
|
||
B(t) uses { p ,p ,p ,p }
|
||
1 2 3 4
|
||
B'(t) uses { v ,v ,v }, where v = 3(p -p ), v = 3(p -p ), v = 3(p -p )
|
||
1 2 3 1 2 1 2 3 2 3 4 3
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/extremities/bf0ad4611c47f8548396e40595c02b55.svg"
|
||
width="537px"
|
||
height="36px"
|
||
loading="lazy"
|
||
/>
|
||
<p>And then, using these <em>v</em> values, we can find out what our <em>a</em>, <em>b</em>, and <em>c</em> should be:</p>
|
||
<!--
|
||
|
||
2 2
|
||
B'(t)= v (1-t) + 2v (1-t)t + v t
|
||
1 2 3
|
||
2 2 2
|
||
...= v (t - 2t + 1) + 2v (t-t ) + v t
|
||
1 2 3
|
||
2 2 2
|
||
...= v t - 2v t + v + 2v t - 2v t + v t
|
||
1 1 1 2 2 3
|
||
2 2 2
|
||
...= v t - 2v t + v t - 2v t + v + 2v t
|
||
1 2 3 1 1 2
|
||
2
|
||
...= (v -2v +v )t + 2(v -v )t + v
|
||
1 2 3 2 1 1
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/extremities/d1c65d927825f20c3c358d1ff96ce881.svg"
|
||
width="315px"
|
||
height="119px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
This gives us three coefficients {a, b, c} that are expressed in terms of <code>v</code> values, where the <code>v</code> values are
|
||
expressions of our original coordinate values, so we can do some substitution to get:
|
||
</p>
|
||
<!--
|
||
|
||
a= v -2v +v = 3(-p + 3p - 3p + p )
|
||
1 2 3 1 2 3 4
|
||
b= 2(v -v ) = 6(p - 2p + p )
|
||
2 1 1 2 3
|
||
c= v = 3(p -p )
|
||
1 2 1
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/extremities/a6acf08f43aa1f48c08a40e76bdd2a31.svg"
|
||
width="308px"
|
||
height="63px"
|
||
loading="lazy"
|
||
/>
|
||
<p>Easy-peasy. We can now almost trivially find the roots by plugging those values into the quadratic formula.</p>
|
||
<p>
|
||
And as a cubic curve, there is also a meaningful second derivative, which we can compute by simple taking the derivative of the
|
||
derivative.
|
||
</p>
|
||
<h3>Quartic curves: Cardano's algorithm.</h3>
|
||
<p>
|
||
We haven't really looked at them before now, but the next step up would be a Quartic curve, a fourth degree Bézier curve. As expected,
|
||
these have a derivative that is a cubic function, and now things get much harder. Cubic functions don't have a "simple" rule to find their
|
||
roots, like the quadratic formula, and instead require quite a bit of rewriting to a form that we can even start to try to solve.
|
||
</p>
|
||
<p>
|
||
Back in the 16<sup>th</sup> century, before Bézier curves were a thing, and even before <em>calculus itself</em> was a thing,
|
||
<a href="https://en.wikipedia.org/wiki/Gerolamo_Cardano">Gerolamo Cardano</a> figured out that even if the general cubic function is
|
||
really hard to solve, it can be rewritten to a form for which finding the roots is "easier" (even if not "easy"):
|
||
</p>
|
||
<!--
|
||
|
||
3 2
|
||
very hard: solve at + bt + ct + d = 0
|
||
3
|
||
easier: solve t + pt + q = 0
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/extremities/d31432533bd7940545d4a269eefbabf2.svg"
|
||
width="253px"
|
||
height="44px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
We can see that the easier formula only has two constants, rather than four, and only two expressions involving <code>t</code>, rather
|
||
than three: this makes things considerably easier to solve because it lets us use
|
||
<a href="https://www.wolframalpha.com/input/?i=t%5E3+%2B+pt+%2B+q">regular calculus</a> to find the values that satisfy the equation.
|
||
</p>
|
||
<p>
|
||
Now, there is one small hitch: as a cubic function, the solutions may be
|
||
<a href="https://en.wikipedia.org/wiki/Complex_number">complex numbers</a> rather than plain numbers... And Cardano realised this,
|
||
centuries before complex numbers were a well-understood and established part of number theory. His interpretation of them was "these
|
||
numbers are impossible but that's okay because they disappear again in later steps", allowing him to not think about them too much, but we
|
||
have it even easier: as we're trying to find the roots for display purposes, we don't even <em>care</em> about complex numbers: we're
|
||
going to simplify Cardano's approach just that tiny bit further by throwing away any solution that's not a plain number.
|
||
</p>
|
||
<p>
|
||
So, how do we rewrite the hard formula into the easier formula? This is explained in detail over at
|
||
<a href="https://trans4mind.com/personal_development/mathematics/polynomials/cubicAlgebra.htm">Ken J. Ward's page</a> for solving the
|
||
cubic equation, so instead of showing the maths, I'm simply going to show the programming code for solving the cubic equation, with the
|
||
complex roots getting totally ignored, but if you're interested you should definitely head over to Ken's page and give the procedure a
|
||
read-through.
|
||
</p>
|
||
<div class="howtocode">
|
||
<h3>Implementing Cardano's algorithm for finding all real roots</h3>
|
||
<p>
|
||
The "real roots" part is fairly important, because while you cannot take a square, cube, etc. root of a negative number in the "real"
|
||
number space (denoted with ℝ), this is perfectly fine in the
|
||
<a href="https://en.wikipedia.org/wiki/Complex_number">"complex" number</a> space (denoted with ℂ). And, as it so happens, Cardano is
|
||
also attributed as the first mathematician in history to have made use of complex numbers in his calculations. For this very algorithm!
|
||
</p>
|
||
|
||
<table class="code">
|
||
<tr>
|
||
<td>1</td>
|
||
<td rowspan="81">
|
||
<textarea disabled rows="81" role="doc-example">
|
||
// A helper function to filter for values in the [0,1] interval:
|
||
function accept(t) {
|
||
return 0<=t && t <=1;
|
||
}
|
||
|
||
// A real-cuberoots-only function:
|
||
function cuberoot(v) {
|
||
if(v<0) return -pow(-v,1/3);
|
||
return pow(v,1/3);
|
||
}
|
||
|
||
// Now then: given cubic coordinates {pa, pb, pc, pd} find all roots.
|
||
function getCubicRoots(pa, pb, pc, pd) {
|
||
var a = (3*pa - 6*pb + 3*pc),
|
||
b = (-3*pa + 3*pb),
|
||
c = pa,
|
||
d = (-pa + 3*pb - 3*pc + pd);
|
||
|
||
// do a check to see whether we even need cubic solving:
|
||
if (approximately(d,0)) {
|
||
// this is not a cubic curve.
|
||
if (approximately(a,0)) {
|
||
// in fact, this is not a quadratic curve either.
|
||
if (approximately(b,0)) {
|
||
// in fact in fact, there are no solutions.
|
||
return [];
|
||
}
|
||
// linear solution
|
||
return [-c / b].filter(accept);
|
||
}
|
||
// quadratic solution
|
||
var q = sqrt(b*b - 4*a*c), 2a = 2*a;
|
||
return [(q-b)/2a, (-b-q)/2a].filter(accept)
|
||
}
|
||
|
||
// at this point, we know we need a cubic solution.
|
||
|
||
a /= d;
|
||
b /= d;
|
||
c /= d;
|
||
|
||
var p = (3*b - a*a)/3,
|
||
p3 = p/3,
|
||
q = (2*a*a*a - 9*a*b + 27*c)/27,
|
||
q2 = q/2,
|
||
discriminant = q2*q2 + p3*p3*p3;
|
||
|
||
// and some variables we're going to use later on:
|
||
var u1, v1, root1, root2, root3;
|
||
|
||
// three possible real roots:
|
||
if (discriminant < 0) {
|
||
var mp3 = -p/3,
|
||
mp33 = mp3*mp3*mp3,
|
||
r = sqrt( mp33 ),
|
||
t = -q / (2*r),
|
||
cosphi = t<-1 ? -1 : t>1 ? 1 : t,
|
||
phi = acos(cosphi),
|
||
crtr = cuberoot(r),
|
||
t1 = 2*crtr;
|
||
root1 = t1 * cos(phi/3) - a/3;
|
||
root2 = t1 * cos((phi+2*pi)/3) - a/3;
|
||
root3 = t1 * cos((phi+4*pi)/3) - a/3;
|
||
return [root1, root2, root3].filter(accept);
|
||
}
|
||
|
||
// three real roots, but two of them are equal:
|
||
if(discriminant === 0) {
|
||
u1 = q2 < 0 ? cuberoot(-q2) : -cuberoot(q2);
|
||
root1 = 2*u1 - a/3;
|
||
root2 = -u1 - a/3;
|
||
return [root1, root2].filter(accept);
|
||
}
|
||
|
||
// one real root, two complex roots
|
||
var sd = sqrt(discriminant);
|
||
u1 = cuberoot(sd - q2);
|
||
v1 = cuberoot(sd + q2);
|
||
root1 = u1 - v1 - a/3;
|
||
return [root1].filter(accept);
|
||
}</textarea
|
||
>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>7</td>
|
||
</tr>
|
||
<tr>
|
||
<td>8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>9</td>
|
||
</tr>
|
||
<tr>
|
||
<td>10</td>
|
||
</tr>
|
||
<tr>
|
||
<td>11</td>
|
||
</tr>
|
||
<tr>
|
||
<td>12</td>
|
||
</tr>
|
||
<tr>
|
||
<td>13</td>
|
||
</tr>
|
||
<tr>
|
||
<td>14</td>
|
||
</tr>
|
||
<tr>
|
||
<td>15</td>
|
||
</tr>
|
||
<tr>
|
||
<td>16</td>
|
||
</tr>
|
||
<tr>
|
||
<td>17</td>
|
||
</tr>
|
||
<tr>
|
||
<td>18</td>
|
||
</tr>
|
||
<tr>
|
||
<td>19</td>
|
||
</tr>
|
||
<tr>
|
||
<td>20</td>
|
||
</tr>
|
||
<tr>
|
||
<td>21</td>
|
||
</tr>
|
||
<tr>
|
||
<td>22</td>
|
||
</tr>
|
||
<tr>
|
||
<td>23</td>
|
||
</tr>
|
||
<tr>
|
||
<td>24</td>
|
||
</tr>
|
||
<tr>
|
||
<td>25</td>
|
||
</tr>
|
||
<tr>
|
||
<td>26</td>
|
||
</tr>
|
||
<tr>
|
||
<td>27</td>
|
||
</tr>
|
||
<tr>
|
||
<td>28</td>
|
||
</tr>
|
||
<tr>
|
||
<td>29</td>
|
||
</tr>
|
||
<tr>
|
||
<td>30</td>
|
||
</tr>
|
||
<tr>
|
||
<td>31</td>
|
||
</tr>
|
||
<tr>
|
||
<td>32</td>
|
||
</tr>
|
||
<tr>
|
||
<td>33</td>
|
||
</tr>
|
||
<tr>
|
||
<td>34</td>
|
||
</tr>
|
||
<tr>
|
||
<td>35</td>
|
||
</tr>
|
||
<tr>
|
||
<td>36</td>
|
||
</tr>
|
||
<tr>
|
||
<td>37</td>
|
||
</tr>
|
||
<tr>
|
||
<td>38</td>
|
||
</tr>
|
||
<tr>
|
||
<td>39</td>
|
||
</tr>
|
||
<tr>
|
||
<td>40</td>
|
||
</tr>
|
||
<tr>
|
||
<td>41</td>
|
||
</tr>
|
||
<tr>
|
||
<td>42</td>
|
||
</tr>
|
||
<tr>
|
||
<td>43</td>
|
||
</tr>
|
||
<tr>
|
||
<td>44</td>
|
||
</tr>
|
||
<tr>
|
||
<td>45</td>
|
||
</tr>
|
||
<tr>
|
||
<td>46</td>
|
||
</tr>
|
||
<tr>
|
||
<td>47</td>
|
||
</tr>
|
||
<tr>
|
||
<td>48</td>
|
||
</tr>
|
||
<tr>
|
||
<td>49</td>
|
||
</tr>
|
||
<tr>
|
||
<td>50</td>
|
||
</tr>
|
||
<tr>
|
||
<td>51</td>
|
||
</tr>
|
||
<tr>
|
||
<td>52</td>
|
||
</tr>
|
||
<tr>
|
||
<td>53</td>
|
||
</tr>
|
||
<tr>
|
||
<td>54</td>
|
||
</tr>
|
||
<tr>
|
||
<td>55</td>
|
||
</tr>
|
||
<tr>
|
||
<td>56</td>
|
||
</tr>
|
||
<tr>
|
||
<td>57</td>
|
||
</tr>
|
||
<tr>
|
||
<td>58</td>
|
||
</tr>
|
||
<tr>
|
||
<td>59</td>
|
||
</tr>
|
||
<tr>
|
||
<td>60</td>
|
||
</tr>
|
||
<tr>
|
||
<td>61</td>
|
||
</tr>
|
||
<tr>
|
||
<td>62</td>
|
||
</tr>
|
||
<tr>
|
||
<td>63</td>
|
||
</tr>
|
||
<tr>
|
||
<td>64</td>
|
||
</tr>
|
||
<tr>
|
||
<td>65</td>
|
||
</tr>
|
||
<tr>
|
||
<td>66</td>
|
||
</tr>
|
||
<tr>
|
||
<td>67</td>
|
||
</tr>
|
||
<tr>
|
||
<td>68</td>
|
||
</tr>
|
||
<tr>
|
||
<td>69</td>
|
||
</tr>
|
||
<tr>
|
||
<td>70</td>
|
||
</tr>
|
||
<tr>
|
||
<td>71</td>
|
||
</tr>
|
||
<tr>
|
||
<td>72</td>
|
||
</tr>
|
||
<tr>
|
||
<td>73</td>
|
||
</tr>
|
||
<tr>
|
||
<td>74</td>
|
||
</tr>
|
||
<tr>
|
||
<td>75</td>
|
||
</tr>
|
||
<tr>
|
||
<td>76</td>
|
||
</tr>
|
||
<tr>
|
||
<td>77</td>
|
||
</tr>
|
||
<tr>
|
||
<td>78</td>
|
||
</tr>
|
||
<tr>
|
||
<td>79</td>
|
||
</tr>
|
||
<tr>
|
||
<td>80</td>
|
||
</tr>
|
||
<tr>
|
||
<td>81</td>
|
||
</tr>
|
||
</table>
|
||
</div>
|
||
|
||
<p>
|
||
And that's it. The maths is complicated, but the code is pretty much just "follow the maths, while caching as many values as we can to
|
||
prevent recomputing things as much as possible" and now we have a way to find all roots for a cubic function and can just move on with
|
||
using that to find extremities of our curves.
|
||
</p>
|
||
<p>
|
||
And of course, as a quartic curve also has meaningful second and third derivatives, we can quite easily compute those by using the
|
||
derivative of the derivative (of the derivative), just as for cubic curves.
|
||
</p>
|
||
<h3>Quintic and higher order curves: finding numerical solutions</h3>
|
||
<p>
|
||
And this is where thing stop, because we <em>cannot</em> find the roots for polynomials of degree 5 or higher using algebra (a fact known
|
||
as <a href="https://en.wikipedia.org/wiki/Abel%E2%80%93Ruffini_theorem">the Abel–Ruffini theorem</a>). Instead, for occasions like these,
|
||
where algebra simply cannot yield an answer, we turn to <a href="https://en.wikipedia.org/wiki/Numerical_analysis">numerical analysis</a>.
|
||
</p>
|
||
<p>
|
||
That's a fancy term for saying "rather than trying to find exact answers by manipulating symbols, find approximate answers by describing
|
||
the underlying process as a combination of steps, each of which <em>can</em> be assigned a number via symbolic manipulation". For example,
|
||
trying to mathematically compute how much water fits in a completely crazy three dimensional shape is very hard, even if it got you the
|
||
perfect, precise answer. A much easier approach, which would be less perfect but still entirely useful, would be to just grab a buck and
|
||
start filling the shape until it was full: just count the number of buckets of water you used. And if we want a more precise answer, we
|
||
can use smaller buckets.
|
||
</p>
|
||
<p>
|
||
So that's what we're going to do here, too: we're going to treat the problem as a sequence of steps, and the smaller we can make each
|
||
step, the closer we'll get to that "perfect, precise" answer. And as it turns out, there is a really nice numerical root-finding
|
||
algorithm, called the <a href="https://en.wikipedia.org/wiki/Newton-Raphson">Newton-Raphson</a> root finding method (yes, after
|
||
<em><a href="https://en.wikipedia.org/wiki/Isaac_Newton">that</a></em> Newton), which we can make use of. The Newton-Raphson approach
|
||
consists of taking our impossible-to-solve function <code>f(x)</code>, picking some initial value <code>x</code> (literally any value will
|
||
do), and calculating <code>f(x)</code>. We can think of that value as the "height" of the function at <code>x</code>. If that height is
|
||
zero, we're done, we have found a root. If it isn't, we calculate the tangent line at <code>f(x)</code> and calculate at which
|
||
<code>x</code> value <em>its</em> height is zero (which we've already seen is very easy). That will give us a new <code>x</code> and we
|
||
repeat the process until we find a root.
|
||
</p>
|
||
<p>
|
||
Mathematically, this means that for some <code>x</code>, at step <code>n=1</code>, we perform the following calculation until
|
||
<code>f<sub>y</sub>(x)</code> is zero, so that the next <code>t</code> is the same as the one we already have:
|
||
</p>
|
||
<!--
|
||
|
||
f (x )
|
||
y n
|
||
x = x - ───────
|
||
n+1 n f' (x )
|
||
y n
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/extremities/53e67a29f134bd561aca550a2091a196.svg"
|
||
width="132px"
|
||
height="45px"
|
||
loading="lazy"
|
||
/>
|
||
<p>(The Wikipedia article has a decent animation for this process, so I will not add a graphic for that here)</p>
|
||
<p>
|
||
Now, this works well only if we can pick good starting points, and our curve is
|
||
<a href="https://en.wikipedia.org/wiki/Continuous_function">continuously differentiable</a> and doesn't have
|
||
<a href="https://en.wikipedia.org/wiki/Oscillation_(mathematics)">oscillations</a>. Glossing over the exact meaning of those terms, the
|
||
curves we're dealing with conform to those constraints, so as long as we pick good starting points, this will work. So the question is:
|
||
which starting points do we pick?
|
||
</p>
|
||
<p>
|
||
As it turns out, Newton-Raphson is so blindingly fast that we could get away with just not picking: we simply run the algorithm from
|
||
<em>t=0</em> to <em>t=1</em> at small steps (say, 1/200<sup>th</sup>) and the result will be all the roots we want. Of course, this may
|
||
pose problems for high order Bézier curves: 200 steps for a 200<sup>th</sup> order Bézier curve is going to go wrong, but that's okay:
|
||
there is no reason (at least, none that I know of) to <em>ever</em> use Bézier curves of crazy high orders. You might use a fifth order
|
||
curve to get the "nicest still remotely workable" approximation of a full circle with a single Bézier curve, but that's pretty much as
|
||
high as you'll ever need to go.
|
||
</p>
|
||
<h3>In conclusion:</h3>
|
||
<p>
|
||
So now that we know how to do root finding, we can determine the first and second derivative roots for our Bézier curves, and show those
|
||
roots overlaid on the previous graphics. For the quadratic curve, that means just the first derivative, in red:
|
||
</p>
|
||
<graphics-element
|
||
title="Quadratic Bézier curve extremities"
|
||
width="825"
|
||
height="275"
|
||
src="./chapters/extremities/extremities.js"
|
||
data-type="quadratic"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="825px" height="275px" src="./images/chapters/extremities/fd68347a917c9b703ff8005287ac6ca4.png" loading="lazy" />
|
||
<label></label> </fallback-image
|
||
></graphics-element>
|
||
<p>And for cubic curves, that means first and second derivatives, in red and purple respectively:</p>
|
||
<graphics-element
|
||
title="Cubic Bézier curve extremities"
|
||
width="825"
|
||
height="275"
|
||
src="./chapters/extremities/extremities.js"
|
||
data-type="cubic"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="825px" height="275px" src="./images/chapters/extremities/fbfe9464c9653f5efcd04411e683faf9.png" loading="lazy" />
|
||
<label></label> </fallback-image
|
||
></graphics-element>
|
||
</section>
|
||
<section id="boundingbox">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#extremities">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#aligning">下</a></div>
|
||
<a href="zh-CN/index.html#boundingbox">Bounding boxes</a>
|
||
</h1>
|
||
<p>
|
||
If we have the extremities, and the start/end points, a simple for-loop that tests for min/max values for x and y means we have the four
|
||
values we need to box in our curve:
|
||
</p>
|
||
<p><em>Computing the bounding box for a Bézier curve</em>:</p>
|
||
<ol>
|
||
<li>Find all <em>t</em> value(s) for the curve derivative's x- and y-roots.</li>
|
||
<li>Discard any <em>t</em> value that's lower than 0 or higher than 1, because Bézier curves only use the interval [0,1].</li>
|
||
<li>
|
||
Determine the lowest and highest value when plugging the values <em>t=0</em>, <em>t=1</em> and each of the found roots into the original
|
||
functions: the lowest value is the lower bound, and the highest value is the upper bound for the bounding box we want to construct.
|
||
</li>
|
||
</ol>
|
||
<p>
|
||
Applying this approach to our previous root finding, we get the following
|
||
<a href="https://en.wikipedia.org/wiki/Bounding_volume#Common_types">axis-aligned bounding boxes</a> (with all curve extremity points
|
||
shown on the curve):
|
||
</p>
|
||
<div class="figure">
|
||
<graphics-element
|
||
title="Quadratic Bézier bounding box"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/boundingbox/bbox.js"
|
||
data-type="quadratic"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/boundingbox/12ec4a5039de2e2cc06611db5e826282.png" loading="lazy" />
|
||
<label>Quadratic Bézier bounding box</label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
<graphics-element
|
||
title="Cubic Bézier bounding box"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/boundingbox/bbox.js"
|
||
data-type="cubic"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/boundingbox/daad01218ba430e2355d151811aa971b.png" loading="lazy" />
|
||
<label>Cubic Bézier bounding box</label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
</div>
|
||
|
||
<p>
|
||
We can construct even nicer boxes by aligning them along our curve, rather than along the x- and y-axis, but in order to do so we first
|
||
need to look at how aligning works.
|
||
</p>
|
||
</section>
|
||
<section id="aligning">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#boundingbox">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#tightbounds">下</a></div>
|
||
<a href="zh-CN/index.html#aligning">Aligning curves</a>
|
||
</h1>
|
||
<p>
|
||
While there are an incredible number of curves we can define by varying the x- and y-coordinates for the control points, not all curves
|
||
are actually distinct. For instance, if we define a curve, and then rotate it 90 degrees, it's still the same curve, and we'll find its
|
||
extremities in the same spots, just at different draw coordinates. As such, one way to make sure we're working with a "unique" curve is to
|
||
"axis-align" it.
|
||
</p>
|
||
<p>
|
||
Aligning also simplifies a curve's functions. We can translate (move) the curve so that the first point lies on (0,0), which turns our
|
||
<em>n</em> term polynomial functions into <em>n-1</em> term functions. The order stays the same, but we have less terms. Then, we can
|
||
rotate the curves so that the last point always lies on the x-axis, too, making its coordinate (...,0). This further simplifies the
|
||
function for the y-component to an <em>n-2</em> term function. For instance, if we have a cubic curve such as this:
|
||
</p>
|
||
<!--
|
||
|
||
╭ 3 2 2 3
|
||
╡ x = \colorblue 120 · (1-t) \colorblue + 35 · 3 · (1-t) · t \colorblue + 220 · 3 · (1-t) · t \colorblue + 220 · t
|
||
│ 3 2 2 3
|
||
╰ y = \colorblue 160 · (1-t) \colorblue + 200 · 3 · (1-t) · t \colorblue + 260 · 3 · (1-t) · t \colorblue + 40 · t
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/aligning/e5db5e945606d3429e4475ff92283a9c.svg" width="497px" height="40px" loading="lazy" />
|
||
<p>
|
||
Then translating it so that the first coordinate lies on (0,0), moving all <em>x</em> coordinates by -120, and all <em>y</em> coordinates
|
||
by -160, gives us:
|
||
</p>
|
||
<!--
|
||
|
||
╭ 3 2 2 3
|
||
╡ x = \colorblue 0 · (1-t) \colorblue - 85 · 3 · (1-t) · t \colorblue + 100 · 3 · (1-t) · t \colorblue + 100 · t
|
||
│ 3 2 2 3
|
||
╰ y = \colorblue 0 · (1-t) \colorblue + 40 · 3 · (1-t) · t \colorblue + 100 · 3 · (1-t) · t \colorblue - 120 · t
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/aligning/d412c72ed342c2036965ff251c8fb443.svg" width="481px" height="40px" loading="lazy" />
|
||
<p>
|
||
If we then rotate the curve so that its end point lies on the x-axis, the coordinates (integer-rounded for illustrative purposes here)
|
||
become:
|
||
</p>
|
||
<!--
|
||
|
||
╭ 3 2 2 3
|
||
╡ x = \colorblue 0 · (1-t) \colorblue - 85 · 3 · (1-t) · t \colorblue - 12 · 3 · (1-t) · t \colorblue + 156 · t
|
||
│ 3 2 2 3
|
||
╰ y = \colorblue 0 · (1-t) \colorblue - 40 · 3 · (1-t) · t \colorblue + 140 · 3 · (1-t) · t \colorblue + 0 · t
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/aligning/e49d7bb45a18d14fbb12df4d91e2c67b.svg" width="473px" height="40px" loading="lazy" />
|
||
<p>If we drop all the zero-terms, this gives us:</p>
|
||
<!--
|
||
|
||
╭ 2 2 3
|
||
╡ x = \colorblue - 85 · 3 · (1-t) · t \colorblue - 12 · 3 · (1-t) · t \colorblue + 156 · t
|
||
│ 2 2
|
||
╰ y = \colorblue - 40 · 3 · (1-t) · t \colorblue + 140 · 3 · (1-t) · t
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/aligning/016f37843b2af2ae34dc8b2975505f3d.svg" width="408px" height="40px" loading="lazy" />
|
||
<p>
|
||
We can see that our original curve definition has been simplified considerably. The following graphics illustrate the result of aligning
|
||
our example curves to the x-axis, with the cubic case using the coordinates that were just used in the example formulae:
|
||
</p>
|
||
<graphics-element
|
||
title="Aligning a quadratic curve"
|
||
width="550"
|
||
height="275"
|
||
src="./chapters/aligning/aligning.js"
|
||
data-type="quadratic"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="550px" height="275px" src="./images/chapters/aligning/28cc0f129fa0c028a1addd702e99f162.png" loading="lazy" />
|
||
<label></label> </fallback-image
|
||
></graphics-element>
|
||
<p> </p>
|
||
<graphics-element
|
||
title="Aligning a cubic curve"
|
||
width="550"
|
||
height="275"
|
||
src="./chapters/aligning/aligning.js"
|
||
data-type="cubic"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="550px" height="275px" src="./images/chapters/aligning/9a6755a1e31a990e8f072a6da98f811a.png" loading="lazy" />
|
||
<label></label> </fallback-image
|
||
></graphics-element>
|
||
</section>
|
||
<section id="tightbounds">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#aligning">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#inflections">下</a></div>
|
||
<a href="zh-CN/index.html#tightbounds">Tight bounding boxes</a>
|
||
</h1>
|
||
<p>
|
||
With our knowledge of bounding boxes, and curve alignment, We can now form the "tight" bounding box for curves. We first align our curve,
|
||
recording the translation we performed, "T", and the rotation angle we used, "R". We then determine the aligned curve's normal bounding
|
||
box. Once we have that, we can map that bounding box back to our original curve by rotating it by -R, and then translating it by -T.
|
||
</p>
|
||
<p>We now have nice tight bounding boxes for our curves:</p>
|
||
<div class="figure">
|
||
<graphics-element
|
||
title="Aligning a quadratic curve"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/tightbounds/tightbounds.js"
|
||
data-type="quadratic"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/tightbounds/ed91133976018ec032d9115344debb36.png" loading="lazy" />
|
||
<label>Aligning a quadratic curve</label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
<graphics-element
|
||
title="Aligning a cubic curve"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/tightbounds/tightbounds.js"
|
||
data-type="cubic"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/tightbounds/9ee5abc64b3fba71e284c70539279d74.png" loading="lazy" />
|
||
<label>Aligning a cubic curve</label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
</div>
|
||
|
||
<p>
|
||
These are, strictly speaking, not necessarily the tightest possible bounding boxes. It is possible to compute the optimal bounding box by
|
||
determining which spanning lines we need to effect a minimal box area, but because of the parametric nature of Bézier curves this is
|
||
actually a rather costly operation, and the gain in bounding precision is often not worth it.
|
||
</p>
|
||
</section>
|
||
<section id="inflections">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#tightbounds">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#canonical">下</a></div>
|
||
<a href="zh-CN/index.html#inflections">Curve inflections</a>
|
||
</h1>
|
||
<p>
|
||
Now that we know how to align a curve, there's one more thing we can calculate: inflection points. Imagine we have a variable size circle
|
||
that we can slide up against our curve. We place it against the curve and adjust its radius so that where it touches the curve, the
|
||
curvatures of the curve and the circle are the same, and then we start to slide the circle along the curve - for quadratic curves, we can
|
||
always do this without the circle behaving oddly: we might have to change the radius of the circle as we slide it along, but it'll always
|
||
sit against the same side of the curve.
|
||
</p>
|
||
<p>
|
||
But what happens with cubic curves? Imagine we have an S curve and we place our circle at the start of the curve, and start sliding it
|
||
along. For a while we can simply adjust the radius and things will be fine, but once we get to the midpoint of that S, something odd
|
||
happens: the circle "flips" from one side of the curve to the other side, in order for the curvatures to keep matching. This is called an
|
||
inflection, and we can find out where those happen relatively easily.
|
||
</p>
|
||
<p>What we need to do is solve a simple equation:</p>
|
||
<!--
|
||
|
||
C(t) = 0
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/inflections/ed68dcfb203517ca080fe48914769fb0.svg" width="59px" height="16px" loading="lazy" />
|
||
<p>
|
||
What we're saying here is that given the curvature function <em>C(t)</em>, we want to know for which values of <em>t</em> this function is
|
||
zero, meaning there is no "curvature", which will be exactly at the point between our circle being on one side of the curve, and our
|
||
circle being on the other side of the curve. So what does <em>C(t)</em> look like? Actually something that seems not too hard:
|
||
</p>
|
||
<!--
|
||
|
||
C(t) = Bézier \prime(t) · Bézier \prime\prime(t) - Bézier \prime(t) · Bézier \prime\prime(t)
|
||
x y y x
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/inflections/852f0346f025c671b8a1ce6b628028aa.svg"
|
||
width="385px"
|
||
height="21px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
The function <em>C(t)</em> is the cross product between the first and second derivative functions for the parametric dimensions of our
|
||
curve. And, as already shown, derivatives of Bézier curves are just simpler Bézier curves, with very easy to compute new coefficients, so
|
||
this should be pretty easy.
|
||
</p>
|
||
<p>
|
||
However as we've seen in the section on aligning, aligning lets us simplify things <em>a lot</em>, by completely removing the
|
||
contributions of the first coordinate from most mathematical evaluations, and removing the last <em>y</em> coordinate as well by virtue of
|
||
the last point lying on the x-axis. So, while we can evaluate <em>C(t) = 0</em> for our curve, it'll be much easier to first axis-align
|
||
the curve and <em>then</em> evaluating the curvature function.
|
||
</p>
|
||
<div class="note">
|
||
<h3>Let's derive the full formula anyway</h3>
|
||
<p>
|
||
Of course, before we do our aligned check, let's see what happens if we compute the curvature function without axis-aligning. We start
|
||
with the first and second derivatives, given our basis functions:
|
||
</p>
|
||
<!--
|
||
|
||
3 2 2 3
|
||
Bézier(t) = x (1-t) + 3x (1-t) t + 3x (1-t)t + x t
|
||
1 2 3 4
|
||
\prime 2 2
|
||
Bézier (t) = a(1-t) + 2b(1-t)t + ct { a=3(x -x ),b=3(x -x ),c=3(x -x ) }
|
||
2 1 3 2 4 3
|
||
\prime\prime
|
||
Bézier (t) = u(1-t) + vt {u=2(b-a),v=2(c-b)}\
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/inflections/35299f4eb8e0bed76b68c7beb2038031.svg"
|
||
width="601px"
|
||
height="71px"
|
||
loading="lazy"
|
||
/>
|
||
<p>And of course the same functions for <em>y</em>:</p>
|
||
<!--
|
||
|
||
3 2 2 3
|
||
Bézier(t) = y (1-t) + 3y (1-t) t + 3y (1-t)t + y t
|
||
1 2 3 4
|
||
\prime 2 2
|
||
Bézier (t) = d(1-t) + 2e(1-t)t + ft
|
||
\prime\prime
|
||
Bézier (t) = w(1-t) + zt
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/inflections/8278b9bec92ae49927283396692b51d5.svg"
|
||
width="399px"
|
||
height="69px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
Asking a computer to now compose the <em>C(t)</em> function for us (and to expand it to a readable form of simple terms) gives us this
|
||
rather overly complicated set of arithmetic expressions:
|
||
</p>
|
||
<!--
|
||
|
||
2 2 2 2 2
|
||
-18 t x y + 36 t x y - 18 t x y + 18 t x y - 54 t x y
|
||
2 1 3 1 4 1 1 2 3 2
|
||
2 2 2 2 2
|
||
+36 t x y - 36 t x y + 54 t x y - 18 t x y + 18 t x y
|
||
4 2 1 3 2 3 4 3 1 4
|
||
2 2
|
||
-36 t x y + 18 t x y + 36 t x y - 54 t x y + 18 t x y - 36 t x y
|
||
2 4 3 4 2 1 3 1 4 1 1 2
|
||
+54 t x y - 18 t x y + 54 t x y - 54 t x y - 18 t x y + 18 t x y
|
||
3 2 4 2 1 3 2 3 1 4 2 4
|
||
-18 x y + 18 x y + 18 x y - 18 x y - 18 x y + 18 x y
|
||
2 1 3 1 1 2 3 2 1 3 2 3
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/inflections/75fae2d0a94eae4addf074c294855fc7.svg"
|
||
width="552px"
|
||
height="97px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
That is... unwieldy. So, we note that there are a lot of terms that involve multiplications involving x1, y1, and y4, which would all
|
||
disappear if we axis-align our curve, which is why aligning is a great idea.
|
||
</p>
|
||
</div>
|
||
|
||
<p>
|
||
Aligning our curve so that three of the eight coefficients become zero, and observing that scale does not affect finding
|
||
<code>t</code> values, we end up with the following simple term function for <em>C(t)</em>:
|
||
</p>
|
||
<!--
|
||
|
||
2
|
||
(3 x y +2 x y +3 x y -x y ) t + (3 x y -x y -3 x y ) t + (x y -x y )
|
||
3 2 4 2 2 3 4 3 3 2 4 2 2 3 2 3 3 2
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/inflections/e50243eaa99b5acc08533dd2e9b71a74.svg"
|
||
width="533px"
|
||
height="20px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
That's a lot easier to work with: we see a fair number of terms that we can compute and then cache, giving us the following
|
||
simplification:
|
||
</p>
|
||
<!--
|
||
|
||
a = x · y ╮
|
||
3 2 │
|
||
b = x · y │ 2
|
||
4 2 ╞ C(t) = (-3a + 2b + 3c - d)t + (3a - b - 3c)t + (c - a)
|
||
c = x · y │
|
||
2 3 │
|
||
d = x · y │
|
||
4 3 ╯
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/inflections/2dbf3071d74e2ba37ab888aaa3c1a17c.svg"
|
||
width="467px"
|
||
height="73px"
|
||
loading="lazy"
|
||
/>
|
||
<p>This is a plain quadratic curve, and we know how to solve <em>C(t) = 0</em>; we use the quadratic formula:</p>
|
||
<!--
|
||
|
||
┌──────────┐
|
||
│ 2
|
||
x = -3a + 2b + 3c - d ╮ -y ±⟍│y - 4 x z
|
||
y = 3a - b - 3c ╞ C(t) = 0 ==> t = ─────────────────
|
||
z = c - a ╯ 2x
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/inflections/d7d564126099bc0740058a7cdd744772.svg"
|
||
width="405px"
|
||
height="55px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
We can easily compute this value <em>if</em> the discriminator isn't a negative number (because we only want real roots, not complex
|
||
roots), and <em>if</em> <em>x</em> is not zero, because divisions by zero are rather useless.
|
||
</p>
|
||
<p>
|
||
Taking that into account, we compute <em>t</em>, we disregard any <em>t</em> value that isn't in the Bézier interval [0,1], and we now
|
||
know at which <em>t</em> value(s) our curve will inflect.
|
||
</p>
|
||
<graphics-element
|
||
title="Finding cubic Bézier curve inflections"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/inflections/inflection.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/inflections/726ece45630c43be14589c51f1606bd7.png" loading="lazy" />
|
||
<label>Finding cubic Bézier curve inflections</label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
</section>
|
||
<section id="canonical">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#inflections">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#yforx">下</a></div>
|
||
<a href="zh-CN/index.html#canonical">The canonical form (for cubic curves)</a>
|
||
</h1>
|
||
<p>
|
||
While quadratic curves are relatively simple curves to analyze, the same cannot be said of the cubic curve. As a curvature is controlled
|
||
by more than one control point, it exhibits all kinds of features like loops, cusps, odd colinear features, and as many as two inflection
|
||
points because the curvature can change direction up to three times. Now, knowing what kind of curve we're dealing with means that some
|
||
algorithms can be run more efficiently than if we have to implement them as generic solvers, so is there a way to determine the curve type
|
||
without lots of work?
|
||
</p>
|
||
<p>
|
||
As it so happens, the answer is yes, and the solution we're going to look at was presented by Maureen C. Stone from Xerox PARC and Tony D.
|
||
deRose from the University of Washington in their joint paper
|
||
<a href="https://graphics.pixar.com/people/derose/publications/CubicClassification/paper.pdf"
|
||
>"A Geometric Characterization of Parametric Cubic curves"</a
|
||
>. It was published in 1989, and defines curves as having a "canonical" form (i.e. a form that all curves can be reduced to) from which we
|
||
can immediately tell what features a curve will have. So how does it work?
|
||
</p>
|
||
<p>
|
||
The first observation that makes things work is that if we have a cubic curve with four points, we can apply a linear transformation to
|
||
these points such that three of the points end up on (0,0), (0,1) and (1,1), with the last point then being "somewhere". After applying
|
||
that transformation, the location of that last point can then tell us what kind of curve we're dealing with. Specifically, we see the
|
||
following breakdown:
|
||
</p>
|
||
<graphics-element
|
||
title="The canonical curve map"
|
||
width="400"
|
||
height="400"
|
||
src="./chapters/canonical/canonical.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="400px" height="400px" src="./images/chapters/canonical/c086e72bd8aaeab37436515ab251b2df.png" loading="lazy" />
|
||
<label></label> </fallback-image
|
||
></graphics-element>
|
||
<p>This is a fairly funky image, so let's see what the various parts of it mean...</p>
|
||
<p>
|
||
We see the three fixed points at (0,0), (0,1) and (1,1). The various regions and boundaries indicate what property the original curve will
|
||
have, if the fourth point is in/on that region or boundary. Specifically, if the fourth point is...
|
||
</p>
|
||
<ol>
|
||
<li>
|
||
<p>
|
||
...anywhere inside the red zone, but not on its boundaries, the curve will be self-intersecting (yielding a loop). We won't know
|
||
<em>where</em> it self-intersects (in terms of <em>t</em> values), but we are guaranteed that it does.
|
||
</p>
|
||
</li>
|
||
<li>
|
||
<p>
|
||
...on the left (red) edge of the red zone, the curve will have a cusp. We again don't know <em>where</em>, but we know there is one.
|
||
This edge is described by the function:
|
||
</p>
|
||
<!--
|
||
|
||
2
|
||
-x + 2x + 3
|
||
y = ────────────, { x ≤1 }
|
||
4
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/canonical/674d251590411398d06fb99cba7920f7.svg"
|
||
width="180px"
|
||
height="37px"
|
||
loading="lazy"
|
||
/>
|
||
</li>
|
||
<li>
|
||
<p>
|
||
...on the almost circular, lower right (pink) edge, the curve's end point touches the curve, forming a loop. This edge is described by
|
||
the function:
|
||
</p>
|
||
<!--
|
||
|
||
┌──────────┐
|
||
│ 2
|
||
⟍│3(4x - x ) - x
|
||
y = ─────────────────, { 0 ≤x ≤1 }
|
||
2
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/canonical/6959a552f2c90a2bcaa787c23e19f488.svg"
|
||
width="231px"
|
||
height="39px"
|
||
loading="lazy"
|
||
/>
|
||
</li>
|
||
<li>
|
||
<p>...on the top (blue) edge, the curve's start point touches the curve, forming a loop. This edge is described by the function:</p>
|
||
<!--
|
||
|
||
2
|
||
-x + 3x
|
||
y = ────────, { x ≤0 }
|
||
3
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/canonical/464b4ec0b67f248459792752be86d46d.svg"
|
||
width="153px"
|
||
height="37px"
|
||
loading="lazy"
|
||
/>
|
||
</li>
|
||
<li>
|
||
<p>...inside the lower (green) zone, past <code>y=1</code>, the curve will have a single inflection (switching concave/convex once).</p>
|
||
</li>
|
||
<li>
|
||
<p>
|
||
...between the left and lower boundaries (below the cusp line but above the single-inflection line), the curve will have two
|
||
inflections (switching from concave to convex and then back again, or from convex to concave and then back again).
|
||
</p>
|
||
</li>
|
||
<li><p>...anywhere on the right of self-intersection zone, the curve will have no inflections. It'll just be a simple arch.</p></li>
|
||
</ol>
|
||
<p>Of course, this map is fairly small, but the regions extend to infinity, with well defined boundaries.</p>
|
||
<div class="note">
|
||
<h3>Wait, where do those lines come from?</h3>
|
||
<p>
|
||
Without repeating the paper mentioned at the top of this section, the loop-boundaries come from rewriting the curve into canonical form,
|
||
and then solving the formulae for which constraints must hold for which possible curve properties. In the paper these functions yield
|
||
formulae for where you will find cusp points, or loops where we know t=0 or t=1, but those functions are derived for the full cubic
|
||
expression, meaning they apply to t=-∞ to t=∞... For Bézier curves we only care about the "clipped interval" t=0 to t=1, so some of the
|
||
properties that apply when you look at the curve over an infinite interval simply don't apply to the Bézier curve interval.
|
||
</p>
|
||
<p>
|
||
The right bound for the loop region, indicating where the curve switches from "having inflections" to "having a loop", for the general
|
||
cubic curve, is actually mirrored over x=1, but for Bézier curves this right half doesn't apply, so we don't need to pay attention to
|
||
it. Similarly, the boundaries for t=0 and t=1 loops are also nice clean curves but get "cut off" when we only look at what the general
|
||
curve does over the interval t=0 to t=1.
|
||
</p>
|
||
<p>
|
||
For the full details, head over to the paper and read through sections 3 and 4. If you still remember your high school pre-calculus, you
|
||
can probably follow along with this paper, although you might have to read it a few times before all the bits "click".
|
||
</p>
|
||
</div>
|
||
|
||
<p>
|
||
So now the question becomes: how do we manipulate our curve so that it fits this canonical form, with three fixed points, and one "free"
|
||
point? Enter linear algebra. Don't worry, I'll be doing all the math for you, as well as show you what the effect is on our curves, but
|
||
basically we're going to be using linear algebra, rather than calculus, because "it's way easier". Sometimes a calculus approach is very
|
||
hard to work with, when the equivalent geometrical solution is super obvious.
|
||
</p>
|
||
<p>
|
||
The approach is going to start with a curve that doesn't have all-colinear points (so we need to make sure the points don't all fall on a
|
||
straight line), and then applying three graphics operations that you will probably have heard of: translation (moving all points by some
|
||
fixed x- and y-distance), scaling (multiplying all points by some x and y scale factor), and shearing (an operation that turns rectangles
|
||
into parallelograms).
|
||
</p>
|
||
<p>
|
||
Step 1: we translate any curve by -p1.x and -p1.y, so that the curve starts at (0,0). We're going to make use of an interesting trick
|
||
here, by pretending our 2D coordinates are 3D, with the <em>z</em> coordinate simply always being 1. This is an old trick in graphics to
|
||
overcome the limitations of 2D transformations: without it, we can only turn (x,y) coordinates into new coordinates of the form (ax + by,
|
||
cx + dy), which means we can't do translation, since that requires we end up with some kind of (x + a, y + b). If we add a bogus
|
||
<em>z</em> coordinate that is always 1, then we can suddenly add arbitrary values. For example:
|
||
</p>
|
||
<!--
|
||
|
||
┌ 1 0 a ┐ ┌ x ┐ ┌ 1 · x + 0 · y + a · z ┐ ┌ x + a · 1 ┐ ┌ x + a ┐
|
||
│ 0 1 b │ · │ y │ = │ 0 · x + 1 · y + b · z │ = │ y + b · 1 │ = │ y + b │
|
||
└ 0 0 1 ┘ └ z=1 ┘ └ 0 · x + 0 · y + 1 · z ┘ └ 1 · z ┘ └ z=1 ┘
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/canonical/7ed8b53100737cbf7d87aa6267395d2b.svg" width="464px" height="55px" loading="lazy" />
|
||
<p>
|
||
Sweet! <em>z</em> stays 1, so we can effectively ignore it entirely, but we added some plain values to our x and y coordinates. So, if we
|
||
want to subtract p1.x and p1.y, we use:
|
||
</p>
|
||
<!--
|
||
|
||
┌ 1 0 -P ┐ ┌ 1 · x + 0 · y - P · 1 ┐ ┌ x - P ┐
|
||
│ 1x │ ┌ x ┐ │ 1x │ │ 1x │
|
||
T = │ 0 1 -P │ · │ y │ = │ 0 · x + 1 · y - P · 1 │ = │ y - P │
|
||
1 │ 1y │ └ 1 ┘ │ 1y │ │ 1y │
|
||
└ 0 0 1 ┘ └ 0 · x + 0 · y + 1 · 1 ┘ └ 1 ┘
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/canonical/f855cbf1d73e4bb7bccbbd4721d95f41.svg" width="447px" height="57px" loading="lazy" />
|
||
<p>
|
||
Running all our coordinates through this transformation gives a new set of coordinates, let's call those <strong>U</strong>, where the
|
||
first coordinate lies on (0,0), and the rest is still somewhat free. Our next job is to make sure point 2 ends up lying on the
|
||
<em>x=0</em> line, so what we want is a transformation matrix that, when we run it, subtracts <em>x</em> from whatever <em>x</em> we
|
||
currently have. This is called <a href="https://en.wikipedia.org/wiki/Shear_matrix">shearing</a>, and the typical x-shear matrix and its
|
||
transformation looks like this:
|
||
</p>
|
||
<!--
|
||
|
||
┌ 1 S 0 ┐ ┌ x ┐ ┌ x + S · y ┐
|
||
│ 0 1 0 │ · │ y │ = │ y │
|
||
└ 0 0 1 ┘ └ 1 ┘ └ 1 ┘
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/canonical/2b6478075f2f9f5e5973e01b3b3a0c8b.svg" width="195px" height="53px" loading="lazy" />
|
||
<p>
|
||
So we want some shearing value that, when multiplied by <em>y</em>, yields <em>-x</em>, so our x coordinate becomes zero. That value is
|
||
simply <em>-x/y</em>, because *-x/y * y = -x*. Done:
|
||
</p>
|
||
<!--
|
||
|
||
┌ U ┐
|
||
│ 2x │
|
||
│ 1 -─── 0 │
|
||
T = │ U │
|
||
2 │ 2y │
|
||
│ 0 1 0 │
|
||
└ 0 0 1 ┘
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/canonical/ccbfd22cbccf633d182f7f451dee5164.svg" width="133px" height="67px" loading="lazy" />
|
||
<p>
|
||
Now, running this on all our points generates a new set of coordinates, let's call those <strong>V</strong>, which now have point 1 on
|
||
(0,0) and point 2 on (0, some-value), and we wanted it at (0,1), so we need to
|
||
<a href="https://en.wikipedia.org/wiki/Scaling_%28geometry%29">do some scaling</a> to make sure it ends up at (0,1). Additionally, we want
|
||
point 3 to end up on (1,1), so we can also scale x to make sure its x-coordinate will be 1 after we run the transform. That means we'll be
|
||
x-scaling by 1/point3<sub>x</sub>, and y-scaling by point2<sub>y</sub>. This is really easy:
|
||
</p>
|
||
<!--
|
||
|
||
┌ 1 ┐
|
||
│ ─── 0 0 │
|
||
│ V │
|
||
│ 3x │
|
||
T = │ 1 │
|
||
3 │ 0 ─── 0 │
|
||
│ V │
|
||
│ 2y │
|
||
└ 0 0 1 ┘
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/canonical/8e39a9e0c7469b4b45a260dd23bd4c6a.svg" width="137px" height="71px" loading="lazy" />
|
||
<p>
|
||
Then, finally, this generates a new set of coordinates, let's call those W, of which point 1 lies on (0,0), point 2 lies on (0,1), and
|
||
point three lies on (1, ...) so all that's left is to make sure point 3 ends up at (1,1) - but we can't scale! Point 2 is already in the
|
||
right place, and y-scaling would move it out of (0,1) again, so our only option is to y-shear point three, just like how we x-sheared
|
||
point 2 earlier. In this case, we do the same trick, but with <code>y/x</code> rather than <code>x/y</code> because we're not x-shearing
|
||
but y-shearing. Additionally, we don't actually want to end up at zero (which is what we did before) so we need to shear towards an
|
||
offset, in this case 1:
|
||
</p>
|
||
<!--
|
||
|
||
┌ 1 0 0 ┐
|
||
│ 1 - W │
|
||
│ 3y │
|
||
T = │ ─────── 1 0 │
|
||
4 │ W │
|
||
│ 3x │
|
||
└ 0 0 1 ┘
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/canonical/9420fd9d7a8de30714e23b8f31b3aa6d.svg" width="140px" height="65px" loading="lazy" />
|
||
<p>
|
||
And this generates our final set of four coordinates. Of these, we already know that points 1 through 3 are (0,0), (0,1) and (1,1), and
|
||
only the last coordinate is "free". In fact, given any four starting coordinates, the resulting "transformation mapped" coordinate will
|
||
be:
|
||
</p>
|
||
<!--
|
||
|
||
╭ (-x +x )(-y +y ) ╮
|
||
│ 1 2 1 4 │
|
||
│ -x + x - ──────────────── │
|
||
│ 1 4 -y +y │
|
||
│ 1 2 │
|
||
│ ─────────────────────────── │
|
||
│ (-x +x )(-y +y ) │
|
||
│ 1 2 1 3 │
|
||
│ -x +x -──────────────── │
|
||
│ 1 3 -y +y │
|
||
mapped = (x) = │ 1 2 │
|
||
4 y │ ╭ -y +y ╮ ╭ (-x +x )(-y +y ) ╮ │
|
||
│ │ 1 3 │ │ 1 2 1 4 │ │
|
||
│ │ 1 - ────── │ │ -x + x - ──────────────── │ │
|
||
│ (-y +y ) │ -y +y │ │ 1 4 -y +y │ │
|
||
│ 1 4 ╰ 1 2 ╯ ╰ 1 2 ╯ │
|
||
│ ──────── + ────────────────────────────────────────────── │
|
||
│ -y +y (-x +x )(-y +y ) │
|
||
│ 1 2 1 2 1 3 │
|
||
│ -x +x -──────────────── │
|
||
│ 1 3 -y +y │
|
||
╰ 1 2 ╯
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/canonical/fff37fa4275e43302f71cf052417a19f.svg" width="455px" height="91px" loading="lazy" />
|
||
<p>
|
||
Okay, well, that looks plain ridiculous, but: notice that every coordinate value is being offset by the initial translation, and also
|
||
notice that <em>a lot</em> of terms in that expression are repeated. Even though the maths looks crazy as a single expression, we can just
|
||
pull this apart a little and end up with an easy-to-calculate bit of code!
|
||
</p>
|
||
<p>
|
||
First, let's just do that translation step as a "preprocessing" operation so we don't have to subtract the values all the time. What does
|
||
that leave?
|
||
</p>
|
||
<!--
|
||
|
||
╭ x · y x · y ╮
|
||
│ 2 4 2 3 │
|
||
│ x - ──────── / x -──────── │ ╭ x ╮
|
||
│ 4 y 3 y │ │ 43 │ ╭ x · y x · y ╮
|
||
│ 2 2 │ │ │ │ 2 4 2 3 │
|
||
... = │ │ = │ y ╭ y ╮ │, where x = │ x - ──────── \middle / x -──────── │
|
||
│ y ╭ y ╮ ╭ x · y x · y ╮ │ │ 4 │ 3 │ │ 43 │ 4 y 3 y │
|
||
│ 4 │ 3 │ │ 2 4 2 3 │ │ │ ── + x │ 1 - ── │ │ ╰ 2 2 ╯
|
||
│ ── + │ 1 - ── │ · │ x - ──────── / x -──────── │ │ │ y 43 │ y │ │
|
||
│ y │ y │ │ 4 y 3 y │ │ ╰ 2 ╰ 2 ╯ ╯
|
||
╰ 2 ╰ 2 ╯ ╰ 2 2 ╯ ╯
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/canonical/5f174bc5019245f467ca63ae84b90a4b.svg" width="775px" height="67px" loading="lazy" />
|
||
<p>
|
||
Suddenly things look a lot simpler: the mapped x is fairly straight forward to compute, and we see that the mapped y actually contains the
|
||
mapped x in its entirety, so we'll have that part already available when we need to evaluate it. In fact, let's pull out all those common
|
||
factors to see just how simple this is:
|
||
</p>
|
||
<!--
|
||
|
||
╭ x ╮ ╭ x - x · y ╮ y y
|
||
│ 43 │ │ 4 2 42 │ 4 3
|
||
... = │ │, where x = │ ────────────── │, y = ──, and y = ──
|
||
│ y + x (1 - y ) │ 43 │ x - x · y │ 42 y 32 y
|
||
╰ 42 43 32 ╯ ╰ 3 2 32 ╯ 2 2
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/canonical/88e3fae7aeef6d7614290587422542c9.svg" width="563px" height="55px" loading="lazy" />
|
||
<p>
|
||
That's kind of super-simple to write out in code, I think you'll agree. Coding math tends to be easier than the formulae initially make it
|
||
look!
|
||
</p>
|
||
<div class="note">
|
||
<h3>How do you track all that?</h3>
|
||
<p>
|
||
Doing maths can be a pain, so whenever possible, I like to make computers do the work for me. Especially for things like this, I simply
|
||
use <a href="https://www.wolfram.com/mathematica/">Mathematica</a>. Tracking all this math by hand is insane, and we invented computers,
|
||
literally, to do this for us. I have no reason to use pen and paper when I can write out what I want to do in a program, and have the
|
||
program do the math for me. And real math, too, with symbols, not with numbers. In fact,
|
||
<a href="https://pomax.github.io/gh-weblog-2/downloads/canonical-curve.nb">here's</a> the Mathematica notebook if you want to see how
|
||
this works for yourself.
|
||
</p>
|
||
<p>
|
||
Now, I know, you're thinking "but Mathematica is super expensive!" and that's true, it's
|
||
<a href="https://www.wolfram.com/mathematica-home-edition/">$344 for home use, up from $295 when I original wrote this</a>, but it's
|
||
<strong>also</strong> <a href="https://www.wolfram.com/raspberry-pi/">free when you buy a $35 raspberry pi</a>. Obviously, I bought a
|
||
raspberry pi, and I encourage you to do the same. With that, as long as you know what you want to <em>do</em>, Mathematica can just do
|
||
it for you. And we don't have to be geniuses to work out what the maths looks like. That's what we have computers for.
|
||
</p>
|
||
</div>
|
||
|
||
<p>
|
||
So, let's write up a sketch that'll show us the canonical form for any curve drawn in blue, overlaid on our canonical map, so that we can
|
||
immediately tell which features our curve must have, based on where the fourth coordinate is located on the map:
|
||
</p>
|
||
<graphics-element
|
||
title="A cubic curve mapped to canonical form"
|
||
width="800"
|
||
height="400"
|
||
src="./chapters/canonical/interactive.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="800px" height="400px" src="./images/chapters/canonical/83fe2473e20ea68b768765129ee44ae4.png" loading="lazy" />
|
||
<label></label> </fallback-image
|
||
></graphics-element>
|
||
</section>
|
||
<section id="yforx">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#canonical">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#arclength">下</a></div>
|
||
<a href="zh-CN/index.html#yforx">Finding Y, given X</a>
|
||
</h1>
|
||
<p>
|
||
One common task that pops up in things like CSS work, or parametric equalizers, or image leveling, or any other number of applications
|
||
where Bézier curves are used as control curves in a way that there is really only ever one "y" value associated with one "x" value, you
|
||
might want to cut out the middle man, as it were, and compute "y" directly based on "x". After all, the function looks simple enough,
|
||
finding the "y" value should be simple too, right? Unfortunately, not really. However, it <em>is</em> possible and as long as you have
|
||
some code in place to help, it's not a lot of a work either.
|
||
</p>
|
||
<p>
|
||
We'll be tackling this problem in two stages: the first, which is the hard part, is figuring out which "t" value belongs to any given "x"
|
||
value. For instance, have a look at the following graphic. On the left we have a Bézier curve that looks for all intents and purposes like
|
||
it fits our criteria: every "x" has one and only one associated "y" value. On the right we see the function for just the "x" values:
|
||
that's a cubic curve, but not a really crazy cubic curve. If you move the graphic's slider, you will see a red line drawn that corresponds
|
||
to the <code>x</code> coordinate: this is a vertical line in the left graphic, and a horizontal line on the right.
|
||
</p>
|
||
<graphics-element
|
||
title="Finding t, given x=x(t). Left: our curve, right: the function x=f(t)"
|
||
width="550"
|
||
height="275"
|
||
src="./chapters/yforx/basics.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="550px" height="275px" src="./images/chapters/yforx/e469af5bf27a2c27d1dd6fc62a78ac27.png" loading="lazy" />
|
||
<label></label>
|
||
</fallback-image>
|
||
<input type="range" min="0" max="1" step="0.01" class="slide-control" />
|
||
</graphics-element>
|
||
<p>
|
||
Now, if you look more closely at that right graphic, you'll notice something interesting: if we treat the red line as "the x axis", then
|
||
the point where the function crosses our line is really just a root for the cubic function x(t) through a shifted "x-axis"... and
|
||
<a href="#extremities">we've already seen</a> how to calculate roots, so let's just run cubic root finding - and not even the complicated
|
||
cubic case either: because of the kind of curve we're starting with, we <em>know</em> there is only root, simplifying the code we need!
|
||
</p>
|
||
<p>First, let's look at the function for x(t):</p>
|
||
<!--
|
||
|
||
3 2 2 3
|
||
x(t) = a(1-t) + 3b(1-t) t + 3c(1-t)t + dt
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/yforx/378d0fd8cefa688d530ac38930d66844.svg" width="335px" height="19px" loading="lazy" />
|
||
<p>
|
||
We can rewrite this to a plain polynomial form, by just fully writing out the expansion and then collecting the polynomial factors, as:
|
||
</p>
|
||
<!--
|
||
|
||
3 2
|
||
x(t) = (-a + 3b- 3c + d)t + (3a - 6b + 3c)t + (-3a + 3b)t + a
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/yforx/021718d3b46893b271f90083ccdceaf8.svg" width="445px" height="19px" loading="lazy" />
|
||
<p>
|
||
Nothing special here: that's a standard cubic polynomial in "power" form (i.e. all the terms are ordered by their power of
|
||
<code>t</code>). So, given that <code>a</code>, <code>b</code>, <code>c</code>, <code>d</code>, <em>and</em> <code>x(t)</code> are all
|
||
known constants, we can trivially rewrite this (by moving the <code>x(t)</code> across the equal sign) as:
|
||
</p>
|
||
<!--
|
||
|
||
3 2
|
||
(-a + 3b - 3c + d)t + (3a - 6b + 3c)t + (-3a + 3b)t + (a-x) = 0
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/yforx/058a76e3e7d67c03f733e075829a6252.svg" width="465px" height="19px" loading="lazy" />
|
||
<p>
|
||
You might be wondering "where did all the other 'minus x' for all the other values a, b, c, and d go?" and the answer there is that they
|
||
all cancel out, so the only one we actually need to subtract is the one at the end. Handy! So now we just solve this equation using
|
||
Cardano's algorithm, and we're left with some rather short code:
|
||
</p>
|
||
|
||
<table class="code">
|
||
<tr>
|
||
<td>1</td>
|
||
<td rowspan="10">
|
||
<textarea disabled rows="10" role="doc-example">
|
||
// prepare our values for root finding:
|
||
x = a value we already know
|
||
xcoord = our set of Bézier curve's x coordinates
|
||
foreach p in xcoord: p.x -= x
|
||
|
||
// find our root, of which we know there is exactly one:
|
||
t = getRoots(p[0], p[1], p[2], p[3])[0]
|
||
|
||
// find our answer:
|
||
y = curve.get(t).y</textarea
|
||
>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>7</td>
|
||
</tr>
|
||
<tr>
|
||
<td>8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>9</td>
|
||
</tr>
|
||
<tr>
|
||
<td>10</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p>
|
||
So the procedure is fairly straight forward: pick an <code>x</code>, find the associated <code>t</code> value, evaluate our curve
|
||
<em>for</em> that <code>t</code> value, which gives us the curve's {x,y} coordinate, which means we know <code>y</code> for this
|
||
<code>x</code>. Move the slider for the following graphic to see this in action:
|
||
</p>
|
||
<graphics-element
|
||
title="Finding By(t), by finding t for a given x"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/yforx/yforx.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/yforx/2fc5c57e5d1ed0eaa1655edc31026252.png" loading="lazy" />
|
||
<label>Finding By(t), by finding t for a given x</label>
|
||
</fallback-image>
|
||
<input type="range" min="0" max="1" step="0.01" class="slide-control" />
|
||
</graphics-element>
|
||
</section>
|
||
<section id="arclength">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#yforx">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#arclengthapprox">下</a></div>
|
||
<a href="zh-CN/index.html#arclength">Arc length</a>
|
||
</h1>
|
||
<p>
|
||
How long is a Bézier curve? As it turns out, that's not actually an easy question, because the answer requires maths that —much like root
|
||
finding— cannot generally be solved the traditional way. If we have a parametric curve with <em>f<sub>x</sub>(t)</em> and
|
||
<em>f<sub>y</sub>(t)</em>, then the length of the curve, measured from start point to some point <em>t = z</em>, is computed using the
|
||
following seemingly straight forward (if a bit overwhelming) formula:
|
||
</p>
|
||
<!--
|
||
|
||
┌───────────────┐
|
||
╭ z │ 2 2
|
||
| │f '(t) +f '(t) dt
|
||
╯ 0⟍│ x y
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/arclength/59ebc3a7c3547a50998d1ea3664fb688.svg" width="140px" height="33px" loading="lazy" />
|
||
<p>or, more commonly written using Leibnitz notation as:</p>
|
||
<!--
|
||
|
||
┌─────────────────┐
|
||
╭ z │ 2 2
|
||
length = | ⟍│(dx/dt) +(dy/dt) dt
|
||
╯ 0
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/arclength/85620f0332fcf16f56c580794fd094c5.svg" width="245px" height="35px" loading="lazy" />
|
||
<p>
|
||
This formula says that the length of a parametric curve is in fact equal to the <strong>area</strong> underneath a function that looks a
|
||
remarkable amount like Pythagoras' rule for computing the diagonal of a straight angled triangle. This sounds pretty simple, right? Sadly,
|
||
it's far from simple... cutting straight to after the chase is over: for quadratic curves, this formula generates an
|
||
<a
|
||
href="https://www.wolframalpha.com/input/?i=antiderivative+for+sqrt((2*(1-t)*t*B+%2B+t%5E2*C)%27%5E2+%2B+(2*(1-t)*t*E)%27%5E2)&incParTime=true"
|
||
>unwieldy computation</a
|
||
>, and we're simply not going to implement things that way. For cubic Bézier curves, things get even more fun, because there is no "closed
|
||
form" solution, meaning that due to the way calculus works, there is no generic formula that allows you to calculate the arc length. Let
|
||
me just repeat this, because it's fairly crucial:
|
||
<strong
|
||
><em
|
||
>for cubic and higher Bézier curves, there is no way to solve this function if you want to use it "for all possible coordinates"</em
|
||
></strong
|
||
>.
|
||
</p>
|
||
<p>Seriously: <a href="https://en.wikipedia.org/wiki/Abel%E2%80%93Ruffini_theorem">It cannot be done</a>.</p>
|
||
<p>
|
||
So we turn to numerical approaches again. The method we'll look at here is the
|
||
<a href="https://www.youtube.com/watch?v=unWguclP-Ds&feature=BFa&list=PLC8FC40C714F5E60F&index=1">Gauss quadrature</a>. This approximation
|
||
is a really neat trick, because for any <em>n<sup>th</sup></em> degree polynomial it finds approximated values for an integral really
|
||
efficiently. Explaining this procedure in length is way beyond the scope of this page, so if you're interested in finding out why it
|
||
works, I can recommend the University of South Florida video lecture on the procedure, linked in this very paragraph. The general solution
|
||
we're looking for is the following:
|
||
</p>
|
||
<!--
|
||
|
||
┌─────────────────┐
|
||
╭ 1 │ 2 2 ╭ 1
|
||
| ⟍│(dx/dt) +(dy/dt) dt = | f(t) dt ≃ ┌ \underset strip 1 \underbrace C · f(t ) + ... + \underset strip n \underbrace C · f(t ) ┐ =
|
||
╯ -1 ╯ -1 └ 1 1 n n ┘
|
||
__ n
|
||
\underset strips 1 through n \underbrace ❯ C · f(t )
|
||
‾‾ i=1 i i
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/arclength/b76753476ad6ecfe4b8f39bcf9432980.svg" width="623px" height="71px" loading="lazy" />
|
||
<p>
|
||
In plain text: an integral function can always be treated as the sum of an (infinite) number of (infinitely thin) rectangular strips
|
||
sitting "under" the function's plotted graph. To illustrate this idea, the following graph shows the integral for a sinusoid function. The
|
||
more strips we use (and of course the more we use, the thinner they get) the closer we get to the true area under the curve, and thus the
|
||
better the approximation:
|
||
</p>
|
||
<div class="figure">
|
||
<graphics-element
|
||
title="A function's approximated integral"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/arclength/draw-slices.js"
|
||
data-steps="10"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/arclength/56533f47e73ad9fea08fa9bb3f597d49.png" loading="lazy" />
|
||
<label>A function's approximated integral</label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
<graphics-element
|
||
title="A better approximation"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/arclength/draw-slices.js"
|
||
data-steps="24"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/arclength/5ce02cbdbc47585c588f2656d5161a32.png" loading="lazy" />
|
||
<label>A better approximation</label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
<graphics-element
|
||
title="An even better approximation"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/arclength/draw-slices.js"
|
||
data-steps="99"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/arclength/fe2663b205d14c157a5a02bfbbd55987.png" loading="lazy" />
|
||
<label>An even better approximation</label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
</div>
|
||
|
||
<p>
|
||
Now, infinitely many terms to sum and infinitely thin rectangles are not something that computers can work with, so instead we're going to
|
||
approximate the infinite summation by using a sum of a finite number of "just thin" rectangular strips. As long as we use a high enough
|
||
number of thin enough rectangular strips, this will give us an approximation that is pretty close to what the real value is.
|
||
</p>
|
||
<p>
|
||
So, the trick is to come up with useful rectangular strips. A naive way is to simply create <em>n</em> strips, all with the same width,
|
||
but there is a far better way using special values for <em>C</em> and <em>f(t)</em> depending on the value of <em>n</em>, which indicates
|
||
how many strips we'll use, and it's called the Legendre-Gauss quadrature.
|
||
</p>
|
||
<p>
|
||
This approach uses strips that are <em>not</em> spaced evenly, but instead spaces them in a special way based on describing the function
|
||
as a polynomial (the more strips, the more accurate the polynomial), and then computing the exact integral for that polynomial. We're
|
||
essentially performing arc length computation on a flattened curve, but flattening it based on the intervals dictated by the
|
||
Legendre-Gauss solution.
|
||
</p>
|
||
<div class="note">
|
||
<p>
|
||
Note that one requirement for the approach we'll use is that the integral must run from -1 to 1. That's no good, because we're dealing
|
||
with Bézier curves, and the length of a section of curve applies to values which run from 0 to "some value smaller than or equal to 1"
|
||
(let's call that value <em>z</em>). Thankfully, we can quite easily transform any integral interval to any other integral interval, by
|
||
shifting and scaling the inputs. Doing so, we get the following:
|
||
</p>
|
||
<!--
|
||
|
||
┌─────────────────┐
|
||
╭ z │ 2 2
|
||
| ⟍│(dx/dt) +(dy/dt) dt
|
||
╯ 0
|
||
z ┌ ╭ z z ╮ ╭ z z ╮ ┐
|
||
≃ \ ─ · │ C · f│ ─ · t + ─ │ + ... + C · f│ ─ · t + ─ │ │
|
||
2 └ 1 ╰ 2 1 2 ╯ n ╰ 2 n 2 ╯ ┘
|
||
z __ n ╭ z z ╮
|
||
= \ ─ · ❯ C · f│ ─ · t + ─ │
|
||
2 ‾‾ i=1 i ╰ 2 i 2 ╯
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/arclength/0748ad25185548150b6c1c4c7039207e.svg"
|
||
width="341px"
|
||
height="72px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
That may look a bit more complicated, but the fraction involving <em>z</em> is a fixed number, so the summation, and the evaluation of
|
||
the <em>f(t)</em> values are still pretty simple.
|
||
</p>
|
||
<p>
|
||
So, what do we need to perform this calculation? For one, we'll need an explicit formula for <em>f(t)</em>, because that derivative
|
||
notation is handy on paper, but not when we have to implement it. We'll also need to know what these <em>C<sub>i</sub></em> and
|
||
<em>t<sub>i</sub></em> values should be. Luckily, that's less work because there are actually many tables available that give these
|
||
values, for any <em>n</em>, so if we want to approximate our integral with only two terms (which is a bit low, really) then
|
||
<a href="./legendre-gauss.html">these tables</a> would tell us that for <em>n=2</em> we must use the following values:
|
||
</p>
|
||
<!--
|
||
|
||
C = 1
|
||
1
|
||
C = 1
|
||
2
|
||
1
|
||
t = - ────
|
||
1 ┌─┐
|
||
⟍│3
|
||
1
|
||
t = + ────
|
||
2 ┌─┐
|
||
⟍│3
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/arclength/a91fbfb7abc38ff712ef660d85679f2e.svg" width="63px" height="93px" loading="lazy" />
|
||
<p>
|
||
Which means that in order for us to approximate the integral, we must plug these values into the approximate function, which gives us:
|
||
</p>
|
||
<!--
|
||
|
||
┌─────────────────┐
|
||
╭ z │ 2 2 z ┌ ╭ z -1 z ╮ ╭ z 1 z ╮ ┐
|
||
| ⟍│(dx/dt) +(dy/dt) dt ≃ ─ · │ f│ ─ · ──── + ─ │ + f│ ─ · ──── + ─ │ │
|
||
╯ 0 2 │ │ 2 ┌─┐ 2 │ │ 2 ┌─┐ 2 │ │
|
||
└ ╰ ⟍│3 ╯ ╰ ⟍│3 ╯ ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/arclength/046bbb52e8c8ed617fdf3a4fd18d62e1.svg"
|
||
width="476px"
|
||
height="44px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
We can program that pretty easily, provided we have that <em>f(t)</em> available, which we do, as we know the full description for the
|
||
Bézier curve functions B<sub>x</sub>(t) and B<sub>y</sub>(t).
|
||
</p>
|
||
</div>
|
||
|
||
<p>
|
||
If we use the Legendre-Gauss values for our <em>C</em> values (thickness for each strip) and <em>t</em> values (location of each strip),
|
||
we can determine the approximate length of a Bézier curve by computing the Legendre-Gauss sum. The following graphic shows a cubic curve,
|
||
with its computed lengths; Go ahead and change the curve, to see how its length changes. One thing worth trying is to see if you can make
|
||
a straight line, and see if the length matches what you'd expect. What if you form a line with the control points on the outside, and the
|
||
start/end points on the inside?
|
||
</p>
|
||
<graphics-element
|
||
title="Arc length for a Bézier curve"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/arclength/arclength.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/arclength/fa4c587126e8097206b88d9ea51974ca.png" loading="lazy" />
|
||
<label>Arc length for a Bézier curve</label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
</section>
|
||
<section id="arclengthapprox">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#arclength">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#curvature">下</a></div>
|
||
<a href="zh-CN/index.html#arclengthapprox">Approximated arc length</a>
|
||
</h1>
|
||
<p>
|
||
Sometimes, we don't actually need the precision of a true arc length, and we can get away with simply computing the approximate arc length
|
||
instead. The by far fastest way to do this is to flatten the curve and then simply calculate the linear distance from point to point. This
|
||
will come with an error, but this can be made arbitrarily small by increasing the segment count.
|
||
</p>
|
||
<p>
|
||
If we combine the work done in the previous sections on curve flattening and arc length computation, we can implement these with minimal
|
||
effort:
|
||
</p>
|
||
<div class="figure">
|
||
<graphics-element
|
||
title="Approximate quadratic curve arc length"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/arclengthapprox/approximate.js"
|
||
data-type="quadratic"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/arclengthapprox/3fc083ea7bdcc6b021560f2f2491f8aa.png" loading="lazy" />
|
||
<label>Approximate quadratic curve arc length</label>
|
||
</fallback-image>
|
||
<input type="range" min="2" max="24" step="1" value="4" class="slide-control" />
|
||
</graphics-element>
|
||
<graphics-element
|
||
title="Approximate cubic curve arc length"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/arclengthapprox/approximate.js"
|
||
data-type="cubic"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/arclengthapprox/537260c4aa9e98ffdea7c8120afbd427.png" loading="lazy" />
|
||
<label>Approximate cubic curve arc length</label>
|
||
</fallback-image>
|
||
<input type="range" min="2" max="32" step="1" value="8" class="slide-control" />
|
||
</graphics-element>
|
||
</div>
|
||
|
||
<p>
|
||
You may notice that even though the error in length is actually pretty significant in absolute terms, even at a low number of segments we
|
||
get a length that agrees with the true length when it comes to just the integer part of the arc length. Quite often, approximations can
|
||
drastically speed things up!
|
||
</p>
|
||
</section>
|
||
<section id="curvature">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#arclengthapprox">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#tracing">下</a></div>
|
||
<a href="zh-CN/index.html#curvature">Curvature of a curve</a>
|
||
</h1>
|
||
<p>
|
||
If we have two curves, and we want to line them in up in a way that "looks right", what would we use as metric to let a computer decide
|
||
what "looks right" means?
|
||
</p>
|
||
<p>
|
||
For instance, we can start by ensuring that the two curves share an end coordinate, so that there is no "gap" between the end of one and
|
||
the start of the next curve, but that won't guarantee that things look right: both curves can be going in wildly different directions, and
|
||
the resulting joined geometry will have a corner in it, rather than a smooth transition from one curve to the next.
|
||
</p>
|
||
<p>
|
||
What we want is to ensure that the <a href="https://en.wikipedia.org/wiki/Curvature">curvature</a> at the transition from one curve to the
|
||
next "looks good". So, we start with a shared coordinate, and then also require that derivatives for both curves match at that coordinate.
|
||
That way, we're assured that their tangents line up, which must mean the curve transition is perfectly smooth. We can even make the
|
||
second, third, etc. derivatives match up for better and better transitions.
|
||
</p>
|
||
<p>Problem solved!</p>
|
||
<p>
|
||
However, there's a problem with this approach: if we think about this a little more, we realise that "what a curve looks like" and its
|
||
derivative values are pretty much entirely unrelated. After all, the section on <a href="#reordering">reordering curves</a> showed us that
|
||
the same looking curve can have an infinite number of curve expressions of arbitrarily high Bézier degree, and each of those will have
|
||
<em>wildly</em> different derivative values.
|
||
</p>
|
||
<p>
|
||
So what we really want is some kind of expression that's not based on any particular expression of <code>t</code>, but is based on
|
||
something that is invariant to the <em>kind</em> of function(s) we use to draw our curve. And the prime candidate for this is our curve
|
||
expression, reparameterised for distance: no matter what order of Bézier curve we use, if we were able to rewrite it as a function of
|
||
distance-along-the-curve, all those different degree Bézier functions would end up being <em>the same</em> function for "coordinate at
|
||
some distance D along the curve".
|
||
</p>
|
||
<p>We've seen this before... that's the arc length function.</p>
|
||
<p>
|
||
So you might think that in order to find the curvature of a curve, we now need to solve the arc length function itself, and that this
|
||
would be quite a problem because we just saw that there is no way to actually do that. Thankfully, we don't. We only need to know the
|
||
<em>form</em> of the arc length function, which we saw above and is fairly simple, rather than needing to <em>solve</em> the arc length
|
||
function. If we start with the arc length expression and the
|
||
<a href="https://mathworld.wolfram.com/Curvature.html">run through the steps necessary</a> to determine <em>its</em> derivative (with an
|
||
alternative, shorter demonstration of how to do this found
|
||
<a href="https://math.stackexchange.com/questions/275248/deriving-curvature-formula/275324#275324">over on Stackexchange</a>), then the
|
||
integral that was giving us so much problems in solving the arc length function disappears entirely (because of the
|
||
<a href="https://en.wikipedia.org/wiki/Fundamental_theorem_of_calculus">fundamental theorem of calculus</a>), and what we're left with us
|
||
some surprisingly simple maths that relates curvature (denoted as κ, "kappa") to—and this is the truly surprising bit—a specific
|
||
combination of derivatives of our original function.
|
||
</p>
|
||
<p>Let me highlight what just happened, because it's pretty special:</p>
|
||
<ol>
|
||
<li>we wanted to make curves line up, and initially thought to match the curves' derivatives, but</li>
|
||
<li>that turned out to be a really bad choice, so instead</li>
|
||
<li>we picked a function that is basically impossible to work with, and then <em>worked with that</em>, which</li>
|
||
<li>gives us a simple formula that is <em>and expression using the curves' derivatives</em>.</li>
|
||
</ol>
|
||
<p><em>That's crazy!</em></p>
|
||
<p>
|
||
But that's also one of the things that makes maths so powerful: even if your initial ideas are off the mark, you might be much closer than
|
||
you thought you were, and the journey from "thinking we're completely wrong" to "actually being remarkably close to being right" is where
|
||
we can find a lot of insight.
|
||
</p>
|
||
<p>So, what does the function look like? This:</p>
|
||
<!--
|
||
|
||
x'y'' - x''y'
|
||
\kappa = ─────────────
|
||
3
|
||
─
|
||
2 2 2
|
||
(x' +y' )
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/curvature/060acd6ff0a050fe4d98a7802a2b3a3f.svg" width="113px" height="47px" loading="lazy" />
|
||
<p>
|
||
Which is really just a "short form" that glosses over the fact that we're dealing with functions of <code>t</code>, so let's expand that a
|
||
tiny bit:
|
||
</p>
|
||
<!--
|
||
|
||
B '(t)B ''(t) - B ''(t)B '(t)
|
||
x y x y
|
||
\kappa(t) = ─────────────────────────────
|
||
3
|
||
─
|
||
2 2 2
|
||
(B '(t) +B '(t) )
|
||
x y
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/curvature/afd8cb8b0fe291ff703752c1c9cc33d4.svg" width="239px" height="55px" loading="lazy" />
|
||
<p>
|
||
And while that's a little more verbose, it's still just as simple to work with as the first function: the curvature at some point on any
|
||
(and this cannot be overstated: <em>any</em>) curve is a ratio between the first and second derivative cross product, and something that
|
||
looks oddly similar to the standard Euclidean distance function. And nothing in these functions is hard to calculate either: for Bézier
|
||
curves, simply knowing our curve coordinates means <a href="#derivatives">we know what the first and second derivatives are</a>, and so
|
||
evaluating this function for any <strong>t</strong> value is just a matter of basic arithematics.
|
||
</p>
|
||
<p>In fact, let's just implement it right now:</p>
|
||
|
||
<table class="code">
|
||
<tr>
|
||
<td>1</td>
|
||
<td rowspan="7">
|
||
<textarea disabled rows="7" role="doc-example">
|
||
function kappa(t, B):
|
||
d = B.getDerivative(t)
|
||
dd = B.getSecondDerivative(t)
|
||
numerator = d.x * dd.y - dd.x * d.y
|
||
denominator = pow(d.x*d.x + d.y*d.y, 3/2)
|
||
if denominator is 0: return NaN;
|
||
return numerator / denominator</textarea
|
||
>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>7</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p>
|
||
That was easy! (Well okay, that "not a number" value will need to be taken into account by downstream code, but that's a reality of
|
||
programming anyway)
|
||
</p>
|
||
<p>
|
||
With all of that covered, let's line up some curves! The following graphic gives you two curves that look identical, but use quadratic and
|
||
cubic functions, respectively. As you can see, despite their derivatives being necessarily different, their curvature (thanks to being
|
||
derived based on maths that "ignores" specific function derivative, and instead gives a formula that smooths out any differences) is
|
||
exactly the same. And because of that, we can put them together such that the point where they overlap has the same curvature for both
|
||
curves, giving us the smoothest transition.
|
||
</p>
|
||
<graphics-element
|
||
title="Matching curvatures for a quadratic and cubic Bézier curve"
|
||
width="825"
|
||
height="275"
|
||
src="./chapters/curvature/curvature.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="825px" height="275px" src="./images/chapters/curvature/4f2647446363ca5d93b11e414fd976df.png" loading="lazy" />
|
||
<label></label> </fallback-image
|
||
></graphics-element>
|
||
<p>
|
||
One thing you may have noticed in this sketch is that sometimes the curvature looks fine, but seems to be pointing in the wrong direction,
|
||
making it hard to line up the curves properly. A way around that, of course, is to show the curvature on both sides of the curve, so let's
|
||
just do that. But let's take it one step further: we can also compute the associated "radius of curvature", which gives us the implicit
|
||
circle that "fits" the curve's curvature at any point, using what is possibly the simplest bit of maths found in this entire primer:
|
||
</p>
|
||
<!--
|
||
|
||
1
|
||
R(t) = ─────────
|
||
\kappa(t)
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/curvature/561ab3a938d655550de0abf458ac2494.svg" width="81px" height="37px" loading="lazy" />
|
||
<p>
|
||
So let's revisit the previous graphic with the curvature visualised on both sides of our curves, as well as showing the circle that "fits"
|
||
our curve at some point that we can control by using a slider:
|
||
</p>
|
||
<graphics-element
|
||
title="(Easier) curvature matching for a quadratic and cubic Bézier curve"
|
||
width="825"
|
||
height="275"
|
||
src="./chapters/curvature/curvature.js"
|
||
data-omni="true"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="825px" height="275px" src="./images/chapters/curvature/392624cedf7c78aed6d4c6065a014b42.png" loading="lazy" />
|
||
<label></label>
|
||
</fallback-image>
|
||
<input type="range" min="0" max="2" step="0.0005" value="0" class="slide-control" />
|
||
</graphics-element>
|
||
</section>
|
||
<section id="tracing">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#curvature">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#intersections">下</a></div>
|
||
<a href="zh-CN/index.html#tracing">Tracing a curve at fixed distance intervals</a>
|
||
</h1>
|
||
<p>
|
||
Say you want to draw a curve with a dashed line, rather than a solid line, or you want to move something along the curve at fixed distance
|
||
intervals over time, like a train along a track, and you want to use Bézier curves.
|
||
</p>
|
||
<p>Now you have a problem.</p>
|
||
<p>
|
||
The reason you have a problem is that Bézier curves are parametric functions with non-linear behaviour, whereas moving a train along a
|
||
track is about as close to a practical example of linear behaviour as you can get. The problem we're faced with is that we can't just pick
|
||
<code>t</code> values at some fixed interval and expect the Bézier functions to generate points that are spaced a fixed distance apart. In
|
||
fact, let's look at the relation between "distance along a curve" and "<code>t</code> value", by plotting them against one another.
|
||
</p>
|
||
<p>
|
||
The following graphic shows a particularly illustrative curve, and its distance-for-t plot. For linear traversal, this line needs to be
|
||
straight, running from (0,0) to (length,1). That is, it's safe to say, not what we'll see: we'll see something very wobbly, instead. To
|
||
make matters even worse, the distance-for-t function is also of a much higher order than our curve is: while the curve we're using for
|
||
this exercise is a cubic curve, which can switch concave/convex form twice at best, the distance function is our old friend the arc length
|
||
function, which can have more inflection points.
|
||
</p>
|
||
<graphics-element
|
||
title="The t-for-distance function"
|
||
width="550"
|
||
height="275"
|
||
src="./chapters/tracing/distance-function.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="550px" height="275px" src="./images/chapters/tracing/d6239520389637a3c42e76ee44d86c41.png" loading="lazy" />
|
||
<label></label> </fallback-image
|
||
></graphics-element>
|
||
<p>
|
||
So, how do we "cut up" the arc length function at regular intervals, when we can't really work with it? We basically cheat: we run through
|
||
the curve using <code>t</code> values, determine the distance-for-this-<code>t</code>-value at each point we generate during the run, and
|
||
then we find "the closest <code>t</code> value that matches some required distance" using those values instead. If we have a low number of
|
||
points sampled, we can then even refine which <code>t</code> value "should" work for our desired distance by interpolating between two
|
||
points, but if we have a high enough number of samples, we don't even need to bother.
|
||
</p>
|
||
<p>
|
||
So let's do exactly that: the following graph is similar to the previous one, showing how we would have to "chop up" our distance-for-t
|
||
curve in order to get regularly spaced points on the curve. It also shows what using those <code>t</code> values on the real curve looks
|
||
like, by coloring each section of curve between two distance markers differently:
|
||
</p>
|
||
<graphics-element
|
||
title="Fixed-interval coloring a curve"
|
||
width="825"
|
||
height="275"
|
||
src="./chapters/tracing/tracing.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="825px" height="275px" src="./images/chapters/tracing/1cd7304fb8d044835bfbc305ca5e5d10.png" loading="lazy" />
|
||
<label></label>
|
||
</fallback-image>
|
||
<input type="range" min="2" max="24" step="1" value="8" class="slide-control" />
|
||
</graphics-element>
|
||
<p>Use the slider to increase or decrease the number of equidistant segments used to colour the curve.</p>
|
||
<p>
|
||
However, are there better ways? One such way is discussed in "<a
|
||
href="https://www.geometrictools.com/Documentation/MovingAlongCurveSpecifiedSpeed.pdf"
|
||
>Moving Along a Curve with Specified Speed</a
|
||
>" by David Eberly of Geometric Tools, LLC, but basically because we have no explicit length function (or rather, one we don't have to
|
||
constantly compute for different intervals), you may simply be better off with a traditional lookup table (LUT).
|
||
</p>
|
||
</section>
|
||
<section id="intersections">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#tracing">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#curveintersection">下</a></div>
|
||
<a href="zh-CN/index.html#intersections">Intersections</a>
|
||
</h1>
|
||
<p>
|
||
Let's look at some more things we will want to do with Bézier curves. Almost immediately after figuring out how to get bounding boxes to
|
||
work, people tend to run into the problem that even though the minimal bounding box (based on rotation) is tight, it's not sufficient to
|
||
perform true collision detection. It's a good first step to make sure there <em>might</em> be a collision (if there is no bounding box
|
||
overlap, there can't be one), but in order to do real collision detection we need to know whether or not there's an intersection on the
|
||
actual curve.
|
||
</p>
|
||
<p>
|
||
We'll do this in steps, because it's a bit of a journey to get to curve/curve intersection checking. First, let's start simple, by
|
||
implementing a line-line intersection checker. While we can solve this the traditional calculus way (determine the functions for both
|
||
lines, then compute the intersection by equating them and solving for two unknowns), linear algebra actually offers a nicer solution.
|
||
</p>
|
||
<h3>Line-line intersections</h3>
|
||
<p>
|
||
If we have two line segments with two coordinates each, segments A-B and C-D, we can find the intersection of the lines these segments are
|
||
an intervals on by linear algebra, using the procedure outlined in this
|
||
<a href="https://www.topcoder.com/community/competitive-programming/tutorials/geometry-concepts-line-intersection-and-its-applications/"
|
||
>top coder</a
|
||
>
|
||
article. Of course, we need to make sure that the intersection isn't just on the lines our line segments lie on, but actually on our line
|
||
segments themselves. So after we find the intersection, we need to verify that it lies without the bounds of our original line segments.
|
||
</p>
|
||
<p>
|
||
The following graphic implements this intersection detection, showing a red point for an intersection on the lines our segments lie on
|
||
(thus being a virtual intersection point), and a green point for an intersection that lies on both segments (being a real intersection
|
||
point).
|
||
</p>
|
||
<graphics-element
|
||
title="Line/line intersections"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/intersections/line-line.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/intersections/b3f61036d8dc9888a6a64a1171583dd1.png" loading="lazy" />
|
||
<label>Line/line intersections</label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
<div class="howtocode">
|
||
<h3>Implementing line-line intersections</h3>
|
||
<p>
|
||
Let's have a look at how to implement a line-line intersection checking function. The basics are covered in the article mentioned above,
|
||
but sometimes you need more function signatures, because you might not want to call your function with eight distinct parameters. Maybe
|
||
you're using point structs for the line. Let's get coding:
|
||
</p>
|
||
|
||
<table class="code">
|
||
<tr>
|
||
<td>1</td>
|
||
<td rowspan="17">
|
||
<textarea disabled rows="17" role="doc-example">
|
||
lli8 = function(x1,y1,x2,y2,x3,y3,x4,y4):
|
||
var nx=(x1*y2-y1*x2)*(x3-x4)-(x1-x2)*(x3*y4-y3*x4),
|
||
ny=(x1*y2-y1*x2)*(y3-y4)-(y1-y2)*(x3*y4-y3*x4),
|
||
d=(x1-x2)*(y3-y4)-(y1-y2)*(x3-x4);
|
||
if d=0:
|
||
return false
|
||
return point(nx/d, ny/d)
|
||
|
||
lli4 = function(p1, p2, p3, p4):
|
||
var x1 = p1.x, y1 = p1.y,
|
||
x2 = p2.x, y2 = p2.y,
|
||
x3 = p3.x, y3 = p3.y,
|
||
x4 = p4.x, y4 = p4.y;
|
||
return lli8(x1,y1,x2,y2,x3,y3,x4,y4)
|
||
|
||
lli = function(line1, line2):
|
||
return lli4(line1.p1, line1.p2, line2.p1, line2.p2)</textarea
|
||
>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>7</td>
|
||
</tr>
|
||
<tr>
|
||
<td>8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>9</td>
|
||
</tr>
|
||
<tr>
|
||
<td>10</td>
|
||
</tr>
|
||
<tr>
|
||
<td>11</td>
|
||
</tr>
|
||
<tr>
|
||
<td>12</td>
|
||
</tr>
|
||
<tr>
|
||
<td>13</td>
|
||
</tr>
|
||
<tr>
|
||
<td>14</td>
|
||
</tr>
|
||
<tr>
|
||
<td>15</td>
|
||
</tr>
|
||
<tr>
|
||
<td>16</td>
|
||
</tr>
|
||
<tr>
|
||
<td>17</td>
|
||
</tr>
|
||
</table>
|
||
</div>
|
||
|
||
<h3>What about curve-line intersections?</h3>
|
||
<p>
|
||
Curve/line intersection is more work, but we've already seen the techniques we need to use in order to perform it: first we
|
||
translate/rotate both the line and curve together, in such a way that the line coincides with the x-axis. This will position the curve in
|
||
a way that makes it cross the line at points where its y-function is zero. By doing this, the problem of finding intersections between a
|
||
curve and a line has now become the problem of performing root finding on our translated/rotated curve, as we already covered in the
|
||
section on finding extremities.
|
||
</p>
|
||
<div class="figure">
|
||
<graphics-element
|
||
title="Quadratic curve/line intersections"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/intersections/curve-line.js"
|
||
data-type="quadratic"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/intersections/9b70fb7b03f082882515e55c0a1eacff.png" loading="lazy" />
|
||
<label>Quadratic curve/line intersections</label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
<graphics-element
|
||
title="Cubic curve/line intersections"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/intersections/curve-line.js"
|
||
data-type="cubic"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/intersections/7196d3dec75d53f5df9d9c832ac3c493.png" loading="lazy" />
|
||
<label>Cubic curve/line intersections</label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
</div>
|
||
|
||
<p>
|
||
Curve/curve intersection, however, is more complicated. Since we have no straight line to align to, we can't simply align one of the
|
||
curves and be left with a simple procedure. Instead, we'll need to apply two techniques we've met before: de Casteljau's algorithm, and
|
||
curve splitting.
|
||
</p>
|
||
</section>
|
||
<section id="curveintersection">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#intersections">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#abc">下</a></div>
|
||
<a href="zh-CN/index.html#curveintersection">Curve/curve intersection</a>
|
||
</h1>
|
||
<p>
|
||
Using de Casteljau's algorithm to split the curve we can now implement curve/curve intersection finding using a "divide and conquer"
|
||
technique:
|
||
</p>
|
||
<ol>
|
||
<li>
|
||
Take two curves <em>C<sub>1</sub></em> and <em>C<sub>2</sub></em
|
||
>, and treat them as a pair.
|
||
</li>
|
||
<li>If their bounding boxes overlap, split up each curve into two sub-curves</li>
|
||
<li>
|
||
With <em>C<sub>1.1</sub></em
|
||
>, <em>C<sub>1.2</sub></em
|
||
>, <em>C<sub>2.1</sub></em> and <em>C<sub>2.2</sub></em
|
||
>, form four new pairs (<em>C<sub>1.1</sub></em
|
||
>,<em>C<sub>2.1</sub></em
|
||
>), (<em>C<sub>1.1</sub></em
|
||
>, <em>C<sub>2.2</sub></em
|
||
>), (<em>C<sub>1.2</sub></em
|
||
>,<em>C<sub>2.1</sub></em
|
||
>), and (<em>C<sub>1.2</sub></em
|
||
>,<em>C<sub>2.2</sub></em
|
||
>).
|
||
</li>
|
||
<li>
|
||
For each pair, check whether their bounding boxes overlap.
|
||
<ol>
|
||
<li>If their bounding boxes do not overlap, discard the pair, as there is no intersection between this pair of curves.</li>
|
||
<li>If there <em>is</em> overlap, rerun all steps for this pair.</li>
|
||
</ol>
|
||
</li>
|
||
<li>
|
||
Once the sub-curves we form are so small that they effectively occupy sub-pixel areas, we consider an intersection found, noting that we
|
||
might have a cluster of multiple intersections at the sub-pixel level, out of which we pick one to act as "found" <code>t</code> value
|
||
(we can either throw all but one away, we can average the cluster's <code>t</code> values, or you can do something even more creative).
|
||
</li>
|
||
</ol>
|
||
<p>
|
||
This algorithm will start with a single pair, "balloon" until it runs in parallel for a large number of potential sub-pairs, and then
|
||
taper back down as it homes in on intersection coordinates, ending up with as many pairs as there are intersections.
|
||
</p>
|
||
<p>
|
||
The following graphic applies this algorithm to a pair of cubic curves, one step at a time, so you can see the algorithm in action. Click
|
||
the button to run a single step in the algorithm, after setting up your curves in some creative arrangement. You can also change the value
|
||
that is used in step 5 to determine whether the curves are small enough. Manipulating the curves or changing the threshold will reset the
|
||
algorithm, so you can try this with lots of different curves.
|
||
</p>
|
||
<p>(can you find the configuration that yields the maximum number of intersections between two cubic curves? Nine intersections!)</p>
|
||
<graphics-element
|
||
title="Curve/curve intersections"
|
||
width="825"
|
||
height="275"
|
||
src="./chapters/curveintersection/curve-curve.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="825px" height="275px" src="./images/chapters/curveintersection/b155682162a5b6da6d40c7b531164a7e.png" loading="lazy" />
|
||
<label></label>
|
||
</fallback-image>
|
||
<button class="next">Advance one step</button>
|
||
<input type="range" min="0.01" max="5" step="0.01" value="1" class="slide-control" />
|
||
</graphics-element>
|
||
<p>
|
||
Finding self-intersections is effectively the same procedure, except that we're starting with a single curve, so we need to turn that into
|
||
two separate curves first. This is trivially achieved by splitting at an inflection point, or if there are none, just splitting at
|
||
<code>t=0.5</code> first, and then running the exact same algorithm as above, with all non-overlapping curve pairs getting removed at each
|
||
iteration, and each successive step homing in on the curve's self-intersection points.
|
||
</p>
|
||
</section>
|
||
<section id="abc">
|
||
<h1>
|
||
<div class="nav">
|
||
<a href="zh-CN/index.html#curveintersection">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#pointcurves">下</a>
|
||
</div>
|
||
<a href="zh-CN/index.html#abc">The projection identity</a>
|
||
</h1>
|
||
<p>
|
||
De Casteljau's algorithm is the pivotal algorithm when it comes to Bézier curves. You can use it not just to split curves, but also to
|
||
draw them efficiently (especially for high-order Bézier curves), as well as to come up with curves based on three points and a tangent.
|
||
Particularly this last thing is really useful because it lets us "mold" a curve, by picking it up at some point, and dragging that point
|
||
around to change the curve's shape.
|
||
</p>
|
||
<p>How does that work? Succinctly: we run de Casteljau's algorithm in reverse!</p>
|
||
<p>
|
||
In order to run de Casteljau's algorithm in reverse, we need a few basic things: a start and end point, a point on the curve that we want
|
||
to be moving around, which has an associated <em>t</em> value, and a point we've not explicitly talked about before, and as far as I know
|
||
has no explicit name, but lives one iteration higher in the de Casteljau process then our on-curve point does. I like to call it "A" for
|
||
reasons that will become obvious.
|
||
</p>
|
||
<p>
|
||
So let's use graphics instead of text to see where this "A" is, because text only gets us so far: move the sliders for the following
|
||
graphics to see what, given a specific <code>t</code> value, our <code>A</code> coordinate is. As well as some other coordinates, which
|
||
taken together let us derive a value that the graphics call "ratio": if you move the curve's points around, A, B, and C will move, what
|
||
happens to that value?
|
||
</p>
|
||
<div class="figure">
|
||
<graphics-element
|
||
inline="{true}"
|
||
title="Projections in a quadratic Bézier curve"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/abc/abc.js"
|
||
data-type="quadratic"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/abc/7a69dd4350ddda5701712e1d3b46b863.png" loading="lazy" />
|
||
<label>Projections in a quadratic Bézier curve</label>
|
||
</fallback-image>
|
||
<input type="range" min="0" max="1" step="0.01" value="0.5" class="slide-control" />
|
||
</graphics-element>
|
||
<graphics-element
|
||
inline="{true}"
|
||
title="Projections in a cubic Bézier curve"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/abc/abc.js"
|
||
data-type="cubic"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/abc/eeec7cf16fb22c666e0143a3a030731f.png" loading="lazy" />
|
||
<label>Projections in a cubic Bézier curve</label>
|
||
</fallback-image>
|
||
<input type="range" min="0" max="1" step="0.01" value="0.5" class="slide-control" />
|
||
</graphics-element>
|
||
</div>
|
||
|
||
<p>So these graphics show us several things:</p>
|
||
<ol>
|
||
<li>a point at the tip of the curve construction's "hat": let's call that <code>A</code>, as well as</li>
|
||
<li>our on-curve point give our chosen <code>t</code> value: let's call that <code>B</code>, and finally,</li>
|
||
<li>
|
||
a point that we get by projecting A, through B, onto the line between the curve's start and end points: let's call that <code>C</code>.
|
||
</li>
|
||
<li>
|
||
for both quadratic and cubic curves, two points <code>e1</code> and <code>e2</code>, which represent the single-to-last step in de
|
||
Casteljau's algorithm: in the last step, we find <code>B</code> at <code>(1-t) * e1 + t * e2</code>.
|
||
</li>
|
||
<li>
|
||
for cubic curves, also the points <code>v1</code> and <code>v2</code>, which together with <code>A</code> represent the first step in de
|
||
Casteljau's algorithm: in the next step, we find <code>e1</code> and <code>e2</code>.
|
||
</li>
|
||
</ol>
|
||
<p>
|
||
These three values A, B, and C allow us to derive an important identity formula for quadratic and cubic Bézier curves: for any point on
|
||
the curve with some <code>t</code> value, the ratio of distances from A to B and B to C is fixed: if some <code>t</code> value sets up a C
|
||
that is 20% away from the start and 80% away from the end, then <em>it doesn't matter where the start, end, or control points are</em>;
|
||
for that <code>t</code> value, <code>C</code> will <em>always</em> lie at 20% from the start and 80% from the end point. Go ahead, pick an
|
||
on-curve point in either graphic and then move all the other points around: if you only move the control points, start and end won't move,
|
||
and so neither will C, and if you move either start or end point, C will move but its relative position will not change.
|
||
</p>
|
||
<p>
|
||
So, how can we compute <code>C</code>? We start with our observation that <code>C</code> always lies somewhere between the start and end
|
||
points, so logically <code>C</code> will have a function that interpolates between those two coordinates:
|
||
</p>
|
||
<!--
|
||
|
||
C = u(t) · P + (1-u(t)) · P
|
||
start end
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/abc/8c6662f605722fb2ff6cd7f65243a126.svg" width="229px" height="16px" loading="lazy" />
|
||
<p>
|
||
If we can figure out what the function <code>u(t)</code> looks like, we'll be done. Although we do need to remember that this
|
||
<code>u(t)</code> will have a different form depending on whether we're working with quadratic or cubic curves.
|
||
<a href="https://mathoverflow.net/questions/122257/finding-the-formula-for-bezier-curve-ratios-hull-point-point-baseline"
|
||
>Running through the maths</a
|
||
>
|
||
(with thanks to Boris Zbarsky) shows us the following two formulae:
|
||
</p>
|
||
<!--
|
||
|
||
2
|
||
(1-t)
|
||
u(t) = ───────────
|
||
quadratic 2 2
|
||
t + (1-t)
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/abc/8e7cfee39c98f2ddf9b635a914066cf6.svg" width="183px" height="41px" loading="lazy" />
|
||
<p>And</p>
|
||
<!--
|
||
|
||
3
|
||
(1-t)
|
||
u(t) = ───────────
|
||
cubic 3 3
|
||
t + (1-t)
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/abc/a0b99054cc82ca1fb147f077e175ef10.svg" width="161px" height="41px" loading="lazy" />
|
||
<p>
|
||
So, if we know the start and end coordinates and the <em>t</em> value, we know C without having to calculate the <code>A</code> or even
|
||
<code>B</code> coordinates. In fact, we can do the same for the ratio function. As another function of <code>t</code>, we technically
|
||
don't need to know what <code>A</code> or <code>B</code> or <code>C</code> are. It, too, can be expressed as a pure function of
|
||
<code>t</code>.
|
||
</p>
|
||
<p>We start by observing that, given <code>A</code>, <code>B</code>, and <code>C</code>, the following always holds:</p>
|
||
<!--
|
||
|
||
distance(B,C)
|
||
ratio(t) = ────────────── = Constant
|
||
distance(A,B)
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/abc/131454dcbac04e567f322979f4af80c6.svg" width="245px" height="39px" loading="lazy" />
|
||
<p>Working out the maths for this, we see the following two formulae for quadratic and cubic curves:</p>
|
||
<!--
|
||
|
||
2 2
|
||
t + (1-t) - 1
|
||
ratio(t) = |───────────────|
|
||
quadratic 2 2
|
||
t + (1-t)
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/abc/5924e162b50272c40c842fad14b8fa48.svg" width="239px" height="41px" loading="lazy" />
|
||
<p>And</p>
|
||
<!--
|
||
|
||
3 3
|
||
t + (1-t) - 1
|
||
ratio(t) = |───────────────|
|
||
cubic 3 3
|
||
t + (1-t)
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/abc/8cd992c1ceaae2e67695285beef23a24.svg" width="219px" height="41px" loading="lazy" />
|
||
<p>
|
||
Which now leaves us with some powerful tools: given three points (start, end, and "some point on the curve"), as well as a
|
||
<code>t</code> value, we can <em>construct</em> curves. We can compute <code>C</code> using the start and end points and our
|
||
<code>u(t)</code> function, and once we have <code>C</code>, we can use our on-curve point (<code>B</code>) and the
|
||
<code>ratio(t)</code> function to find <code>A</code>:
|
||
</p>
|
||
<!--
|
||
|
||
C - B B - C
|
||
A = B - ───────── = B + ─────────
|
||
ratio(t) ratio(t)
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/abc/51a9d0588be822a5c80ea38f7d348641.svg" width="215px" height="37px" loading="lazy" />
|
||
<p>
|
||
With <code>A</code> found, finding <code>e1</code> and <code>e2</code> for quadratic curves is a matter of running the linear
|
||
interpolation with <code>t</code> between start and <code>A</code> to yield <code>e1</code>, and between <code>A</code> and end to yield
|
||
<code>e2</code>. For cubic curves, there is no single pair of points that can act as <code>e1</code> and <code>e2</code> (there are
|
||
infinitely many, because the tangent at B is a free parameter for cubic curves) so as long as the distance ratio between
|
||
<code>e1</code> to <code>B</code> and <code>B</code> to <code>e2</code> is the Bézier ratio <code>(1-t):t</code>, we are free to pick any
|
||
pair, after which we can reverse engineer <code>v1</code> and <code>v2</code>:
|
||
</p>
|
||
<!--
|
||
|
||
╭ e - t · A
|
||
│ 1
|
||
╭ e = (1-t) · v + t · A │ v = ───────────
|
||
╡ 1 1 ==> ╡ 1 1-t
|
||
│ e = (1-t) · A + t · v │ e - (1-t) · A
|
||
╰ 2 2 │ 2
|
||
│ v = ───────────────
|
||
╰ 2 t
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/abc/3166afa345aec1abda432c39b68d39a0.svg" width="339px" height="73px" loading="lazy" />
|
||
<p>And then reverse engineer the curve's control points:</p>
|
||
<!--
|
||
|
||
╭ v - (1-t) · start
|
||
│ 1
|
||
╭ v = (1-t) · start + t · C │ C = ────────────────────
|
||
╡ 1 1 ==> ╡ 1 t
|
||
│ v = (1-t) · C + t · end │ v - t · end
|
||
╰ 2 2 │ 2
|
||
│ C = ──────────────
|
||
╰ 2 1-t
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/abc/8bd3e6fed5bf8d871d30221ae400fd93.svg" width="383px" height="75px" loading="lazy" />
|
||
<p>
|
||
So: if we have a curve's start and end points, as well as some third point B that we want the curve to pass through, then for any
|
||
<code>t</code> value we implicitly know all the ABC values, which (combined with an educated guess on appropriate <code>e1</code> and
|
||
<code>e2</code> coordinates for cubic curves) gives us the necessary information to reconstruct a curve's "de Casteljau skeleton". Which
|
||
means that we can now do several things: we can "fit" curves using only three points, which means we can also "mold" curves by moving an
|
||
on-curve point but leaving its start and end points, and then reconstruct the curve based on where we moved the on-curve point to. These
|
||
are very useful things, and we'll look at both in the next few sections.
|
||
</p>
|
||
</section>
|
||
<section id="pointcurves">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#abc">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#projections">下</a></div>
|
||
<a href="zh-CN/index.html#pointcurves">Creating a curve from three points</a>
|
||
</h1>
|
||
<p>
|
||
Given the preceding section, you might be wondering if we can use that knowledge to just "create" curves by placing some points and having
|
||
the computer do the rest, to which the answer is: that's exactly what we can now do!
|
||
</p>
|
||
<p>
|
||
For quadratic curves, things are pretty easy. Technically, we'll need a <code>t</code> value in order to compute the ratio function used
|
||
in computing the ABC coordinates, but we can just as easily approximate one by treating the distance between the start and
|
||
<code>B</code> point, and <code>B</code> and end point as a ratio, using
|
||
</p>
|
||
<!--
|
||
|
||
╭ d = || Start - B||
|
||
│ 1
|
||
│ d = || End - B||
|
||
│ 2
|
||
╡ d
|
||
│ 1
|
||
│ t= ─────
|
||
│ d +d
|
||
╰ 1 2
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/pointcurves/f8182445c1cd7ae9f368b88fa7090e53.svg"
|
||
width="119px"
|
||
height="84px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
With this code in place, creating a quadratic curve from three points is literally just computing the ABC values, and using
|
||
<code>A</code> as our curve's control point:
|
||
</p>
|
||
<graphics-element
|
||
title="Fitting a quadratic Bézier curve"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/pointcurves/quadratic.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/pointcurves/067a3df30e32708fc0d13f8eb78c0b05.png" loading="lazy" />
|
||
<label>Fitting a quadratic Bézier curve</label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
<p>
|
||
For cubic curves we need to do a little more work, but really only just a little. We're first going to assume that a decent curve through
|
||
the three points should approximate a circular arc, which first requires knowing how to fit a circle to three points. You may remember (if
|
||
you ever learned it!) that a line between two points on a circle is called a
|
||
<a href="https://en.wikipedia.org/wiki/Chord_%28geometry%29">chord</a>, and that one property of chords is that the line from the center
|
||
of any chord, perpendicular to that chord, passes through the center of the circle.
|
||
</p>
|
||
<p>
|
||
That means that if we have three points on a circle, we have three (different) chords, and consequently, three (different) lines that go
|
||
from those chords through the center of the circle: if we find two of those lines, then their intersection will be our circle's center,
|
||
and the circle's radius will—by definition!—be the distance from the center to any of our three points:
|
||
</p>
|
||
<graphics-element
|
||
title="Finding a circle through three points"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/pointcurves/circle.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/pointcurves/43875f6ad588bfd04cdb65b591a62052.png" loading="lazy" />
|
||
<label>Finding a circle through three points</label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
<p>
|
||
With that covered, we now also know the tangent line to our point <code>B</code>, because the tangent to any point on the circle is a line
|
||
through that point, perpendicular to the line from that point to the center. That just leaves marking appropriate points
|
||
<code>e1</code> and <code>e2</code> on that tangent, so that we can construct a new cubic curve hull. We use the approach as we did for
|
||
quadratic curves to automatically determine a reasonable <code>t</code> value, and then our <code>e1</code> and
|
||
<code>e2</code> coordinates must obey the standard de Casteljau rule for linear interpolation:
|
||
</p>
|
||
<!--
|
||
|
||
╭ e = B + t · d
|
||
╡ 1
|
||
│ e = B - (1-t) · d
|
||
╰ 2
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/pointcurves/8ffdd4a58cbd0fc24caef781f23a7950.svg"
|
||
width="139px"
|
||
height="40px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
Where <code>d</code> is the total length of the line segment from <code>e1</code> to <code>e2</code>. So how long do we make that? There
|
||
are again all kinds of approaches we can take, and a simple-but-effective one is to set the length of that segment to "one third the
|
||
length of the baseline". This forces <code>e1</code> and <code>e2</code> to always be the "linear curve" distance apart, which means if we
|
||
place our three points on a line, it will actually <em>look</em> like a line. Nice! The last thing we'll need to do is make sure to flip
|
||
the sign of <code>d</code> depending on which side of the baseline our <code>B</code> is located, so we don't end up creating a funky
|
||
curve with a loop in it. To do this, we can use the <a href="https://en.wikipedia.org/wiki/Atan2">atan2</a> function:
|
||
</p>
|
||
<!--
|
||
|
||
\phi = (atan2(E -S , E -S ) - atan2(B -S , B -S ) + 2 \pi) mod 2 \pi
|
||
y y x x y y x x
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/pointcurves/a5cd63b54be6b554290c38787cfbbabd.svg"
|
||
width="488px"
|
||
height="24px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
This angle φ will be between 0 and π if <code>B</code> is "above" the baseline (rotating all three points so that the start is on the left
|
||
and the end is the right), so we can use a relatively straight forward check to make sure we're using the correct sign for our value
|
||
<code>d</code>:
|
||
</p>
|
||
<!--
|
||
|
||
d = { d if 0 ≤\phi ≤\pi
|
||
-d if \phi < 0 \lor \phi > \pi
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/pointcurves/4fe687c8a65265a2a755ba5841d0e31d.svg"
|
||
width="177px"
|
||
height="40px"
|
||
loading="lazy"
|
||
/>
|
||
<p>The result of this approach looks as follows:</p>
|
||
<graphics-element
|
||
title="Finding the cubic e₁ and e₂ given three points "
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/pointcurves/circle.js"
|
||
data-show-curve="true"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/pointcurves/75f7b5b31e98444e13f17e5c3e5b7322.png" loading="lazy" />
|
||
<label>Finding the cubic e₁ and e₂ given three points </label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
<p>
|
||
It is important to remember that even though we're using a circular arc to come up with decent <code>e1</code> and <code>e2</code> terms,
|
||
we're <em>not</em> trying to perfectly create a circular arc with a cubic curve (which is good, because we can't;
|
||
<a href="#arcapproximation">more on that later</a>), we're <em>only</em> trying to come up with some reasonable <code>e1</code> and
|
||
<code>e2</code> points so we can construct a new cubic curve... so now that we have those: let's see what kind of cubic curve that gives
|
||
us:
|
||
</p>
|
||
<graphics-element
|
||
title="Fitting a cubic Bézier curve"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/pointcurves/cubic.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/pointcurves/eab6ea46fa93030e03ec0ef7deb571dc.png" loading="lazy" />
|
||
<label>Fitting a cubic Bézier curve</label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
<p>That looks perfectly serviceable!</p>
|
||
<p>
|
||
Of course, we can take this one step further: we can't just "create" curves, we also have (almost!) all the tools available to "mold"
|
||
curves, where we can reshape a curve by dragging a point on the curve around while leaving the start and end fixed, effectively molding
|
||
the shape as if it were clay or the like. We'll see the last tool we need to do that in the next section, and then we'll look at
|
||
implementing curve molding in the section after that, so read on!
|
||
</p>
|
||
</section>
|
||
<section id="projections">
|
||
<h1>
|
||
<div class="nav">
|
||
<a href="zh-CN/index.html#pointcurves">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#circleintersection">下</a>
|
||
</div>
|
||
<a href="zh-CN/index.html#projections">Projecting a point onto a Bézier curve</a>
|
||
</h1>
|
||
<p>
|
||
Before we can move on to actual curve molding, it'll be good if know how to actually be able to find "some point on the curve" that we're
|
||
trying to click on. After all, if all we have is our Bézier coordinates, that is not in itself enough to figure out which point on the
|
||
curve our cursor will be closest to. So, how do we project points onto a curve?
|
||
</p>
|
||
<p>
|
||
If the Bézier curve is of low enough order, we might be able to
|
||
<a href="https://web.archive.org/web/20140713004709/http://jazzros.blogspot.com/2011/03/projecting-point-on-bezier-curve.html"
|
||
>work out the maths for how to do this</a
|
||
>, and get a perfect <code>t</code> value back, but in general this is an incredibly hard problem and the easiest solution is, really, a
|
||
numerical approach again. We'll be finding our ideal <code>t</code> value using a
|
||
<a href="https://en.wikipedia.org/wiki/Binary_search_algorithm">binary search</a>. First, we do a coarse distance-check based on
|
||
<code>t</code> values associated with the curve's "to draw" coordinates (using a lookup table, or LUT). This is pretty fast:
|
||
</p>
|
||
|
||
<table class="code">
|
||
<tr>
|
||
<td>1</td>
|
||
<td rowspan="8">
|
||
<textarea disabled rows="8" role="doc-example">
|
||
p = some point to project onto the curve
|
||
d = some initially huge value
|
||
i = 0
|
||
for (coordinate, index) in LUT:
|
||
q = distance(coordinate, p)
|
||
if q < d:
|
||
d = q
|
||
i = index</textarea
|
||
>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>7</td>
|
||
</tr>
|
||
<tr>
|
||
<td>8</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p>
|
||
After this runs, we know that <code>LUT[i]</code> is the coordinate on the curve <em>in our LUT</em> that is closest to the point we want
|
||
to project, so that's a pretty good initial guess as to what the best projection onto our curve is. To refine it, we note that
|
||
<code>LUT[i]</code> is a better guess than both <code>LUT[i-1]</code> and <code>LUT[i+1]</code>, but there might be an even better
|
||
projection <em>somewhere else</em> between those two values, so that's what we're going to be testing for, using a variation of the binary
|
||
search.
|
||
</p>
|
||
<ol>
|
||
<li>
|
||
we start with our point <code>p</code>, and the <code>t</code> values <code>t1=LUT[i-1].t</code> and <code>t2=LUT[i+1].t</code>, which
|
||
span an interval <code>v = t2-t1</code>.
|
||
</li>
|
||
<li>
|
||
we test this interval in five spots: the start, middle, and end (which we already have), and the two points in between the middle and
|
||
start/end points
|
||
</li>
|
||
<li>
|
||
we then check which of these five points is the closest to our original point <code>p</code>, and then repeat step 1 with the points
|
||
before and after the closest point we just found.
|
||
</li>
|
||
</ol>
|
||
<p>
|
||
This makes the interval we check smaller and smaller at each iteration, and we can keep running the three steps until the interval becomes
|
||
so small as to lead to distances that are, for all intents and purposes, the same for all points.
|
||
</p>
|
||
<p>
|
||
So, let's see that in action: in this case, I'm going to arbitrarily say that if we're going to run the loop until the interval is smaller
|
||
than 0.001, and show you what that means for projecting your mouse cursor or finger tip onto a rather complex Bézier curve (which, of
|
||
course, you can reshape as you like). Also shown are the original three points that our coarse check finds.
|
||
</p>
|
||
<graphics-element
|
||
title="Projecting a point onto a Bézier curve"
|
||
width="400"
|
||
height="400"
|
||
src="./chapters/projections/project.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="400px" height="400px" src="./images/chapters/projections/536a94885dc1637c066d0ef4f6e4e650.png" loading="lazy" />
|
||
<label></label> </fallback-image
|
||
></graphics-element>
|
||
</section>
|
||
<section id="circleintersection">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#projections">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#molding">下</a></div>
|
||
<a href="zh-CN/index.html#circleintersection">Intersections with a circle</a>
|
||
</h1>
|
||
<p>
|
||
It might seem odd to cover this subject so much later than the line/line, line/curve, and curve/curve intersection topics from several
|
||
sections earlier, but the reason we can't cover circle/curve intersections is that we can't really discuss circle/curve intersection until
|
||
we've covered the kind of lookup table (LUT) walking that the section on projecting a point onto a curve uses. To see why, let's look at
|
||
what we would have to do if we wanted to find the intersections between a curve and a circle using calculus.
|
||
</p>
|
||
<p>
|
||
First, we observe that "finding intersections" in this case means that, given a circle defined by a center point
|
||
<code>c = (x,y)</code> and a radius <code>r</code>, we want to find all points on the Bezier curve for which the distance to the circle's
|
||
center point is equal to the circle radius, which by definition means those points lie on the circle, and so count as intersections. In
|
||
maths, that means we're trying to solve:
|
||
</p>
|
||
<!--
|
||
|
||
dist(B(t), c) = r
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/circleintersection/373248ec6a579bacf6c6a317e6db597a.svg"
|
||
width="107px"
|
||
height="16px"
|
||
loading="lazy"
|
||
/>
|
||
<p>Which seems simple enough. Unfortunately, when we expand that <code>dist</code> function, things get a lot more problematic:</p>
|
||
<!--
|
||
|
||
r= dist(B(t), c)
|
||
┌─────────────────────────┐
|
||
│ 2 2
|
||
= │(B t - c ) + (B t - c )
|
||
⟍│ x x y y
|
||
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||
│╭ 3 2 2 3 ╮2 ╭ 3 2 2 3 ╮2
|
||
= ││ x (1-t) + 3 x (1-t) t + 2 x (1-t) t + x t - c │ + │ y (1-t) + 3 y (1-t) t + 2 y (1-t) t + y t - c │
|
||
⟍│╰ 1 2 3 4 x ╯ ╰ 1 2 3 4 y ╯
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/circleintersection/2f42c862a0a9d0764727d42b16cf68a0.svg"
|
||
width="811px"
|
||
height="103px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
And now we have a problem because that's a sixth degree polynomial inside the square root. So, thanks to the
|
||
<a href="https://en.wikipedia.org/wiki/Abel%E2%80%93Ruffini_theorem">Abel-Ruffini theorem</a> that we saw before, we can't solve this by
|
||
just going "square both sides because we don't care about signs"... we can't solve a sixth degree polynomial. So, we're going to have to
|
||
actually evaluate that expression. We can "simplify" this by translating all our coordinates so that the center of the circle is (0,0) and
|
||
all our coordinates are shifted accordingly, which makes the c<sub>x</sub> and c<sub>y</sub> terms fall away, but then we're still left
|
||
with a monstrous function to solve.
|
||
</p>
|
||
<p>
|
||
So instead, we turn to the same kind of "LUT walking" that we saw for projecting points onto a curve, with a twist: instead of finding the
|
||
on-curve point with the smallest distance to our projection point, we want to find the on-curve point that has the exact distance
|
||
<code>r</code> to our projection point (namely, our circle center). Of course, there can be more than one such point, so there's also a
|
||
bit more code to make sure we find all of them, but let's look at the steps involved:
|
||
</p>
|
||
|
||
<table class="code">
|
||
<tr>
|
||
<td>1</td>
|
||
<td rowspan="9">
|
||
<textarea disabled rows="9" role="doc-example">
|
||
p = our circle's center point
|
||
r = our circle's radius
|
||
d = some initially huge value
|
||
i = 0
|
||
for (coordinate, index) in LUT:
|
||
q = abs(distance(coordinate, p) - r)
|
||
if q < d:
|
||
d = q
|
||
i = index</textarea
|
||
>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>7</td>
|
||
</tr>
|
||
<tr>
|
||
<td>8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>9</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p>
|
||
This is <em>very</em> similar to the code in the previous section, with an extra input <code>r</code> for the circle radius, and a minor
|
||
change in the "distance for this coordinate": rather than just <code>distance(coordinate, p)</code> we want to know the difference between
|
||
that distance and the circle radius. After all, if that difference is zero, then the distance from the coordinate to the circle center is
|
||
exactly the radius, so the coordinate lies on both the curve and the circle.
|
||
</p>
|
||
<p>So far so good.</p>
|
||
<p>However, we also want to make sure we find <em>all</em> the points, not just a single one, so we need a little more code for that:</p>
|
||
|
||
<table class="code">
|
||
<tr>
|
||
<td>1</td>
|
||
<td rowspan="10">
|
||
<textarea disabled rows="10" role="doc-example">
|
||
p = our circle's center point
|
||
r = our circle's radius
|
||
d = some initially huge value
|
||
start = 0
|
||
values = []
|
||
do:
|
||
i = findClosest(start, p, r, LUT)
|
||
if i < start, or i>0 but the same as start: stop
|
||
values.add(i);
|
||
start = i + 2;</textarea
|
||
>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>7</td>
|
||
</tr>
|
||
<tr>
|
||
<td>8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>9</td>
|
||
</tr>
|
||
<tr>
|
||
<td>10</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p>
|
||
After running this code, <code>values</code> will be the list of all LUT coordinates that are closest to the distance <code>r</code>: we
|
||
can use those values to run the same kind of refinement lookup we used for point projection (with the caveat that we're now
|
||
<em>not</em> checking for smallest distance, but for "distance closest to <code>r</code>"), and we'll have all our intersection points. Of
|
||
course, that does require explaining what <code>findClosest</code> does: rather than looking for a global minimum, we're now interested in
|
||
finding a <em>local</em> minimum, so instead of checking a single point and looking at its distance value, we check <em>three</em> points
|
||
("current", "previous" and "before previous") and then check whether they form a local minimum:
|
||
</p>
|
||
|
||
<table class="code">
|
||
<tr>
|
||
<td>1</td>
|
||
<td rowspan="19">
|
||
<textarea disabled rows="19" role="doc-example">
|
||
findClosest(start, p, r, LUT):
|
||
minimizedDistance = some very large number
|
||
pd2 = LUT[start-2], if it exists. Otherwise use minimizedDistance
|
||
pd1 = LUT[start-1], if it exists. Otherwise use minimizedDistance
|
||
slice = LUT.subset(start, LUT.length)
|
||
epsilon = the largest point-to-point distance in our LUT
|
||
i = -1;
|
||
|
||
for (coordinate, index) in slice:
|
||
q = abs(dist(coordinate, p) - r);
|
||
if pd1 less than all three values epsilon, pd2, and q:
|
||
i = index - 1
|
||
break
|
||
|
||
minimizedDistance = min(q, minimizedDistance)
|
||
pd2 = pd1
|
||
pd1 = q
|
||
|
||
return start + i</textarea
|
||
>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>7</td>
|
||
</tr>
|
||
<tr>
|
||
<td>8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>9</td>
|
||
</tr>
|
||
<tr>
|
||
<td>10</td>
|
||
</tr>
|
||
<tr>
|
||
<td>11</td>
|
||
</tr>
|
||
<tr>
|
||
<td>12</td>
|
||
</tr>
|
||
<tr>
|
||
<td>13</td>
|
||
</tr>
|
||
<tr>
|
||
<td>14</td>
|
||
</tr>
|
||
<tr>
|
||
<td>15</td>
|
||
</tr>
|
||
<tr>
|
||
<td>16</td>
|
||
</tr>
|
||
<tr>
|
||
<td>17</td>
|
||
</tr>
|
||
<tr>
|
||
<td>18</td>
|
||
</tr>
|
||
<tr>
|
||
<td>19</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p>
|
||
In words: given a <code>start</code> index, the circle center and radius, and our LUT, we check where (closest to our
|
||
<code>start</code> index) we can find a local minimum for the difference between "the distance from the curve to the circle center", and
|
||
the circle's radius. We track this by looking at three values (associated with the indices <code>index-2</code>, <code>index-1</code>, and
|
||
<code>index</code>), and we know we've found a local minimum if the three values show that the middle value (<code>pd1</code>) is less
|
||
than either value beside it. When we do, we can set our "best guess, relative to <code>start</code>" as <code>index-1</code>. Of course,
|
||
since we're now checking values relative to some <code>start</code> value, we might not find another candidate value at all, in which case
|
||
we return <code>start - 1</code>, so that a simple "is the result less than <code>start</code>?" lets us determine that there are no more
|
||
intersections to find.
|
||
</p>
|
||
<p>
|
||
Finally, while not necessary for point projection, there is one more step we need to perform when we run the binary refinement function on
|
||
our candidate LUT indices, because we've so far only been testing using distances "closest to the radius of the circle", and that's
|
||
actually not good enough... we need distances that <em>are</em> the radius of the circle. So, after running the refinement for each of
|
||
these indices, we need to discard any final value that isn't the circle radius. And because we're working with floating point numbers,
|
||
what this really means is that we need to discard any value that's a pixel or more "off". Or, if we want to get really fancy, "some small
|
||
<code>epsilon</code> value".
|
||
</p>
|
||
<p>
|
||
Based on all of that, the following graphic shows this off for the standard cubic curve (which you can move the coordinates around for, of
|
||
course) and a circle with a controllable radius centered on the graphic's center, using the code approach described above.
|
||
</p>
|
||
<graphics-element
|
||
title="circle intersection"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/circleintersection/circle.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/circleintersection/3a3ea30971de247da93034de614a63a4.png" loading="lazy" />
|
||
<label>circle intersection</label>
|
||
</fallback-image>
|
||
<input type="range" min="1" max="150" step="1" value="70" class="slide-control" />
|
||
</graphics-element>
|
||
<p>And of course, for the full details, click that "view source" link.</p>
|
||
</section>
|
||
<section id="molding">
|
||
<h1>
|
||
<div class="nav">
|
||
<a href="zh-CN/index.html#circleintersection">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#curvefitting">下</a>
|
||
</div>
|
||
<a href="zh-CN/index.html#molding">Molding a curve</a>
|
||
</h1>
|
||
<p>
|
||
Armed with knowledge of the "ABC" relation, point-on-curve projection, and guestimating reasonable looking helper values for cubic curve
|
||
construction, we can finally cover curve molding: updating a curve's shape interactively, by dragging points on the curve around.
|
||
</p>
|
||
<p>
|
||
For quadratic curve, this is a really simple trick: we project our cursor onto the curve, which gives us a <code>t</code> value and
|
||
initial <code>B</code> coordinate. We don't even need the latter: with our <code>t</code> value and "wherever the cursor is" as target
|
||
<code>B</code>, we can compute the associated <code>C</code>:
|
||
</p>
|
||
<!--
|
||
|
||
C = u(t) · Start + (1-u(t) ) · End
|
||
q q
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/molding/48887d68a861a0acdf8313e23fb19880.svg" width="247px" height="24px" loading="lazy" />
|
||
<p>And then the associated <code>A</code>:</p>
|
||
<!--
|
||
|
||
C - B B - C
|
||
A = B - ────────── = B + ──────────
|
||
ratio(t) ratio(t)
|
||
q q
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/molding/2e65bc9c934380c2de6a24bcd5c1c7b7.svg" width="228px" height="41px" loading="lazy" />
|
||
<p>And we're done, because that's our new quadratic control point!</p>
|
||
<graphics-element
|
||
title="Molding a quadratic Bézier curve"
|
||
width="825"
|
||
height="275"
|
||
src="./chapters/molding/molding.js"
|
||
data-type="quadratic"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="825px" height="275px" src="./images/chapters/molding/2bd215d1db191b52a89a94727b6aa5ce.png" loading="lazy" />
|
||
<label></label> </fallback-image
|
||
></graphics-element>
|
||
<p>
|
||
As before, cubic curves are a bit more work, because while it's easy to find our initial <code>t</code> value and ABC values, getting
|
||
those all-important <code>e1</code> and <code>e2</code> coordinates is going to pose a bit of a problem... in the section on curve
|
||
creation, we were free to pick an appropriate <code>t</code> value ourselves, which allowed us to find appropriate <code>e1</code> and
|
||
<code>e2</code> coordinates. That's great, but when we're curve molding we don't have that luxury: whatever point we decide to start
|
||
moving around already has its own <code>t</code> value, and its own <code>e1</code> and <code>e2</code> values, and those may not make
|
||
sense for the rest of the curve.
|
||
</p>
|
||
<p>
|
||
For example, let's see what happens if we just "go with what we get" when we pick a point and start moving it around, preserving its
|
||
<code>t</code> value and <code>e1</code>/<code>e2</code> coordinates:
|
||
</p>
|
||
<graphics-element
|
||
title="Molding a cubic Bézier curve"
|
||
width="825"
|
||
height="275"
|
||
src="./chapters/molding/molding.js"
|
||
data-type="cubic"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="825px" height="275px" src="./images/chapters/molding/a54f990aa49fb9ea8200b59259f955f3.png" loading="lazy" />
|
||
<label></label> </fallback-image
|
||
></graphics-element>
|
||
<p>
|
||
That looks reasonable, close to the original point, but the further we drag our point, the less "useful" things become. Especially if we
|
||
drag our point across the baseline, rather than turning into a nice curve.
|
||
</p>
|
||
<p>
|
||
One way to combat this might be to combine the above approach with the approach from the
|
||
<a href="#pointcurves">creating curves</a> section: generate both the "unchanged <code>t</code>/<code>e1</code>/<code>e2</code>" curve, as
|
||
well as the "idealized" curve through the start/cursor/end points, with idealized <code>t</code> value, and then interpolating between
|
||
those two curves:
|
||
</p>
|
||
<graphics-element
|
||
title="Molding a cubic Bézier curve"
|
||
width="825"
|
||
height="275"
|
||
src="./chapters/molding/molding.js"
|
||
data-type="cubic"
|
||
data-interpolated="true"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="825px" height="275px" src="./images/chapters/molding/1039d0bb0e49cfb472c2fa37f9010190.png" loading="lazy" />
|
||
<label></label>
|
||
</fallback-image>
|
||
<input type="range" min="10" max="200" step="1" value="100" class="slide-control" />
|
||
</graphics-element>
|
||
<p>
|
||
The slide controls the "falloff distance" relative to where the original point on the curve is, so that as we drag our point around, it
|
||
interpolates with a bias towards "preserving <code>t</code>/<code>e1</code>/<code>e2</code>" closer to the original point, and bias
|
||
towards "idealized" form the further away we move our point, with anything that's further than our falloff distance simply
|
||
<em>being</em> the idealized curve. We don't even try to interpolate at that point.
|
||
</p>
|
||
<p>
|
||
A more advanced way to try to smooth things out is to implement <em>continuous</em> molding, where we constantly update the curve as we
|
||
move around, and constantly change what our <code>B</code> point is, based on constantly projecting the cursor on the curve
|
||
<em>as we're updating it</em> - this is, you won't be surprised to learn, tricky, and beyond the scope of this section: interpolation
|
||
(with a reasonable distance) will do for now!
|
||
</p>
|
||
</section>
|
||
<section id="curvefitting">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#molding">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#catmullconv">下</a></div>
|
||
<a href="zh-CN/index.html#curvefitting">Curve fitting</a>
|
||
</h1>
|
||
<p>
|
||
Given the previous section, one question you might have is "what if I don't want to guess <code>t</code> values?". After all, plenty of
|
||
graphics packages do automated curve fitting, so how can we implement that in a way that just finds us reasonable <code>t</code> values
|
||
all on its own?
|
||
</p>
|
||
<p>
|
||
And really this is just a variation on the question "how do I get the curve through these X points?", so let's look at that. Specifically,
|
||
let's look at the answer: "curve fitting". This is in fact a rather rich field in geometry, applying to anything from data modelling to
|
||
path abstraction to "drawing", so there's a fair number of ways to do curve fitting, but we'll look at one of the most common approaches:
|
||
something called a <a href="https://en.wikipedia.org/wiki/Least_squares">least squares</a>
|
||
<a href="https://en.wikipedia.org/wiki/Polynomial_regression">polynomial regression</a>. In this approach, we look at the number of points
|
||
we have in our data set, roughly determine what would be an appropriate order for a curve that would fit these points, and then tackle the
|
||
question "given that we want an <code>nth</code> order curve, what are the coordinates we can find such that our curve is "off" by the
|
||
least amount?".
|
||
</p>
|
||
<p>
|
||
Now, there are many ways to determine how "off" points are from the curve, which is where that "least squares" term comes in. The most
|
||
common tool in the toolbox is to minimise the <em>squared distance</em> between each point we have, and the corresponding point on the
|
||
curve we end up "inventing". A curve with a snug fit will have zero distance between those two, and a bad fit will have non-zero distances
|
||
between every such pair. It's a workable metric. You might wonder why we'd need to square, rather than just ensure that distance is a
|
||
positive value (so that the total error is easy to compute by just summing distances) and the answer really is "because it tends to be a
|
||
little better". There's lots of literature on the web if you want to deep-dive the specific merits of least squared error metrics versus
|
||
least absolute error metrics, but those are <em>well</em> beyond the scope of this material.
|
||
</p>
|
||
<p>
|
||
So let's look at what we end up with in terms of curve fitting if we start with the idea of performing least squares Bézier fitting. We're
|
||
going to follow a procedure similar to the one described by Jim Herold over on his
|
||
<a href="https://web.archive.org/web/20180403213813/http://jimherold.com/2012/04/20/least-squares-bezier-fit/"
|
||
>"Least Squares Bézier Fit"</a
|
||
>
|
||
article, and end with some nice interactive graphics for doing some curve fitting.
|
||
</p>
|
||
<p>
|
||
Before we begin, we're going to use the curve in matrix form. In the <a href="#matrix">section on matrices</a>, I mentioned that some
|
||
things are easier if we use the matrix representation of a Bézier curve rather than its calculus form, and this is one of those things.
|
||
</p>
|
||
<p>
|
||
As such, the first step in the process is expressing our Bézier curve as powers/coefficients/coordinate matrix <strong>T x M x C</strong>,
|
||
by expanding the Bézier functions.
|
||
</p>
|
||
<div class="note">
|
||
<h2>Revisiting the matrix representation</h2>
|
||
<p>
|
||
Rewriting Bézier functions to matrix form is fairly easy, if you first expand the function, and then arrange them into a multiple line
|
||
form, where each line corresponds to a power of t, and each column is for a specific coefficient. First, we expand the function:
|
||
</p>
|
||
<!--
|
||
|
||
2 2
|
||
B = a (1-t) + 2 b (1-t) t + c t
|
||
quadratic
|
||
2 2 2
|
||
= a - 2at + at + 2bt - 2bt + ct
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/curvefitting/dd303afb51d580fb2bf1b914c010f83d.svg"
|
||
width="287px"
|
||
height="43px"
|
||
loading="lazy"
|
||
/>
|
||
<p>And then we (trivially) rearrange the terms across multiple lines:</p>
|
||
<!--
|
||
|
||
B =a
|
||
quadratic
|
||
- 2at+ 2bt
|
||
2 2 2
|
||
+ at - 2bt + ct
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/curvefitting/ff701138fd7a6e35700a2e1ee3e9c020.svg"
|
||
width="209px"
|
||
height="64px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
This rearrangement has "factors of t" at each row (the first row is t⁰, i.e. "1", the second row is t¹, i.e. "t", the third row is t²)
|
||
and "coefficient" at each column (the first column is all terms involving "a", the second all terms involving "b", the third all terms
|
||
involving "c").
|
||
</p>
|
||
<p>With that arrangement, we can easily decompose this as a matrix multiplication:</p>
|
||
<!--
|
||
|
||
┌ a + 0 + 0 ┐ ┌ 1 0 0 ┐ ┌ a ┐
|
||
B = T · M · C = ┌ 2 ┐ · │ -2a + 2b + 0 │ = ┌ 2 ┐ · │ -2 2 0 │ · │ b │
|
||
quadratic └ 1 t t ┘ └ a + -2b + c ┘ └ 1 t t ┘ └ 1 -2 1 ┘ └ c ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/curvefitting/db85a7a46b869c892662c26b6aea15a1.svg"
|
||
width="616px"
|
||
height="53px"
|
||
loading="lazy"
|
||
/>
|
||
<p>We can do the same for the cubic curve, of course. We know the base function for cubics:</p>
|
||
<!--
|
||
|
||
3 2 2 3
|
||
B =a(1-t) + 3b(1-t) t + 3c(1-t)t + dt
|
||
cubic
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/curvefitting/17d5fbeffcdcceca98cdba537295d258.svg"
|
||
width="351px"
|
||
height="19px"
|
||
loading="lazy"
|
||
/>
|
||
<p>So we write out the expansion and rearrange:</p>
|
||
<!--
|
||
|
||
B =a
|
||
cubic
|
||
- 3at + 3bt
|
||
2 2 2
|
||
+ 3at - 6bt +3ct
|
||
3 3 3 3
|
||
- at + 3bt -3ct + dt
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/curvefitting/8928f757abd1376abdc4069e1aa774f2.svg"
|
||
width="244px"
|
||
height="87px"
|
||
loading="lazy"
|
||
/>
|
||
<p>Which we can then decompose:</p>
|
||
<!--
|
||
|
||
┌ 1 0 0 0 ┐ ┌ a ┐
|
||
B = T · M · C = ┌ 2 3 ┐ · │ -3 3 0 0 │ · │ b │
|
||
cubic └ 1 t t t ┘ │ 3 -6 3 0 │ │ c │
|
||
└ -1 3 -3 1 ┘ └ d ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/curvefitting/4e6e20c823c8cc72e0cc00e4ab5b7556.svg"
|
||
width="401px"
|
||
height="72px"
|
||
loading="lazy"
|
||
/>
|
||
<p>And, of course, we can do this for quartic curves too (skipping the expansion step):</p>
|
||
<!--
|
||
|
||
┌ 1 0 0 0 0 ┐ ┌ a ┐
|
||
│ -4 4 0 0 0 │ │ b │
|
||
B = T · M · C = ┌ 2 3 4 ┐ · │ 6 -12 6 0 0 │ · │ c │
|
||
quartic └ 1 t t t t ┘ │ -4 12 -12 4 0 │ │ d │
|
||
└ 1 -4 6 -4 1 ┘ └ e ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/curvefitting/8a66af7570bac674966f6316820ea31b.svg"
|
||
width="485px"
|
||
height="92px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
And so and on so on. Now, let's see how to use these <strong>T</strong>, <strong>M</strong>, and <strong>C</strong>, to do some curve
|
||
fitting.
|
||
</p>
|
||
</div>
|
||
|
||
<p>
|
||
Let's get started: we're going to assume we picked the right order curve: for <code>n</code> points we're fitting an <code>n-1</code
|
||
><sup>th</sup> order curve, so we "start" with a vector <strong>P</strong> that represents the coordinates we already know, and for which
|
||
we want to do curve fitting:
|
||
</p>
|
||
<!--
|
||
|
||
┌ p ┐
|
||
│ 1 │
|
||
│ p │
|
||
P = │ 2 │
|
||
│ ... │
|
||
│ p │
|
||
└ n ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/curvefitting/b017da988c9a778a4ce6a6f4ea4790d4.svg"
|
||
width="63px"
|
||
height="73px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
Next, we need to figure out appropriate <code>t</code> values for each point in the curve, because we need something that lets us tie "the
|
||
actual coordinate" to "some point on the curve". There's a fair number of different ways to do this (and a large part of optimizing "the
|
||
perfect fit" is about picking appropriate <code>t</code> values), but in this case let's look at two "obvious" choices:
|
||
</p>
|
||
<ol>
|
||
<li>equally spaced <code>t</code> values, and</li>
|
||
<li><code>t</code> values that align with distance along the polygon.</li>
|
||
</ol>
|
||
<p>
|
||
The first one is really simple: if we have <code>n</code> points, then we'll just assign each point <code>i</code> a <code>t</code> value
|
||
of <code>(i-1)/(n-1)</code>. So if we have four points, the first point will have <code>t=(1-1)/(4-1)=0/3</code>, the second point will
|
||
have <code>t=(2-1)/(4-1)=1/3</code>, the third point will have <code>t=2/3</code>, and the last point will be <code>t=1</code>. We're just
|
||
straight up spacing the <code>t</code> values to match the number of points we have.
|
||
</p>
|
||
<p>
|
||
The second one is a little more interesting: since we're doing polynomial regression, we might as well exploit the fact that our base
|
||
coordinates just constitute a collection of line segments. At the first point, we're fixing t=0, and the last point, we want t=1, and
|
||
anywhere in between we're simply going to say that <code>t</code> is equal to the distance along the polygon, scaled to the [0,1] domain.
|
||
</p>
|
||
<p>To get these values, we first compute the general "distance along the polygon" matrix:</p>
|
||
<!--
|
||
|
||
╭ d = 0
|
||
D = ┌ d d ... d ┐, where ╡ 1
|
||
└ 1 2 n ┘ │ d = d + length(p , p )
|
||
╰ i i-1 i-1 i
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/curvefitting/2f82371abb7835f9b9d440dc5dd151a8.svg"
|
||
width="395px"
|
||
height="40px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
Where <code>length()</code> is literally just that: the length of the line segment between the point we're looking at, and the previous
|
||
point. This isn't quite enough, of course: we still need to make sure that all the values between <code>i=1</code> and
|
||
<code>i=n</code> fall in the [0,1] interval, so we need to scale all values down by whatever the total length of the polygon is:
|
||
</p>
|
||
<!--
|
||
|
||
╭ s = 0
|
||
│ 1
|
||
S = ┌ s s ... s ┐, where ╡ s = d / d
|
||
└ 1 2 n ┘ │ i i n
|
||
│ s = 1
|
||
╰ n
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/curvefitting/6f734d319a1cfe0de76574a65abb07e1.svg"
|
||
width="272px"
|
||
height="55px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
And now we can move on to the actual "curve fitting" part: what we want is a function that lets us compute "ideal" control point values
|
||
such that if we build a Bézier curve with them, that curve passes through all our original points. Or, failing that, have an overall error
|
||
distance that is as close to zero as we can get it. So, let's write out what the error distance looks like.
|
||
</p>
|
||
<p>
|
||
As mentioned before, this function is really just "the distance between the actual coordinate, and the coordinate that the curve evaluates
|
||
to for the associated <code>t</code> value", which we'll square to get rid of any pesky negative signs:
|
||
</p>
|
||
<!--
|
||
|
||
2
|
||
E(C) = (p - Bézier(s ))
|
||
i i i
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/curvefitting/d39ca235454ced9681b523be056864d2.svg"
|
||
width="177px"
|
||
height="23px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
Since this function only deals with individual coordinates, we'll need to sum over all coordinates in order to get the full error
|
||
function. So, we literally just do that; the total error function is simply the sum of all these individual errors:
|
||
</p>
|
||
<!--
|
||
|
||
__ n 2
|
||
E(C) = ❯ (p - Bézier(s ))
|
||
‾‾ i=1 i i
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/curvefitting/097aa1948b6cdbf9dc7579643a7af246.svg"
|
||
width="195px"
|
||
height="41px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
And here's the trick that justifies using matrices: while we can work with individual values using calculus, with matrices we can compute
|
||
as many values as we make our matrices big, all at the "same time", We can replace the individual terms p<sub>i</sub> with the full
|
||
<strong>P</strong> coordinate matrix, and we can replace Bézier(s<sub>i</sub>) with the matrix representation
|
||
<strong>T x M x C</strong> we talked about before, which gives us:
|
||
</p>
|
||
<!--
|
||
|
||
2
|
||
E(C) = (P - TMC)
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/curvefitting/7b0199bb515d2754c03d8f796b29febf.svg"
|
||
width="141px"
|
||
height="21px"
|
||
loading="lazy"
|
||
/>
|
||
<p>In which we can replace the rather cumbersome "squaring" operation with a more conventional matrix equivalent:</p>
|
||
<!--
|
||
|
||
T
|
||
E(C) = (P - TMC) (P - TMC)
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/curvefitting/8068231b915832938136d5833f74751d.svg"
|
||
width="225px"
|
||
height="21px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
Here, the letter <code>T</code> is used instead of the number 2, to represent the
|
||
<a href="https://en.wikipedia.org/wiki/Transpose">matrix transpose</a>; each row in the original matrix becomes a column in the transposed
|
||
matrix instead (row one becomes column one, row two becomes column two, and so on).
|
||
</p>
|
||
<p>
|
||
This leaves one problem: <strong>T</strong> isn't actually the matrix we want: we don't want symbolic <code>t</code> values, we want the
|
||
actual numerical values that we computed for <strong>S</strong>, so we need to form a new matrix, which we'll call 𝕋, that makes use of
|
||
those, and then use that 𝕋 instead of <strong>T</strong> in our error function:
|
||
</p>
|
||
<!--
|
||
|
||
┌ 0 1 n-2 n-1 ┐
|
||
│ s s ... s s │
|
||
│ 1 1 1 1 │
|
||
│ │
|
||
𝕋 = │ \vdots ... \vdots │
|
||
│ │
|
||
│ 0 1 n-2 n-1 │
|
||
│ s s ... s s │
|
||
└ n n n n ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/curvefitting/4dd55c228a26bb50da912a45e8721024.svg"
|
||
width="201px"
|
||
height="96px"
|
||
loading="lazy"
|
||
/>
|
||
<p>Which, because of the first and last values in <strong>S</strong>, means:</p>
|
||
<!--
|
||
|
||
┌ 1 0 ... 0 0 ┐
|
||
│ n-2 n-1 │
|
||
│ 1 s s s │
|
||
│ 2 2 2 │
|
||
𝕋 = │ \vdots ... \vdots │
|
||
│ n-2 n-1 │
|
||
│ 1 s s s │
|
||
│ n-1 n-1 n-1 │
|
||
└ 1 1 ... 1 1 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/curvefitting/38bb81bdd3eaa72c2336514187aa374b.svg"
|
||
width="212px"
|
||
height="92px"
|
||
loading="lazy"
|
||
/>
|
||
<p>Now we can properly write out the error function as matrix operations:</p>
|
||
<!--
|
||
|
||
T
|
||
E(C) = (P - 𝕋MC) (P - 𝕋MC)
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/curvefitting/989f2ad06ae308f71cef527a5594129a.svg"
|
||
width="231px"
|
||
height="21px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
So, we have our error function: we now need to figure out the expression for where that function has minimal value, e.g. where the error
|
||
between the true coordinates and the coordinates generated by the curve fitting is smallest. Like in standard calculus, this requires
|
||
taking the derivative, and determining where that derivative is zero:
|
||
</p>
|
||
<!--
|
||
|
||
∂E T
|
||
── = 0 = -2𝕋 (P - 𝕋MC)
|
||
∂C
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/curvefitting/ea24b0e42f0a89464bda275ac8f9bacf.svg"
|
||
width="197px"
|
||
height="36px"
|
||
loading="lazy"
|
||
/>
|
||
<div class="note">
|
||
<h2>Where did this derivative come from?</h2>
|
||
<p>
|
||
That... is a good question. In fact, when trying to run through this approach, I ran into the same question! And you know what? I
|
||
straight up had no idea. I'm decent enough at calculus, I'm decent enough at linear algebra, and I just don't know.
|
||
</p>
|
||
<p>
|
||
So I did what I always do when I don't understand something: I asked someone to help me understand how things work. In this specific
|
||
case, I
|
||
<a href="https://math.stackexchange.com/questions/2825438/how-do-you-compute-the-derivative-of-a-matrix-algebra-expression"
|
||
>posted a question</a
|
||
>
|
||
to <a href="https://math.stackexchange.com">Math.stackexchange</a>, and received a answer that goes into way more detail than I had
|
||
hoped to receive.
|
||
</p>
|
||
<p>
|
||
Is that answer useful to you? Probably: no. At least, not unless you like understanding maths on a recreational level. And I do mean
|
||
maths in general, not just basic algebra. But it does help in giving us a reference in case you ever wonder "Hang on. Why was that
|
||
true?". There are answers. They might just require some time to come to understand.
|
||
</p>
|
||
</div>
|
||
|
||
<p>
|
||
Now, given the above derivative, we can rearrange the terms (following the rules of matrix algebra) so that we end up with an expression
|
||
for <strong>C</strong>:
|
||
</p>
|
||
<!--
|
||
|
||
-1 T -1 T
|
||
C = M (𝕋 𝕋) 𝕋 P
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/curvefitting/9dec10b81a61b456ca1550cd9b7ba513.svg"
|
||
width="168px"
|
||
height="27px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
Here, the "to the power negative one" is the notation for the
|
||
<a href="https://en.wikipedia.org/wiki/Invertible_matrix">matrix inverse</a>. But that's all we have to do: we're done. Starting with
|
||
<strong>P</strong> and inventing some <code>t</code> values based on the polygon the coordinates in <strong>P</strong> define, we can
|
||
compute the corresponding Bézier coordinates <strong>C</strong> that specify a curve that goes through our points. Or, if it can't go
|
||
through them exactly, as near as possible.
|
||
</p>
|
||
<p>
|
||
So before we try that out, how much code is involved in implementing this? Honestly, that answer depends on how much you're going to be
|
||
writing yourself. If you already have a matrix maths library available, then really not that much code at all. On the other hand, if you
|
||
are writing this from scratch, you're going to have to write some utility functions for doing your matrix work for you, so it's really
|
||
anywhere from 50 lines of code to maybe 200 lines of code. Not a bad price to pay for being able to fit curves to pre-specified
|
||
coordinates.
|
||
</p>
|
||
<p>
|
||
So let's try it out! The following graphic lets you place points, and will start computing exact-fit curves once you've placed at least
|
||
three. You can click for more points, and the code will simply try to compute an exact fit using a Bézier curve of the appropriate order.
|
||
Four points? Cubic Bézier. Five points? Quartic. And so on. Of course, this does break down at some point: depending on where you place
|
||
your points, it might become mighty hard for the fitter to find an exact fit, and things might actually start looking horribly off once
|
||
there's enough points for compound
|
||
<a href="https://en.wikipedia.org/wiki/Round-off_error#Floating-point_number_system">floating point rounding errors</a> to start making a
|
||
difference (which is around 10~11 points).
|
||
</p>
|
||
<graphics-element
|
||
title="Fitting a Bézier curve"
|
||
width="550"
|
||
height="275"
|
||
src="./chapters/curvefitting/curve-fitting.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="550px" height="275px" src="./images/chapters/curvefitting/798f3d7151dfb2887c7881a08e65cdd3.png" loading="lazy" />
|
||
<label></label>
|
||
</fallback-image>
|
||
<button class="toggle">toggle</button>
|
||
<!-- additional sliders will get created on the fly -->
|
||
</graphics-element>
|
||
<p>
|
||
You'll note there is a convenient "toggle" buttons that lets you toggle between equidistant <code>t</code> values, and distance ratio
|
||
along the polygon formed by the points. Arguably more interesting is that once you have points to abstract a curve, you also get
|
||
<em>direct control</em> over the time values through sliders for each, because if the time values are our degree of freedom, you should be
|
||
able to freely manipulate them and see what the effect on your curve is.
|
||
</p>
|
||
</section>
|
||
<section id="catmullconv">
|
||
<h1>
|
||
<div class="nav">
|
||
<a href="zh-CN/index.html#curvefitting">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#catmullfitting">下</a>
|
||
</div>
|
||
<a href="zh-CN/index.html#catmullconv">Bézier curves and Catmull-Rom curves</a>
|
||
</h1>
|
||
<p>
|
||
Taking an excursion to different splines, the other common design curve is the
|
||
<a href="https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Catmull.E2.80.93Rom_spline">Catmull-Rom spline</a>, which unlike Bézier curves
|
||
pass <em>through</em> each control point, so they offer a kind of "built-in" curve fitting.
|
||
</p>
|
||
<p>
|
||
In fact, let's start with just playing with one: the following graphic has a predefined curve that you manipulate the points for, and lets
|
||
you add points by clicking/tapping the background, as well as let you control "how fast" the curve passes through its point using the
|
||
tension slider. The tenser the curve, the more the curve tends towards straight lines from one point to the next.
|
||
</p>
|
||
<graphics-element
|
||
title="A Catmull-Rom curve"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/catmullconv/catmull-rom.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/catmullconv/aa46749b9469341d9249ca452390d875.png" loading="lazy" />
|
||
<label>A Catmull-Rom curve</label>
|
||
</fallback-image>
|
||
<input type="range" min="0.1" max="1" step="0.01" value="0.5" class="slide-control tension" />
|
||
</graphics-element>
|
||
<p>
|
||
Now, it may look like Catmull-Rom curves are very different from Bézier curves, because these curves can get very long indeed, but what
|
||
looks like a single Catmull-Rom curve is actually a <a href="https://en.wikipedia.org/wiki/Spline_(mathematics)">spline</a>: a single
|
||
curve built up of lots of identically-computed pieces, similar to if you just took a whole bunch of Bézier curves, placed them end to end,
|
||
and lined up their control points so that things look like a single curve. For a Catmull-Rom curve, each "piece" between two points is
|
||
defined by the point's coordinates, and the tangent for those points, the latter of which
|
||
<a href="https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Catmull%E2%80%93Rom_spline">can trivially be derived</a> from knowing the
|
||
previous and next point:
|
||
</p>
|
||
<!--
|
||
|
||
┌ V = P ┐
|
||
│ 1 2 │
|
||
┌ P ┐ │ V = P │
|
||
│ 1 │ │ 2 3 │
|
||
│ P │ │ P - P │
|
||
│ 2 │ = │ 3 1 │
|
||
│ P │points │ V' = ─────── │ point-tangent
|
||
│ 3 │ │ 1 2 │
|
||
│ P │ │ P - P │
|
||
└ 4 ┘ │ 4 2 │
|
||
│ V' = ─────── │
|
||
└ 2 2 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/catmullconv/639ca0b74a805c3aebac79b181eac908.svg"
|
||
width="272px"
|
||
height="88px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
One downside of this is that—as you may have noticed from the graphic—the first and last point of the overall curve don't actually join up
|
||
with the rest of the curve: they don't have a previous/next point respectively, and so there is no way to calculate what their tangent
|
||
should be. Which also makes it rather tricky to fit a Catmull-Rom curve to three points like we were able to do for Bézier curves. More on
|
||
that in <a href="#catmullfitting">the next section</a>.
|
||
</p>
|
||
<p>
|
||
In fact, before we move on, let's look at how to actually draw the basic form of these curves (I say basic, because there are a number of
|
||
variations that make things
|
||
<a href="https://en.wikipedia.org/wiki/Centripetal_Catmull%E2%80%93Rom_spline#Definition">considerable</a> more
|
||
<a href="https://en.wikipedia.org/wiki/Kochanek%E2%80%93Bartels_spline">complex</a>):
|
||
</p>
|
||
|
||
<table class="code">
|
||
<tr>
|
||
<td>1</td>
|
||
<td rowspan="19">
|
||
<textarea disabled rows="19" role="doc-example">
|
||
tension = some value greater than 0, defaulting to 1
|
||
points = a list of at least 4 coordinates
|
||
|
||
for p = 1 to points.length-3 (inclusive):
|
||
p0 = points[p-1]
|
||
v1 = p1 = points[p]
|
||
v2 = p2 = points[p+1]
|
||
p3 = points[p+2]
|
||
|
||
s = 2 * tension
|
||
dv1 = (p2-p0) / s
|
||
dv2 = (p3-p1) / s
|
||
|
||
for t = 0 to 1 (inclusive):
|
||
c0 = 2*t^3 - 3*t^2 + 1,
|
||
c1 = t^3 - 2*t^2 + t,
|
||
c2 = -2*t^3 + 3*t^2,
|
||
c3 = t^3 - t^2
|
||
point(c0 * v1 + c1 * dv1 + c2 * v2 + c3 * dv2)</textarea
|
||
>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>7</td>
|
||
</tr>
|
||
<tr>
|
||
<td>8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>9</td>
|
||
</tr>
|
||
<tr>
|
||
<td>10</td>
|
||
</tr>
|
||
<tr>
|
||
<td>11</td>
|
||
</tr>
|
||
<tr>
|
||
<td>12</td>
|
||
</tr>
|
||
<tr>
|
||
<td>13</td>
|
||
</tr>
|
||
<tr>
|
||
<td>14</td>
|
||
</tr>
|
||
<tr>
|
||
<td>15</td>
|
||
</tr>
|
||
<tr>
|
||
<td>16</td>
|
||
</tr>
|
||
<tr>
|
||
<td>17</td>
|
||
</tr>
|
||
<tr>
|
||
<td>18</td>
|
||
</tr>
|
||
<tr>
|
||
<td>19</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p>
|
||
Now, since a Catmull-Rom curve is a form of <a href="https://en.wikipedia.org/wiki/Cubic_Hermite_spline">cubic Hermite spline</a>, and as
|
||
cubic Bézier curves are <em>also</em> a form of cubic Hermite spline, we run into an interesting bit of maths programming: we can convert
|
||
one to the other and back, and the maths for doing so is surprisingly simple!
|
||
</p>
|
||
<p>The main difference between Catmull-Rom curves and Bézier curves is "what the points mean":</p>
|
||
<ul>
|
||
<li>
|
||
A cubic Bézier curve is defined by a start point, a control point that implies the tangent at the start, a control point that implies
|
||
the tangent at the end, and an end point, plus a characterizing matrix that we can multiply by that point vector to get on-curve
|
||
coordinates.
|
||
</li>
|
||
<li>
|
||
A Catmull-Rom curve is defined by a start point, a tangent that for that starting point, an end point, and a tangent for that end point,
|
||
plus a characteristic matrix that we can multiple by the point vector to get on-curve coordinates.
|
||
</li>
|
||
</ul>
|
||
<p>
|
||
Those are <em>very</em> similar, so let's see exactly <em>how</em> similar they are. We've already see the matrix form for Bézier curves,
|
||
so how different is the matrix form for Catmull-Rom curves?:
|
||
</p>
|
||
<!--
|
||
|
||
┌ V ┐
|
||
│ 1 │
|
||
┌ 1 0 0 0 ┐ │ V │
|
||
CatmullRom(t) = ┌ 2 3 ┐ · │ 0 0 1 0 │ · │ 2 │
|
||
└ 1 t t t ┘ │ -3 3 -2 -1 │ │ V' │
|
||
└ 2 -2 1 1 ┘ │ 1 │
|
||
│ V' │
|
||
└ 2 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/catmullconv/98ddf6415bd9827a6d899b21d0a5f736.svg"
|
||
width="409px"
|
||
height="75px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
That's pretty dang similar. So the question is: how can we convert that expression with Catmull-Rom matrix and vector into an expression
|
||
of the Bézier matrix and vector? The short answer is of course "by using linear algebra", but the longer answer is the rest of this
|
||
section, and involves some maths that you may not even care for: if you just want to know the (incredibly simple) conversions between the
|
||
two curve forms, feel free to skip to the end of the following explanation, but if you want to <em>how</em> we can get one from the
|
||
other... let's get mathing!
|
||
</p>
|
||
<div class="note">
|
||
<h2>Deriving the conversion formulae</h2>
|
||
<p>
|
||
In order to convert between Catmull-Rom curves and Bézier curves, we need to know two things. Firstly, how to express the Catmull-Rom
|
||
curve using a "set of four coordinates", rather than a mix of coordinates and tangents, and secondly, how to convert those Catmull-Rom
|
||
coordinates to and from Bézier form.
|
||
</p>
|
||
<p>
|
||
We start with the first part, to figure out how we can go from Catmull-Rom <strong>V</strong> coordinates to Bézier
|
||
<strong>P</strong> coordinates, by applying "some matrix <strong>T</strong>". We don't know what that <strong>T</strong> is yet, but
|
||
we'll get to that:
|
||
</p>
|
||
<!--
|
||
|
||
┌ P ┐
|
||
│ 2 │
|
||
┌ V ┐ ┌ P ┐ │ P │
|
||
│ 1 │ │ 1 │ │ 3 │
|
||
│ V │ │ P │ │ P - P │
|
||
│ 2 │ = T · │ 2 │ = │ 3 1 │
|
||
│ V' │ │ P │ │ ─────── │
|
||
│ 1 │ │ 3 │ │ 2 │
|
||
│ V' │ │ P │ │ P - P │
|
||
└ 2 ┘ └ 4 ┘ │ 4 2 │
|
||
│ ─────── │
|
||
└ 2 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/catmullconv/b94a4dafc12ba7e4fbf3aff924f55464.svg"
|
||
width="187px"
|
||
height="83px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
So, this mapping says that in order to map a Catmull-Rom "point + tangent" vector to something based on an "all coordinates" vector, we
|
||
need to determine the mapping matrix such that applying <em>T</em> yields P2 as start point, P3 as end point, and two tangents based on
|
||
the lines between P1 and P3, and P2 nd P4, respectively.
|
||
</p>
|
||
<p>Computing <em>T</em> is really more "arranging the numbers":</p>
|
||
<!--
|
||
|
||
┌ P ┐
|
||
│ 2 │
|
||
┌ P ┐ │ P │ ┌ 0 · P1 + 1 · P2 + 0 · P3 + 0 · P4 ┐ ┌ 0 1 0 0 ┐ ┌ P ┐
|
||
│ 1 │ │ 3 │ │ 0 · P1 + 0 · P2 + 1 · P3 + 0 · P4 │ │ 0 0 1 0 │ │ 1 │
|
||
│ P │ │ P - P │ │ -1 1 │ │ -1 1 │ │ P │
|
||
T · │ 2 │ = │ 3 1 │ = │ ── · P1 + 0 · P2 + ─ · P3 + 0 · P4 │ = │ ── 0 ─ 0 │ · │ 2 │
|
||
│ P │ │ ─────── │ │ 2 2 │ │ 2 2 │ │ P │
|
||
│ 3 │ │ 2 │ │ -1 1 │ │ -1 1 │ │ 3 │
|
||
│ P │ │ P - P │ │ 0 · P1 ── · P2 + 0 · P3 + ─ · P4 │ │ 0 ── 0 ─ │ │ P │
|
||
└ 4 ┘ │ 4 2 │ └ 2 2 ┘ └ 2 2 ┘ └ 4 ┘
|
||
│ ─────── │
|
||
└ 2 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/catmullconv/a55773fdcdfd99947acc4f86ad2d4a3d.svg"
|
||
width="591px"
|
||
height="83px"
|
||
loading="lazy"
|
||
/>
|
||
<p>Thus:</p>
|
||
<!--
|
||
|
||
┌ 0 1 0 0 ┐
|
||
│ 0 0 1 0 │
|
||
│ -1 1 │
|
||
T = │ ── 0 ─ 0 │
|
||
│ 2 2 │
|
||
│ -1 1 │
|
||
│ 0 ── 0 ─ │
|
||
└ 2 2 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/catmullconv/7bab9dd3da654b05fa065076894e2d82.svg"
|
||
width="143px"
|
||
height="81px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
However, we're not <em>quite</em> done, because Catmull-Rom curves have that "tension" parameter, written as τ (a lowercase"tau"), which
|
||
is a scaling factor for the tangent vectors: the bigger the tension, the smaller the tangents, and the smaller the tension, the bigger
|
||
the tangents. As such, the tension factor goes in the denominator for the tangents, and before we continue, let's add that tension
|
||
factor into both our coordinate vector representation, and mapping matrix <em>T</em>:
|
||
</p>
|
||
<!--
|
||
|
||
┌ P ┐
|
||
│ 2 │
|
||
┌ V ┐ │ P │ ┌ 0 1 0 0 ┐
|
||
│ 1 │ │ 3 │ │ 0 0 1 0 │
|
||
│ V │ │ P - P │ │ -1 1 │
|
||
│ 2 │ = │ 3 1 │ ,\ T = │ ── 0 ── 0 │
|
||
│ V' │ │ ─────── │ │ 2τ 2τ │
|
||
│ 1 │ │ 2τ │ │ -1 1 │
|
||
│ V' │ │ P - P │ │ 0 ── 0 ── │
|
||
└ 2 ┘ │ 4 2 │ └ 2τ 2τ ┘
|
||
│ ─────── │
|
||
└ 2τ ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/catmullconv/8de53f207d68b25854a5f0b924ac6010.svg"
|
||
width="285px"
|
||
height="84px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
With the mapping matrix properly done, let's rewrite the "point + tangent" Catmull-Rom matrix form to a matrix form in terms of four
|
||
coordinates, and see what we end up with:
|
||
</p>
|
||
<!--
|
||
|
||
┌ V ┐
|
||
│ 1 │
|
||
┌ 1 0 0 0 ┐ │ V │
|
||
CatmullRom(t) = ┌ 2 3 ┐ · │ 0 0 1 0 │ · │ 2 │
|
||
└ 1 t t t ┘ │ -3 3 -2 -1 │ │ V' │
|
||
└ 2 -2 1 1 ┘ │ 1 │
|
||
│ V' │
|
||
└ 2 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/catmullconv/902c290a790b4d44d10236f4a1456cdc.svg"
|
||
width="409px"
|
||
height="75px"
|
||
loading="lazy"
|
||
/>
|
||
<p>Replace point/tangent vector with the expression for all-coordinates:</p>
|
||
<!--
|
||
|
||
┌ 0 1 0 0 ┐ ┌ P ┐
|
||
│ 0 0 1 0 │ │ 1 │
|
||
┌ 1 0 0 0 ┐ │ -1 1 │ │ P │
|
||
CatmullRom(t) = ┌ 2 3 ┐ · │ 0 0 1 0 │ · │ ── 0 ── 0 │ · │ 2 │
|
||
└ 1 t t t ┘ │ -3 3 -2 -1 │ │ 2τ 2τ │ │ P │
|
||
└ 2 -2 1 1 ┘ │ -1 1 │ │ 3 │
|
||
│ 0 ── 0 ── │ │ P │
|
||
└ 2τ 2τ ┘ └ 4 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/catmullconv/032409c03915a6ba75864e1dceae416d.svg"
|
||
width="549px"
|
||
height="81px"
|
||
loading="lazy"
|
||
/>
|
||
<p>and merge the matrices:</p>
|
||
<!--
|
||
|
||
┌ 0 1 0 0 ┐
|
||
│ -1 1 │ ┌ P ┐
|
||
│ ── 0 ── 0 │ │ 1 │
|
||
│ 2τ 2τ │ │ P │
|
||
CatmullRom(t) = ┌ 2 3 ┐ · │ 1 1 1 -1 │ · │ 2 │
|
||
└ 1 t t t ┘ │ ─ ── - 3 3 - ─ ── │ │ P │
|
||
│ τ 2t t 2t │ │ 3 │
|
||
│ -1 1 1 1 │ │ P │
|
||
│ ── 2 - ── ── - 2 ── │ └ 4 ┘
|
||
└ 2t 2τ 2τ 2t ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/catmullconv/e653724c11600cbf682f1c809c8c6508.svg"
|
||
width="455px"
|
||
height="84px"
|
||
loading="lazy"
|
||
/>
|
||
<p>This looks a lot like the Bézier matrix form, which as we saw in the chapter on Bézier curves, should look like this:</p>
|
||
<!--
|
||
|
||
┌ P ┐
|
||
│ 1 │
|
||
┌ 1 0 0 0 ┐ │ P │
|
||
Bézier(t) = ┌ 2 3 ┐ · │ -3 3 0 0 │ · │ 2 │
|
||
└ 1 t t t ┘ │ 3 -6 3 0 │ │ P │
|
||
└ -1 3 -3 1 ┘ │ 3 │
|
||
│ P │
|
||
└ 4 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/catmullconv/389a1ea8c9e92df9a2b38718e34bae7b.svg"
|
||
width="353px"
|
||
height="73px"
|
||
loading="lazy"
|
||
/>
|
||
<p>So, if we want to express a Catmull-Rom curve using a Bézier curve, we'll need to turn this Catmull-Rom bit:</p>
|
||
<!--
|
||
|
||
┌ 0 1 0 0 ┐
|
||
│ -1 1 │ ┌ P ┐
|
||
│ ── 0 ── 0 │ │ 1 │
|
||
│ 2τ 2τ │ │ P │
|
||
│ 1 1 1 -1 │ · │ 2 │
|
||
│ ─ ── - 3 3 - ─ ── │ │ P │
|
||
│ τ 2t t 2t │ │ 3 │
|
||
│ -1 1 1 1 │ │ P │
|
||
│ ── 2 - ── ── - 2 ── │ └ 4 ┘
|
||
└ 2t 2τ 2τ 2t ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/catmullconv/574bed6665be06b309b8da722c616a41.svg"
|
||
width="227px"
|
||
height="84px"
|
||
loading="lazy"
|
||
/>
|
||
<p>Into something that looks like this:</p>
|
||
<!--
|
||
|
||
┌ P ┐
|
||
│ 1 │
|
||
┌ 1 0 0 0 ┐ │ P │
|
||
│ -3 3 0 0 │ · │ 2 │
|
||
│ 3 -6 3 0 │ │ P │
|
||
└ -1 3 -3 1 ┘ │ 3 │
|
||
│ P │
|
||
└ 4 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/catmullconv/fe2e6fd487df224b2f55a601898ce333.svg"
|
||
width="167px"
|
||
height="73px"
|
||
loading="lazy"
|
||
/>
|
||
<p>And the way we do that is with a fairly straight forward bit of matrix rewriting. We start with the equality we need to ensure:</p>
|
||
<!--
|
||
|
||
┌ 0 1 0 0 ┐
|
||
│ -1 1 │ ┌ P ┐ ┌ P ┐
|
||
│ ── 0 ── 0 │ │ 1 │ │ 1 │
|
||
│ 2τ 2τ │ │ P │ ┌ 1 0 0 0 ┐ │ P │
|
||
│ 1 1 1 -1 │ · │ 2 │ = │ -3 3 0 0 │ · V · │ 2 │
|
||
│ ─ ── - 3 3 - ─ ── │ │ P │ │ 3 -6 3 0 │ │ P │
|
||
│ τ 2t t 2t │ │ 3 │ └ -1 3 -3 1 ┘ │ 3 │
|
||
│ -1 1 1 1 │ │ P │ │ P │
|
||
│ ── 2 - ── ── - 2 ── │ └ 4 ┘ └ 4 ┘
|
||
└ 2t 2τ 2τ 2t ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/catmullconv/b59ff8d654e65df4c874901983208893.svg"
|
||
width="440px"
|
||
height="84px"
|
||
loading="lazy"
|
||
/>
|
||
<p>Then we remove the coordinate vector from both sides without affecting the equality:</p>
|
||
<!--
|
||
|
||
┌ 0 1 0 0 ┐
|
||
│ -1 1 │
|
||
│ ── 0 ── 0 │
|
||
│ 2τ 2τ │ ┌ 1 0 0 0 ┐
|
||
│ 1 1 1 -1 │ = │ -3 3 0 0 │ · V
|
||
│ ─ ── - 3 3 - ─ ── │ │ 3 -6 3 0 │
|
||
│ τ 2t t 2t │ └ -1 3 -3 1 ┘
|
||
│ -1 1 1 1 │
|
||
│ ── 2 - ── ── - 2 ── │
|
||
└ 2t 2τ 2τ 2t ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/catmullconv/65e589eafae8ff2f39392d8143d2845c.svg"
|
||
width="353px"
|
||
height="84px"
|
||
loading="lazy"
|
||
/>
|
||
<p>Then we can "get rid of" the Bézier matrix on the right by left-multiply both with the inverse of the Bézier matrix:</p>
|
||
<!--
|
||
|
||
┌ 0 1 0 0 ┐
|
||
│ -1 1 │
|
||
│ ── 0 ── 0 │
|
||
┌ 1 0 0 0 ┐ -1 │ 2τ 2τ │ ┌ 1 0 0 0 ┐ -1 ┌ 1 0 0 0 ┐
|
||
│ -3 3 0 0 │ · │ 1 1 1 -1 │ = │ -3 3 0 0 │ · │ -3 3 0 0 │ · V
|
||
│ 3 -6 3 0 │ │ ─ ── - 3 3 - ─ ── │ │ 3 -6 3 0 │ │ 3 -6 3 0 │
|
||
└ -1 3 -3 1 ┘ │ τ 2t t 2t │ └ -1 3 -3 1 ┘ └ -1 3 -3 1 ┘
|
||
│ -1 1 1 1 │
|
||
│ ── 2 - ── ── - 2 ── │
|
||
└ 2t 2τ 2τ 2t ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/catmullconv/917b176a45959b026c56f81999505dc7.svg"
|
||
width="657px"
|
||
height="88px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
A matrix times its inverse is the matrix equivalent of 1, and because "something times 1" is the same as "something", so we can just
|
||
outright remove any matrix/inverse pair:
|
||
</p>
|
||
<!--
|
||
|
||
┌ 0 1 0 0 ┐
|
||
│ -1 1 │
|
||
│ ── 0 ── 0 │
|
||
┌ 1 0 0 0 ┐ -1 │ 2τ 2τ │
|
||
│ -3 3 0 0 │ · │ 1 1 1 -1 │ = V
|
||
│ 3 -6 3 0 │ │ ─ ── - 3 3 - ─ ── │
|
||
└ -1 3 -3 1 ┘ │ τ 2t t 2t │
|
||
│ -1 1 1 1 │
|
||
│ ── 2 - ── ── - 2 ── │
|
||
└ 2t 2τ 2τ 2t ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/catmullconv/4e0da16710a7339f04dd844c7705423e.svg"
|
||
width="369px"
|
||
height="88px"
|
||
loading="lazy"
|
||
/>
|
||
<p>And now we're <em>basically</em> done. We just multiply those two matrices and we know what <em>V</em> is:</p>
|
||
<!--
|
||
|
||
┌ 0 1 0 0 ┐
|
||
│ -1 1 │
|
||
│ ── 1 ── 0 │
|
||
│ 6τ 6τ │ = V
|
||
│ 1 -1 │
|
||
│ 0 ── 1 ── │
|
||
│ 6τ 6τ │
|
||
└ 0 0 1 0 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/catmullconv/1cef6bbf7b3d10e8c0aaecfac816cc86.svg"
|
||
width="161px"
|
||
height="77px"
|
||
loading="lazy"
|
||
/>
|
||
<p>We now have the final piece of our function puzzle. Let's run through each step.</p>
|
||
<ol>
|
||
<li>Start with the Catmull-Rom function:</li>
|
||
</ol>
|
||
<!--
|
||
|
||
┌ V ┐
|
||
│ 1 │
|
||
┌ 1 0 0 0 ┐ │ V │
|
||
CatmullRom(t) = ┌ 2 3 ┐ · │ 0 0 1 0 │ · │ 2 │
|
||
└ 1 t t t ┘ │ -3 3 -2 -1 │ │ V' │
|
||
└ 2 -2 1 1 ┘ │ 1 │
|
||
│ V' │
|
||
└ 2 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/catmullconv/902c290a790b4d44d10236f4a1456cdc.svg"
|
||
width="409px"
|
||
height="75px"
|
||
loading="lazy"
|
||
/>
|
||
<ol start="2">
|
||
<li>rewrite to pure coordinate form:</li>
|
||
</ol>
|
||
<!--
|
||
|
||
┌ P ┐
|
||
│ 2 │
|
||
│ P │
|
||
│ 3 │
|
||
┌ 1 0 0 0 ┐ │ P - P │
|
||
= ┌ 2 3 ┐ · │ 0 0 1 0 │ · │ 3 1 │
|
||
└ 1 t t t ┘ │ -3 3 -2 -1 │ │ ─────── │
|
||
└ 2 -2 1 1 ┘ │ 2τ │
|
||
│ P - P │
|
||
│ 4 2 │
|
||
│ ─────── │
|
||
└ 2τ ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/catmullconv/211dadbb9d0f6b2e381f18ea3c4d12fb.svg"
|
||
width="324px"
|
||
height="84px"
|
||
loading="lazy"
|
||
/>
|
||
<ol start="3">
|
||
<li>rewrite for "normal" coordinate vector:</li>
|
||
</ol>
|
||
<!--
|
||
|
||
┌ 0 1 0 0 ┐ ┌ P ┐
|
||
│ 0 0 1 0 │ │ 1 │
|
||
┌ 1 0 0 0 ┐ │ -1 1 │ │ P │
|
||
= ┌ 2 3 ┐ · │ 0 0 1 0 │ · │ ── 0 ── 0 │ · │ 2 │
|
||
└ 1 t t t ┘ │ -3 3 -2 -1 │ │ 2τ 2τ │ │ P │
|
||
└ 2 -2 1 1 ┘ │ -1 1 │ │ 3 │
|
||
│ 0 ── 0 ── │ │ P │
|
||
└ 2τ 2τ ┘ └ 4 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/catmullconv/a8158b35ec221cccff51a53cdc7f440b.svg"
|
||
width="441px"
|
||
height="81px"
|
||
loading="lazy"
|
||
/>
|
||
<ol start="4">
|
||
<li>merge the inner matrices:</li>
|
||
</ol>
|
||
<!--
|
||
|
||
┌ 0 1 0 0 ┐
|
||
│ -1 1 │ ┌ P ┐
|
||
│ ── 0 ── 0 │ │ 1 │
|
||
│ 2τ 2τ │ │ P │
|
||
= ┌ 2 3 ┐ · │ 1 1 1 -1 │ · │ 2 │
|
||
└ 1 t t t ┘ │ ─ ── - 3 3 - ─ ── │ │ P │
|
||
│ τ 2t t 2t │ │ 3 │
|
||
│ -1 1 1 1 │ │ P │
|
||
│ ── 2 - ── ── - 2 ── │ └ 4 ┘
|
||
└ 2t 2τ 2τ 2t ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/catmullconv/53f216327c0bbcf02b2a331fbf44d389.svg"
|
||
width="348px"
|
||
height="84px"
|
||
loading="lazy"
|
||
/>
|
||
<ol start="5">
|
||
<li>rewrite for Bézier matrix form:</li>
|
||
</ol>
|
||
<!--
|
||
|
||
┌ 0 1 0 0 ┐ ┌ P ┐
|
||
│ -1 1 │ │ 1 │
|
||
┌ 1 0 0 0 ┐ │ ── 1 ── 0 │ │ P │
|
||
= ┌ 2 3 ┐ · │ -3 3 0 0 │ · │ 6τ 6τ │ · │ 2 │
|
||
└ 1 t t t ┘ │ 3 -6 3 0 │ │ 1 -1 │ │ P │
|
||
└ -1 3 -3 1 ┘ │ 0 ── 1 ── │ │ 3 │
|
||
│ 6τ 6τ │ │ P │
|
||
└ 0 0 1 0 ┘ └ 4 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/catmullconv/4c8684109149b0dc79f5583a5912fcd9.svg"
|
||
width="431px"
|
||
height="77px"
|
||
loading="lazy"
|
||
/>
|
||
<ol start="6">
|
||
<li>and transform the coordinates so we have a "pure" Bézier expression:</li>
|
||
</ol>
|
||
<!--
|
||
|
||
┌ P ┐
|
||
│ 2 │
|
||
│ P -P │
|
||
│ 3 1 │
|
||
┌ 1 0 0 0 ┐ │ P + ────── │
|
||
= ┌ 2 3 ┐ · │ -3 3 0 0 │ · │ 2 6 · τ │
|
||
└ 1 t t t ┘ │ 3 -6 3 0 │ │ P -P │
|
||
└ -1 3 -3 1 ┘ │ 4 2 │
|
||
│ P - ────── │
|
||
│ 3 6 · τ │
|
||
│ P │
|
||
└ 3 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/catmullconv/157b287d6b74109d8c8b634990ea6549.svg"
|
||
width="348px"
|
||
height="81px"
|
||
loading="lazy"
|
||
/>
|
||
<p>And we're done: we finally know how to convert these two curves!</p>
|
||
</div>
|
||
|
||
<p>
|
||
If we have a Catmull-Rom curve defined by four coordinates P<sub>1</sub> through P<sub>4</sub>, then we can draw that curve using a Bézier
|
||
curve that has the vector:
|
||
</p>
|
||
<!--
|
||
|
||
┌ P ┐
|
||
│ 2 │
|
||
┌ P ┐ │ P -P │
|
||
│ 1 │ │ 3 1 │
|
||
│ P │ │ P + ────── │
|
||
│ 2 │ ==> │ 2 6 · τ │
|
||
│ P │ CatmullRom │ P -P │ Bézier
|
||
│ 3 │ │ 4 2 │
|
||
│ P │ │ P - ────── │
|
||
└ 4 ┘ │ 3 6 · τ │
|
||
│ P │
|
||
└ 3 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/catmullconv/a323848e706c473833cda0b02bc220ef.svg"
|
||
width="241px"
|
||
height="85px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
Similarly, if we have a Bézier curve defined by four coordinates P<sub>1</sub> through P<sub>4</sub>, we can draw that using a standard
|
||
tension Catmull-Rom curve with the following coordinate values:
|
||
</p>
|
||
<!--
|
||
|
||
┌ P ┐ ┌ P ┐
|
||
│ 1 │ │ 1 │
|
||
│ P │ │ P │
|
||
│ 2 │ ==> │ 4 │
|
||
│ P │ Bézier │ P + 3(P - P ) │ CatmullRom
|
||
│ 3 │ │ 4 1 2 │
|
||
│ P │ │ P + 3(P - P ) │
|
||
└ 4 ┘ └ 1 4 3 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/catmullconv/9ae99b090883023a485be7be098858e9.svg"
|
||
width="276px"
|
||
height="77px"
|
||
loading="lazy"
|
||
/>
|
||
<p>Or, if your API allows you to specify Catmull-Rom curves using plain coordinates:</p>
|
||
<!--
|
||
|
||
┌ P ┐ ┌ P + 6(P - P ) ┐
|
||
│ 1 │ │ 4 1 2 │
|
||
│ P │ │ P │
|
||
│ 2 │ ==> │ 1 │
|
||
│ P │ Bézier │ P │ CatmullRom
|
||
│ 3 │ │ 4 │
|
||
│ P │ │ P + 6(P - P ) │
|
||
└ 4 ┘ └ 1 4 3 ┘
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/catmullconv/012a8ab7a4de935c1c8d61dcd14fc62c.svg"
|
||
width="276px"
|
||
height="77px"
|
||
loading="lazy"
|
||
/>
|
||
</section>
|
||
<section id="catmullfitting">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#catmullconv">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#polybezier">下</a></div>
|
||
<a href="zh-CN/index.html#catmullfitting">Creating a Catmull-Rom curve from three points</a>
|
||
</h1>
|
||
<p>
|
||
Much shorter than the previous section: we saw that Catmull-Rom curves need at least 4 points to draw anything sensible, so how do we
|
||
create a Catmull-Rom curve from three points?
|
||
</p>
|
||
<p>Short and sweet: we don't.</p>
|
||
<p>
|
||
We run through the maths that lets us <a href="#pointcurves">create a cubic Bézier curve</a>, and then convert its coordinates to
|
||
Catmull-Rom form using the conversion formulae we saw above.
|
||
</p>
|
||
</section>
|
||
<section id="polybezier">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#catmullfitting">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#offsetting">下</a></div>
|
||
<a href="zh-CN/index.html#polybezier">Forming poly-Bézier curves</a>
|
||
</h1>
|
||
<p>
|
||
Much like lines can be chained together to form polygons, Bézier curves can be chained together to form poly-Béziers, and the only trick
|
||
required is to make sure that:
|
||
</p>
|
||
<ol>
|
||
<li>the end point of each section is the starting point of the following section, and</li>
|
||
<li>the derivatives across that dual point line up.</li>
|
||
</ol>
|
||
<p>Unless you want sharp corners, of course. Then you don't even need 2.</p>
|
||
<p>
|
||
We'll cover three forms of poly-Bézier curves in this section. First, we'll look at the kind that just follows point 1. where the end
|
||
point of a segment is the same point as the start point of the next segment. This leads to poly-Béziers that are pretty hard to work with,
|
||
but they're the easiest to implement:
|
||
</p>
|
||
<graphics-element
|
||
title="Unlinked quadratic poly-Bézier"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/polybezier/poly.js"
|
||
data-type="quadratic"
|
||
data-link="coordinate"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/polybezier/41522a397171423e8a465dc8c74f6e87.png" loading="lazy" />
|
||
<label>Unlinked quadratic poly-Bézier</label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
<graphics-element
|
||
title="Unlinked cubic poly-Bézier"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/polybezier/poly.js"
|
||
data-type="cubic"
|
||
data-link="coordinate"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/polybezier/6fb33629373d7a731b6ac3f5365cb9f0.png" loading="lazy" />
|
||
<label>Unlinked cubic poly-Bézier</label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
<p>
|
||
Dragging the control points around only affects the curve segments that the control point belongs to, and moving an on-curve point leaves
|
||
the control points where they are, which is not the most useful for practical modelling purposes. So, let's add in the logic we need to
|
||
make things a little better. We'll start by linking up control points by ensuring that the "incoming" derivative at an on-curve point is
|
||
the same as it's "outgoing" derivative:
|
||
</p>
|
||
<!--
|
||
|
||
B'(1) = B'(0)
|
||
n n+1
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/polybezier/a37252ff55837b918d9d64078ae92ae7.svg" width="124px" height="17px" loading="lazy" />
|
||
<p>
|
||
We can effect this quite easily, because we know that the vector from a curve's last control point to its last on-curve point is equal to
|
||
the derivative vector. If we want to ensure that the first control point of the next curve matches that, all we have to do is mirror that
|
||
last control point through the last on-curve point. And mirroring any point A through any point B is really simple:
|
||
</p>
|
||
<!--
|
||
|
||
┌ B + (B - A ) ┐ ┌ 2B - A ┐
|
||
Mirrored = │ x x x │ = │ x x │
|
||
│ B + (B - A ) │ │ 2B - A │
|
||
└ y y y ┘ └ y y ┘
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/polybezier/2249056953a47ab1944bb5a41dcbed8c.svg" width="301px" height="40px" loading="lazy" />
|
||
<p>
|
||
So let's implement that and see what it gets us. The following two graphics show a quadratic and a cubic poly-Bézier curve again, but this
|
||
time moving the control points around moves others, too. However, you might see something unexpected going on for quadratic curves...
|
||
</p>
|
||
<graphics-element
|
||
title="Connected quadratic poly-Bézier"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/polybezier/poly.js"
|
||
data-type="quadratic"
|
||
data-link="derivative"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/polybezier/db04f805f42bdc9a1b7ec4d6b401d853.png" loading="lazy" />
|
||
<label>Connected quadratic poly-Bézier</label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
<graphics-element
|
||
title="Connected cubic poly-Bézier"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/polybezier/poly.js"
|
||
data-type="cubic"
|
||
data-link="derivative"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/polybezier/fe41b628f46f7035d151a8210d30111f.png" loading="lazy" />
|
||
<label>Connected cubic poly-Bézier</label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
<p>
|
||
As you can see, quadratic curves are particularly ill-suited for poly-Bézier curves, as all the control points are effectively linked.
|
||
Move one of them, and you move all of them. Not only that, but if we move the on-curve points, it's possible to get a situation where a
|
||
control point cannot satisfy the constraint that it's the reflection of its two neighbouring control points... This means that we cannot
|
||
use quadratic poly-Béziers for anything other than really, really simple shapes. And even then, they're probably the wrong choice. Cubic
|
||
curves are pretty decent, but the fact that the derivatives are linked means we can't manipulate curves as well as we might if we relaxed
|
||
the constraints a little.
|
||
</p>
|
||
<p>So: let's relax the requirement a little.</p>
|
||
<p>
|
||
We can change the constraint so that we still preserve the <em>angle</em> of the derivatives across sections (so transitions from one
|
||
section to the next will still look natural), but give up the requirement that they should also have the same <em>vector length</em>.
|
||
Doing so will give us a much more useful kind of poly-Bézier curve:
|
||
</p>
|
||
<graphics-element
|
||
title="Angularly connected quadratic poly-Bézier"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/polybezier/poly.js"
|
||
data-type="quadratic"
|
||
data-link="direction"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/polybezier/777f3814965c39ec3cdbb13eab0c4eeb.png" loading="lazy" />
|
||
<label>Angularly connected quadratic poly-Bézier</label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
<graphics-element
|
||
title="Angularly connected cubic poly-Bézier"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/polybezier/poly.js"
|
||
data-type="cubic"
|
||
data-link="direction"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/polybezier/f6c55cbc66333b6630939f67fc20e086.png" loading="lazy" />
|
||
<label>Angularly connected cubic poly-Bézier</label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
<p>
|
||
Cubic curves are now better behaved when it comes to dragging control points around, but the quadratic poly-Bézier still has the problem
|
||
that moving one control points will move the control points and may ending up defining "the next" control point in a way that doesn't
|
||
work. Quadratic curves really aren't very useful to work with...
|
||
</p>
|
||
<p>
|
||
Finally, we also want to make sure that moving the on-curve coordinates preserves the relative positions of the associated control points.
|
||
With that, we get to the kind of curve control that you might be familiar with from applications like Photoshop, Inkscape, Blender, etc.
|
||
</p>
|
||
<graphics-element
|
||
title="Standard connected quadratic poly-Bézier"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/polybezier/poly.js"
|
||
data-type="quadratic"
|
||
data-link="conventional"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/polybezier/b3aebf7803f4430187c249a891095062.png" loading="lazy" />
|
||
<label>Standard connected quadratic poly-Bézier</label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
<graphics-element
|
||
title="Standard connected cubic poly-Bézier"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/polybezier/poly.js"
|
||
data-type="cubic"
|
||
data-link="conventional"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/polybezier/1b94c6ada011bd8e50330e31a851a62e.png" loading="lazy" />
|
||
<label>Standard connected cubic poly-Bézier</label>
|
||
</fallback-image></graphics-element
|
||
>
|
||
<p>
|
||
Again, we see that cubic curves are now rather nice to work with, but quadratic curves have a new, very serious problem: we can move an
|
||
on-curve point in such a way that we can't compute what needs to "happen next". Move the top point down, below the left and right points,
|
||
for instance. There is no way to preserve correct control points without a kink at the bottom point. Quadratic curves: just not that
|
||
good...
|
||
</p>
|
||
<p>
|
||
A final improvement is to offer fine-level control over which points behave which, so that you can have "kinks" or individually controlled
|
||
segments when you need them, with nicely well-behaved curves for the rest of the path. Implementing that, is left as an exercise for the
|
||
reader.
|
||
</p>
|
||
</section>
|
||
<section id="offsetting">
|
||
<h1>
|
||
<div class="nav">
|
||
<a href="zh-CN/index.html#polybezier">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#graduatedoffset">下</a>
|
||
</div>
|
||
<a href="zh-CN/index.html#offsetting">Curve offsetting</a>
|
||
</h1>
|
||
<p>
|
||
Perhaps you're like me, and you've been writing various small programs that use Bézier curves in some way or another, and at some point
|
||
you make the step to implementing path extrusion. But you don't want to do it pixel based; you want to stay in the vector world. You find
|
||
that extruding lines is relatively easy, and tracing outlines is coming along nicely (although junction caps and fillets are a bit of a
|
||
hassle), and then you decide to do things properly and add Bézier curves to the mix. Now you have a problem.
|
||
</p>
|
||
<p>
|
||
Unlike lines, you can't simply extrude a Bézier curve by taking a copy and moving it around, because of the curvatures; rather than a
|
||
uniform thickness, you get an extrusion that looks too thin in places, if you're lucky, but more likely will self-intersect. The trick,
|
||
then, is to scale the curve, rather than simply copying it. But how do you scale a Bézier curve?
|
||
</p>
|
||
<p>
|
||
Bottom line: <strong>you can't</strong>. So you cheat. We're not going to do true curve scaling, or rather curve offsetting, because
|
||
that's impossible. Instead we're going to try to generate 'looks good enough' offset curves.
|
||
</p>
|
||
<div class="note">
|
||
<h3>"What do you mean, you can't? Prove it."</h3>
|
||
<p>
|
||
First off, when I say "you can't," what I really mean is "you can't offset a Bézier curve with another Bézier curve", not even by using
|
||
a really high order curve. You can find the function that describes the offset curve, but it won't be a polynomial, and as such it
|
||
cannot be represented as a Bézier curve, which <strong>has</strong> to be a polynomial. Let's look at why this is:
|
||
</p>
|
||
<p>
|
||
From a mathematical point of view, an offset curve <code>O(t)</code> is a curve such that, given our original curve <code>B(t)</code>,
|
||
any point on <code>O(t)</code> is a fixed distance <code>d</code> away from coordinate <code>B(t)</code>. So let's math that:
|
||
</p>
|
||
<!--
|
||
|
||
O(t) = B(t) + d
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/offsetting/3c80407cfd0bd8c8ebea239272aeabe5.svg"
|
||
width="108px"
|
||
height="16px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
However, we're working in 2D, and <code>d</code> is a single value, so we want to turn it into a vector. If we want a point distance
|
||
<code>d</code> "away" from the curve <code>B(t)</code> then what we really mean is that we want a point at <code>d</code> times the
|
||
"normal vector" from point <code>B(t)</code>, where the "normal" is a vector that runs perpendicular ("at a right angle") to the tangent
|
||
at <code>B(t)</code>. Easy enough:
|
||
</p>
|
||
<!--
|
||
|
||
O(t) = B(t) + d · N(t)
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/offsetting/de8cdb128273beff2d98534b9f090b85.svg"
|
||
width="151px"
|
||
height="16px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
Now this still isn't very useful unless we know what the formula for <code>N(t)</code> is, so let's find out. <code>N(t)</code> runs
|
||
perpendicular to the original curve tangent, and we know that the tangent is simply <code>B'(t)</code>, so we could just rotate that 90
|
||
degrees and be done with it. However, we need to ensure that <code>N(t)</code> has the same magnitude for every <code>t</code>, or the
|
||
offset curve won't be at a uniform distance, thus not being an offset curve at all. The easiest way to guarantee this is to make sure
|
||
<code>N(t)</code> always has length 1, which we can achieve by dividing <code>B'(t)</code> by its magnitude:
|
||
</p>
|
||
<!--
|
||
|
||
╭ B'(t) ╮
|
||
N(t) \bot │ ────────── │
|
||
╰ || B'(t)|| ╯
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/offsetting/57e62f3f2f7526b2cf7c1b276c17e472.svg"
|
||
width="120px"
|
||
height="40px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
Determining the length requires computing an arc length, and this is where things get Tricky with a capital T. First off, to compute arc
|
||
length from some start <code>a</code> to end <code>b</code>, we must use the formula we saw earlier. Noting that "length" is usually
|
||
denoted with double vertical bars:
|
||
</p>
|
||
<!--
|
||
|
||
┌───────────┐
|
||
╭ b │ 2 2
|
||
|| f(x,y)|| = | │f ' + f '
|
||
╯ a ⟍│ x y
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/offsetting/cf8e602eb0595cf4d9b851c6bda741af.svg"
|
||
width="169px"
|
||
height="36px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
So if we want the length of the tangent, we plug in <code>B'(t)</code>, with <code>t = 0</code> as start and <code>t = 1</code> as end:
|
||
</p>
|
||
<!--
|
||
|
||
┌───────────────────┐
|
||
╭ 1 │ 2 2
|
||
|| B'(t)|| = | │B ''(t) + B ''(t)
|
||
╯ 0 ⟍│ x y
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/offsetting/af4b584bb280cc941603255f62c9cc1a.svg"
|
||
width="209px"
|
||
height="36px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
And that's where things go wrong. It doesn't even really matter what the second derivative for <code>B(t)</code> is, that square root is
|
||
screwing everything up, because it turns our nice polynomials into things that are no longer polynomials.
|
||
</p>
|
||
<p>
|
||
There is a small class of polynomials where the square root is also a polynomial, but they're utterly useless to us: any polynomial with
|
||
unweighted binomial coefficients has a square root that is also a polynomial. Now, you might think that Bézier curves are just fine
|
||
because they do, but they don't; remember that only the <strong>base</strong> function has binomial coefficients. That's before we
|
||
factor in our coordinates, which turn it into a non-binomial polygon. The only way to make sure the functions stay binomial is to make
|
||
all our coordinates have the same value. And that's not a curve, that's a point. We can already create offset curves for points, we call
|
||
them circles, and they have much simpler functions than Bézier curves.
|
||
</p>
|
||
<p>
|
||
So, since the tangent length isn't a polynomial, the normalised tangent won't be a polynomial either, which means
|
||
<code>N(t)</code> won't be a polynomial, which means that <code>d</code> times <code>N(t)</code> won't be a polynomial, which means
|
||
that, ultimately, <code>O(t)</code> won't be a polynomial, which means that even if we can determine the function for
|
||
<code>O(t)</code> just fine (and that's far from trivial!), it simply cannot be represented as a Bézier curve.
|
||
</p>
|
||
<p>
|
||
And that's one reason why Bézier curves are tricky: there are actually a <em>lot</em> of curves that cannot be represented as a Bézier
|
||
curve at all. They can't even model their own offset curves. They're weird that way. So how do all those other programs do it? Well,
|
||
much like we're about to do, they cheat. We're going to approximate an offset curve in a way that will look relatively close to what the
|
||
real offset curve would look like, if we could compute it.
|
||
</p>
|
||
</div>
|
||
|
||
<p>
|
||
So, you cannot offset a Bézier curve perfectly with another Bézier curve, no matter how high-order you make that other Bézier curve.
|
||
However, we can chop up a curve into "safe" sub-curves (where "safe" means that all the control points are always on a single side of the
|
||
baseline, and the midpoint of the curve at <code>t=0.5</code> is roughly in the center of the polygon defined by the curve coordinates)
|
||
and then point-scale each sub-curve with respect to its scaling origin (which is the intersection of the point normals at the start and
|
||
end points).
|
||
</p>
|
||
<p>
|
||
A good way to do this reduction is to first find the curve's extreme points, as explained in the earlier section on curve extremities, and
|
||
use these as initial splitting points. After this initial split, we can check each individual segment to see if it's "safe enough" based
|
||
on where the center of the curve is. If the on-curve point for <code>t=0.5</code> is too far off from the center, we simply split the
|
||
segment down the middle. Generally this is more than enough to end up with safe segments.
|
||
</p>
|
||
<p>
|
||
The following graphics show off curve offsetting, and you can use the slider to control the distance at which the curve gets offset. The
|
||
curve first gets reduced to safe segments, each of which is then offset at the desired distance. Especially for simple curves,
|
||
particularly easily set up for quadratic curves, no reduction is necessary, but the more twisty the curve gets, the more the curve needs
|
||
to be reduced in order to get segments that can safely be scaled.
|
||
</p>
|
||
<graphics-element
|
||
title="Offsetting a quadratic Bézier curve"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/offsetting/offsetting.js"
|
||
data-type="quadratic"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/offsetting/03b8e0849e7c8ba64d8c076f47fe2ec7.png" loading="lazy" />
|
||
<label>Offsetting a quadratic Bézier curve</label>
|
||
</fallback-image>
|
||
<input type="range" min="5" max="50" step="1" value="20" class="slide-control" />
|
||
</graphics-element>
|
||
<graphics-element
|
||
title="Offsetting a cubic Bézier curve"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/offsetting/offsetting.js"
|
||
data-type="cubic"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/offsetting/4c4738b6bf9f83eded12d680a29e337b.png" loading="lazy" />
|
||
<label>Offsetting a cubic Bézier curve</label>
|
||
</fallback-image>
|
||
<input type="range" min="5" max="50" step="1" value="20" class="slide-control" />
|
||
</graphics-element>
|
||
<p>
|
||
You may notice that this may still lead to small 'jumps' in the sub-curves when moving the curve around. This is caused by the fact that
|
||
we're still performing a naive form of offsetting, moving the control points the same distance as the start and end points. If the curve
|
||
is large enough, this may still lead to incorrect offsets.
|
||
</p>
|
||
</section>
|
||
<section id="graduatedoffset">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#offsetting">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#circles">下</a></div>
|
||
<a href="zh-CN/index.html#graduatedoffset">Graduated curve offsetting</a>
|
||
</h1>
|
||
<p>
|
||
What if we want to do graduated offsetting, starting at some distance <code>s</code> but ending at some other distance <code>e</code>?
|
||
Well, if we can compute the length of a curve (which we can if we use the Legendre-Gauss quadrature approach) then we can also determine
|
||
how far "along the line" any point on the curve is. With that knowledge, we can offset a curve so that its offset curve is not uniformly
|
||
wide, but graduated between with two different offset widths at the start and end.
|
||
</p>
|
||
<p>
|
||
Like normal offsetting we cut up our curve in sub-curves, and then check at which distance along the original curve each sub-curve starts
|
||
and ends, as well as to which point on the curve each of the control points map. This gives us the distance-along-the-curve for each
|
||
interesting point in the sub-curve. If we call the total length of all sub-curves seen prior to seeing "the current" sub-curve
|
||
<code>S</code> (and if the current sub-curve is the first one, <code>S</code> is zero), and we call the full length of our original curve
|
||
<code>L</code>, then we get the following graduation values:
|
||
</p>
|
||
<ul>
|
||
<li>start: map <code>S</code> from interval (<code>0,L</code>) to interval <code>(s,e)</code></li>
|
||
<li>c1: <code>map(<strong>S+d1</strong>, 0,L, s,e)</code>, d1 = distance along curve to projection of c1</li>
|
||
<li>c2: <code>map(<strong>S+d2</strong>, 0,L, s,e)</code>, d2 = distance along curve to projection of c2</li>
|
||
<li>...</li>
|
||
<li>end: <code>map(<strong>S+length(subcurve)</strong>, 0,L, s,e)</code></li>
|
||
</ul>
|
||
<p>
|
||
At each of the relevant points (start, end, and the projections of the control points onto the curve) we know the curve's normal, so
|
||
offsetting is simply a matter of taking our original point, and moving it along the normal vector by the offset distance for each point.
|
||
Doing so will give us the following result (these have with a starting width of 0, and an end width of 40 pixels, but can be controlled
|
||
with your up and down arrow keys):
|
||
</p>
|
||
<graphics-element
|
||
title="Offsetting a quadratic Bézier curve"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/graduatedoffset/offsetting.js"
|
||
data-type="quadratic"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/graduatedoffset/8cc724d5343c65685d88c92b2d069a2a.png" loading="lazy" />
|
||
<label>Offsetting a quadratic Bézier curve</label>
|
||
</fallback-image>
|
||
<input type="range" min="5" max="50" step="1" value="20" class="slide-control" />
|
||
</graphics-element>
|
||
<graphics-element
|
||
title="Offsetting a cubic Bézier curve"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/graduatedoffset/offsetting.js"
|
||
data-type="cubic"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/graduatedoffset/17bf62e05a1fc3387b0c210f2decff45.png" loading="lazy" />
|
||
<label>Offsetting a cubic Bézier curve</label>
|
||
</fallback-image>
|
||
<input type="range" min="5" max="50" step="1" value="20" class="slide-control" />
|
||
</graphics-element>
|
||
</section>
|
||
<section id="circles">
|
||
<h1>
|
||
<div class="nav">
|
||
<a href="zh-CN/index.html#graduatedoffset">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#circles_cubic">下</a>
|
||
</div>
|
||
<a href="zh-CN/index.html#circles">Circles and quadratic Bézier curves</a>
|
||
</h1>
|
||
<p>
|
||
Circles and Bézier curves are very different beasts, and circles are infinitely easier to work with than Bézier curves. Their formula is
|
||
much simpler, and they can be drawn more efficiently. But, sometimes you don't have the luxury of using circles, or ellipses, or arcs.
|
||
Sometimes, all you have are Bézier curves. For instance, if you're doing font design, fonts have no concept of geometric shapes, they only
|
||
know straight lines, and Bézier curves. OpenType fonts with TrueType outlines only know quadratic Bézier curves, and OpenType fonts with
|
||
Type 2 outlines only know cubic Bézier curves. So how do you draw a circle, or an ellipse, or an arc?
|
||
</p>
|
||
<p>You approximate.</p>
|
||
<p>
|
||
We already know that Bézier curves cannot model all curves that we can think of, and this includes perfect circles, as well as ellipses,
|
||
and their arc counterparts. However, we can certainly approximate them to a degree that is visually acceptable. Quadratic and cubic curves
|
||
offer us different curvature control, so in order to approximate a circle we will first need to figure out what the error is if we try to
|
||
approximate arcs of increasing degree with quadratic and cubic curves, and where the coordinates even lie.
|
||
</p>
|
||
<p>
|
||
Since arcs are mid-point-symmetrical, we need the control points to set up a symmetrical curve. For quadratic curves this means that the
|
||
control point will be somewhere on a line that intersects the baseline at a right angle. And we don't get any choice on where that will
|
||
be, since the derivatives at the start and end point have to line up, so our control point will lie at the intersection of the tangents at
|
||
the start and end point.
|
||
</p>
|
||
<p>
|
||
First, let's try to fit the quadratic curve onto a circular arc. In the following sketch you can move the mouse around over a unit circle,
|
||
to see how well, or poorly, a quadratic curve can approximate the arc from (1,0) to where your mouse cursor is:
|
||
</p>
|
||
<graphics-element
|
||
title="Quadratic Bézier arc approximation"
|
||
width="400"
|
||
height="400"
|
||
src="./chapters/circles/arc-approximation.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="400px" height="400px" src="./images/chapters/circles/08ca09aacb271735e063e7e8d941a195.png" loading="lazy" />
|
||
<label></label>
|
||
</fallback-image>
|
||
<input type="range" min="-3.1415" max="3.1415" step="0.01" value="-0.7854" class="slide-control" />
|
||
</graphics-element>
|
||
<p>
|
||
As you can see, things go horribly wrong quite quickly; even trying to approximate a quarter circle using a quadratic curve is a bad idea.
|
||
An eighth of a turns might look okay, but how okay is okay? Let's apply some maths and find out. What we're interested in is how far off
|
||
our on-curve coordinates are with respect to a circular arc, given a specific start and end angle. We'll be looking at how much space
|
||
there is between the circular arc, and the quadratic curve's midpoint.
|
||
</p>
|
||
<p>
|
||
We start out with our start and end point, and for convenience we will place them on a unit circle (a circle around 0,0 with radius 1), at
|
||
some angle <em>φ</em>:
|
||
</p>
|
||
<!--
|
||
|
||
S = \begin{pmatrix} 1
|
||
0 \end{pmatrix} , \ E = \begin{pmatrix} cos(φ)
|
||
sin(φ) \end{pmatrix}
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/circles/7ab3da0922477af4cc09f5852100976b.svg" width="175px" height="40px" loading="lazy" />
|
||
<p>What we want to find is the intersection of the tangents, so we want a point C such that:</p>
|
||
<!--
|
||
|
||
C = S + a · \begin{pmatrix} 0
|
||
1 \end{pmatrix} , \ C = E + b · \begin{pmatrix} -sin(φ)
|
||
cos(φ) \end{pmatrix}
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/circles/d4bfb47b623c968e3231566c9705c6c4.svg" width="284px" height="40px" loading="lazy" />
|
||
<p>
|
||
i.e. we want a point that lies on the vertical line through S (at some distance <em>a</em> from S) and also lies on the tangent line
|
||
through E (at some distance <em>b</em> from E). Solving this gives us:
|
||
</p>
|
||
<!--
|
||
|
||
╭ C = 1 = cos(φ) + b · -sin(φ)
|
||
╡ x
|
||
│ C = a = sin(φ) + b · cos(φ)
|
||
╰ y
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/circles/8237af1396bb567d70c8b5e4dd7f8115.svg" width="219px" height="40px" loading="lazy" />
|
||
<p>First we solve for <em>b</em>:</p>
|
||
<!--
|
||
|
||
1 = cos(φ) + b · -sin(φ) → \ 1 - cos(φ) = -b · sin(φ) → \ -1 + cos(φ) = b · sin(φ)
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/circles/1829e42ea956ee4df0e45d9ac5334ef7.svg" width="560px" height="17px" loading="lazy" />
|
||
<p>which yields:</p>
|
||
<!--
|
||
|
||
cos(φ)-1
|
||
b = ────────
|
||
sin(φ)
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/circles/06369b00338310df0a810c592485aa0a.svg" width="101px" height="39px" loading="lazy" />
|
||
<p>which we can then substitute in the expression for <em>a</em>:</p>
|
||
<!--
|
||
|
||
a= sin(φ) + b · cos(φ)
|
||
-1 + cos(φ)
|
||
..= sin(φ) + ─────────── · cos(φ)
|
||
sin(φ)
|
||
2
|
||
-cos(φ) + cos (φ)
|
||
..= sin(φ) + ─────────────────
|
||
sin(φ)
|
||
2 2
|
||
sin (φ) + cos (φ) - cos(φ)
|
||
..= ──────────────────────────
|
||
sin(φ)
|
||
1 - cos(φ)
|
||
a= ──────────
|
||
sin(φ)
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/circles/5a23f3bc298c85540c6dd18e304d9224.svg" width="231px" height="195px" loading="lazy" />
|
||
<p>
|
||
A quick check shows that plugging these values for <em>a</em> and <em>b</em> into the expressions for C<sub>x</sub> and C<sub>y</sub> give
|
||
the same x/y coordinates for both "<em>a</em> away from A" and "<em>b</em> away from B", so let's continue: now that we know the
|
||
coordinate values for C, we know where our on-curve point T for <em>t=0.5</em> (or angle φ/2) is, because we can just evaluate the Bézier
|
||
polynomial, and we know where the circle arc's actual point P is for angle φ/2:
|
||
</p>
|
||
<!--
|
||
|
||
φ φ
|
||
P = cos(─) , \ P = sin(─)
|
||
x 2 y 2
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/circles/8b4e1d0a62380ed011f27c645ed13b28.svg" width="188px" height="32px" loading="lazy" />
|
||
<p>We compute T, observing that if <em>t=0.5</em>, the polynomial values (1-t)², 2(1-t)t, and t² are 0.25, 0.5, and 0.25 respectively:</p>
|
||
<!--
|
||
|
||
1 2 1 1
|
||
T = ─S + ─C + ─E = ─(S + 2C + E)
|
||
4 4 4 4
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/circles/a04bd1558a76e60b8ca6e1fe4fa38c00.svg" width="252px" height="35px" loading="lazy" />
|
||
<p>Which, worked out for the x and y components, gives:</p>
|
||
<!--
|
||
|
||
╭ 1
|
||
│ T = ─(3 + cos(φ))
|
||
╡ x 4
|
||
│ 1╭ 2-2cos(φ) ╮ 1╭ ╭ φ ╮ ╮
|
||
│ T = ─│ ───────── + sin(φ) │ = ─│ 2tan│ ─ │ + sin(φ) │
|
||
╰ y 4╰ sin(φ) ╯ 4╰ ╰ 2 ╯ ╯
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/circles/986ae9104e0bc52e95689ae7ae4504db.svg" width="408px" height="77px" loading="lazy" />
|
||
<p>And the distance between these two is the standard Euclidean distance:</p>
|
||
<!--
|
||
|
||
1 φ 4╭ φ ╮
|
||
d (φ)= T - P = ─(3 + cos(φ)) - cos(─) = 2sin │ ─ │ ,
|
||
x x x 4 2 ╰ 4 ╯
|
||
1╭ ╭ φ ╮ ╮ φ
|
||
d (φ)= T - P = ─│ 2tan│ ─ │ + sin(φ) │ - sin(─) ,
|
||
y y y 4╰ ╰ 2 ╯ ╯ 2
|
||
⇓
|
||
┌───────┐ ┌───────┐
|
||
│ 2 2 4 φ │ 1
|
||
d(φ)= │d + d = ... = 2sin (─) │───────
|
||
⟍│ x y 4 │ 2 φ
|
||
│cos (─)
|
||
⟍│ 2
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/circles/942c90bc8311e49d94059b3127fc78d5.svg" width="399px" height="153px" loading="lazy" />
|
||
<p>
|
||
So, what does this distance function look like when we plot it for a number of ranges for the angle φ, such as a half circle, quarter
|
||
circle and eighth circle?
|
||
</p>
|
||
<table>
|
||
<tbody>
|
||
<tr>
|
||
<td>
|
||
<img src="images/arc-q-pi.gif" height="190" />
|
||
plotted for 0 ≤ φ ≤ π:
|
||
</td>
|
||
<td>
|
||
<img src="images/arc-q-pi2.gif" height="187" />
|
||
plotted for 0 ≤ φ ≤ ½π:
|
||
</td>
|
||
<td>
|
||
<a
|
||
href="https://www.wolframalpha.com/input/?i=plot+sqrt%28%281%2F4+*+%28sin%28x%29+%2B+2tan%28x%2F2%29%29+-+sin%28x%2F2%29%29%5E2+%2B+%282sin%5E4%28x%2F4%29%29%5E2%29+for+0+%3C%3D+x+%3C%3D+pi%2F4"
|
||
>
|
||
<img src="images/arc-q-pi4.gif" height="174" />
|
||
</a>
|
||
plotted for 0 ≤ φ ≤ ¼π:
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<p>
|
||
We now see why the eighth circle arc looks decent, but the quarter circle arc doesn't: an error of roughly 0.06 at <em>t=0.5</em> means
|
||
we're 6% off the mark... we will already be off by one pixel on a circle with pixel radius 17. Any decent sized quarter circle arc, say
|
||
with radius 100px, will be way off if approximated by a quadratic curve! For the eighth circle arc, however, the error is only roughly
|
||
0.003, or 0.3%, which explains why it looks so close to the actual eighth circle arc. In fact, if we want a truly tiny error, like 0.001,
|
||
we'll have to contend with an angle of (rounded) 0.593667, which equates to roughly 34 degrees. We'd need 11 quadratic curves to form a
|
||
full circle with that precision! (technically, 10 and ten seventeenth, but we can't do partial curves, so we have to round up). That's a
|
||
whole lot of curves just to get a shape that can be drawn using a simple function!
|
||
</p>
|
||
<p>
|
||
In fact, let's flip the function around, so that if we plug in the precision error, labelled ε, we get back the maximum angle for that
|
||
precision:
|
||
</p>
|
||
<!--
|
||
|
||
╭ ┌─────────────┐ ╮
|
||
│ │ ┌──────┐ │
|
||
│ ⟍│2+ε-⟍│ε(2+ε) │
|
||
φ = 4 · arccos │ ──────────────── │
|
||
│ ┌─┐ │
|
||
╰ ⟍│2 ╯
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/circles/f9d15462df31186feef8c3d53c0f6163.svg" width="247px" height="53px" loading="lazy" />
|
||
<p>
|
||
And frankly, things are starting to look a bit ridiculous at this point, we're doing way more maths than we've ever done, but thankfully
|
||
this is as far as we need the maths to take us: If we plug in the precisions 0.1, 0.01, 0.001 and 0.0001 we get the radians values 1.748,
|
||
1.038, 0.594 and 0.3356; in degrees, that means we can cover roughly 100 degrees (requiring four curves), 59.5 degrees (requiring six
|
||
curves), 34 degrees (requiring 11 curves), and 19.2 degrees (requiring a whopping nineteen curves).
|
||
</p>
|
||
<p>
|
||
The bottom line? <strong>Quadratic curves are kind of lousy</strong> if you want circular (or elliptical, which are circles that have been
|
||
squashed in one dimension) curves. We can do better, even if it's just by raising the order of our curve once. So let's try the same thing
|
||
for cubic curves.
|
||
</p>
|
||
</section>
|
||
<section id="circles_cubic">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#circles">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#arcapproximation">下</a></div>
|
||
<a href="zh-CN/index.html#circles_cubic">Circular arcs and cubic Béziers</a>
|
||
</h1>
|
||
<p>Let's look at approximating circles and circular arcs using cubic Béziers. How much better is that?</p>
|
||
<graphics-element
|
||
title="Cubic Bézier arc approximation"
|
||
width="400"
|
||
height="400"
|
||
src="./chapters/circles_cubic/arc-approximation.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="400px" height="400px" src="./images/chapters/circles_cubic/891d50a946936c9701adc855de12623d.png" loading="lazy" />
|
||
<label></label>
|
||
</fallback-image>
|
||
<input type="range" min="-3.1415" max="3.1415" step="0.01" value="1.4" class="slide-control" />
|
||
</graphics-element>
|
||
<p>At cursory glance, a fair bit better, but let's find out <em>how much</em> better by looking at how to construct the Bézier curve.</p>
|
||
<p>
|
||
<img
|
||
src="images/chapter-assets/circles/image-20210417165543902.png"
|
||
alt="A construction diagram for a cubic approximation of a circular arc"
|
||
/>
|
||
</p>
|
||
<p>
|
||
The start and end points are trivial, but the mid point requires a bit of work, but it's mostly basic trigonometry once we know the angle
|
||
θ for our circular arc: if we scale our circular arc to a unit circle, we can always start our arc, with radius 1, at (1,0) and then given
|
||
our arc angle θ, we also know that the circular arc has length θ (because unit circles are nice that way). We also know our end point,
|
||
because that's just (cos(θ), sin(θ)), and so the challenge is to figure out what control points we need in order for the curve at
|
||
<em>t</em>=0.5 to exactly touch the circular arc at the angle θ/2:
|
||
</p>
|
||
<p>So let's again formally describe this:</p>
|
||
<!--
|
||
|
||
P = (1, 0)
|
||
1
|
||
P = (1, k)
|
||
2
|
||
P = P + k · (sin(θ), -cos(θ))
|
||
3 4
|
||
P = (cos(θ), sin(θ))
|
||
4
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/circles_cubic/9054528132317434ae2c0be27572d86b.svg"
|
||
width="208px"
|
||
height="85px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
Only P<sub>3</sub> isn't quite straight-forward here, and its description is based on the fact that the triangle (origin, P<sub>4</sub>,
|
||
P<sub>3</sub>) is a right angled triangle, with the distance between the origin and P<sub>4</sub> being 1 (because we're working with a
|
||
unit circle), and the distance between P<sub>4</sub> and P<sub>3</sub> being <em>k</em>, so that we can represent P<sub>3</sub> as "The
|
||
point P<sub>4</sub> plus the vector from the origin to P<sub>4</sub> but then rotated a quarter circle, counter-clockwise, and scaled by
|
||
<em>k</em>".
|
||
</p>
|
||
<p>
|
||
With that, we can determine the <em>y</em>-coordinates for A, B, e<sub>1</sub>, and e<sub>2</sub>, after which we have all the information
|
||
we need to determine what the value of <em>k</em> is. We can find these values by using (no surprise here) linear interpolation between
|
||
known points, as A is midway between P<sub>2</sub> and P<sub>3</sub>, e<sub>1</sub> is between A and "midway between P<sub>1</sub> and
|
||
P<sub>2</sub>" (which is "half height" P<sub>2</sub>), and so forth:
|
||
</p>
|
||
<!--
|
||
|
||
P + P
|
||
2 3
|
||
y y k + sin(θ) - k · cos(θ)
|
||
A = ───────── = ────────────────────────
|
||
y 2 2
|
||
1
|
||
A + ─P k + sin(θ) - k · cos(θ)
|
||
y 2 2 ──────────────────────── + ─
|
||
y 2 2 2k + sin(θ) + k · cos(θ)
|
||
e = ───────── = ──────────────────────────── = ─────────────────────────
|
||
1 2 2 4
|
||
y
|
||
k
|
||
A + mid(P , P ) A + sin(θ) - ─ cos(θ)
|
||
y 4 3 y 2 k + 3sin(θ) 2k · cos(θ)
|
||
e = ───────────────── = ────────────────────── = ────────────────────────
|
||
2 2 2 4
|
||
y
|
||
e + e
|
||
1 2
|
||
y y 3k + 4sin(θ) - 3k · cos(θ)
|
||
B = ───────── = ───────────────────────────
|
||
y 2 8
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/circles_cubic/e0d46f5fd4bf01e72f23495757f64448.svg"
|
||
width="505px"
|
||
height="173px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
Which now gives us two identities for B, because in addition to determining B through linear interpolation, we also know that B's
|
||
<em>y</em> coordinate is just <em>sin(θ/2)</em>: we started this exercise by saying we were going to approximate the circular arc using a
|
||
Bézier curve that had its midpoint, which is point B, touching the unit circle at the arc's half-angle, by definition making B the point
|
||
at (cos(θ/2), sin(θ/2)).
|
||
</p>
|
||
<p>This means we can equate the two identities we now have for B<sub>y</sub> and solve for <em>k</em>.</p>
|
||
<div class="note">
|
||
<h2>Deriving <em>k</em></h2>
|
||
<p>
|
||
Solving for <em>k</em> is fairly straight forward, but it's a fair few steps, and if you just the immediate result: using a tool like
|
||
<a href="https://www.wolframalpha.com/">Wolfram Alpha</a> is definitely the way to go. That said, let's get going:
|
||
</p>
|
||
<!--
|
||
|
||
3k + 4sin(θ)) - 3k · cos(θ) θ
|
||
────────────────────────────= sin(─)
|
||
8 2
|
||
╭ θ ╮
|
||
3k + 4sin(θ)) - 3k · cos(θ)= 8sin│ ─ │
|
||
╰ 2 ╯
|
||
╭ θ ╮
|
||
3k - 3k · cos(θ)= 8sin│ ─ │ - 4sin(θ)
|
||
╰ 2 ╯
|
||
╭ ╭ θ ╮ ╮
|
||
3k (1 - cos(θ))= 4 │ 2sin│ ─ │ - sin(θ) │
|
||
╰ ╰ 2 ╯ ╯
|
||
θ
|
||
2sin(─) - sin(θ)
|
||
2
|
||
3k= 4 · ────────────────
|
||
1 - cos(θ)
|
||
╭ θ ╮
|
||
2sin│ ─ │ - sin(θ)
|
||
4 ╰ 2 ╯
|
||
k= ─ · ──────────────────
|
||
3 1 - cos(θ)
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/circles_cubic/cb6686f1aff26d9f47ed4c695109fd5f.svg"
|
||
width="357px"
|
||
height="263px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
And finally, we can take further advantage of several trigonometric identities to <em>drastically</em> simplify our formula for
|
||
<em>k</em>:
|
||
</p>
|
||
<!--
|
||
|
||
╭ θ ╮
|
||
2sin│ ─ │ - sin(θ)
|
||
4 ╰ 2 ╯
|
||
k= ─ · ──────────────────
|
||
3 1 - cos(θ)
|
||
╭ ╭ θ ╮ ╮
|
||
│ 2sin│ ─ │ │
|
||
4 │ ╰ 2 ╯ sin(θ) │
|
||
k= ─ · │ ────────── - ────────── │
|
||
3 ╰ 1 - cos(θ) 1 - cos(θ) ╯
|
||
4 ╭ ╭ θ ╮ ╭ θ ╮ ╮
|
||
k= ─ · │ csc│ ─ │ - cot│ ─ │ │
|
||
3 ╰ ╰ 2 ╯ ╰ 2 ╯ ╯
|
||
4 ╭ θ ╮
|
||
k= ─ · tan│ ─ │
|
||
3 ╰ 4 ╯
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/circles_cubic/f65f4e30a9f7a08c5c0092a1a3853922.svg"
|
||
width="235px"
|
||
height="188px"
|
||
loading="lazy"
|
||
/>
|
||
<p>And we're done.</p>
|
||
</div>
|
||
|
||
<p>
|
||
So, the distance of our control points to the start/end points can be expressed as a number that we get from an almost trivial expression
|
||
involving the circular arc's angle:
|
||
</p>
|
||
<!--
|
||
|
||
4 ╭ θ ╮
|
||
k = f(θ) = ─ tan│ ─ │
|
||
3 ╰ 4 ╯
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/circles_cubic/f47561c3870425499e31c7527c38dc94.svg"
|
||
width="145px"
|
||
height="40px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
Which means that for any circular arc with angle θ and radius <em>r</em>, our Bézier approximation based on three points of incidence is:
|
||
</p>
|
||
<!--
|
||
|
||
start= (r, 0)
|
||
control = (r, k)
|
||
1
|
||
control = r · (cos(θ) + k · sin(θ), sin(θ) - k · cos(θ))
|
||
2
|
||
end= r · (cos(θ), sin(θ))
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/circles_cubic/d880c4a1b3d7b651b054b008e952b493.svg"
|
||
width="359px"
|
||
height="85px"
|
||
loading="lazy"
|
||
/>
|
||
<p>Which also gives us the commonly found value of 0.55228 for quarter circles, based on them having an angle of half π:</p>
|
||
<!--
|
||
|
||
╭ \pi ╮ 4 ╭ \pi ╮ 4 ┌─┐
|
||
f│ ─── │ = ─ · tan│ ─── │ = ─(⟍│2 -1) ≅ 0.55228474983[...]
|
||
╰ 2 ╯ 3 ╰ 8 ╯ 3
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/circles_cubic/acbde4be3cde3838b99b0ffc933f1f89.svg"
|
||
width="387px"
|
||
height="36px"
|
||
loading="lazy"
|
||
/>
|
||
<p>And thus giving us the following Bézier coordinates for a quarter circle of radius <em>r</em>:</p>
|
||
<!--
|
||
|
||
start= (r, 0)
|
||
control = (r, 0.55228 · r)
|
||
1
|
||
control = (0.55228 · r, r)
|
||
2
|
||
end= (0, r)
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/circles_cubic/ad4d512cb92280ac88af531309cdbb8c.svg"
|
||
width="177px"
|
||
height="85px"
|
||
loading="lazy"
|
||
/>
|
||
<div class="note">
|
||
<h2>So, how accurate is this?</h2>
|
||
<p>
|
||
Unlike for the quadratic curve, we can't use <i>t=0.5</i> as our reference point because by its very nature it's one of the three points
|
||
that are actually guaranteed to be on the circular arc itself. Instead, we need a different <i>t</i> value that will give us the maximum
|
||
deflection - there are two possible choices (as our curve is still strictly "overshoots" the circular arc, and it's symmetrical) but
|
||
rather than trying to use calculus to find the perfect <em>t</em> value—which we could! the maths is perfectly reasonable as long as we
|
||
get to use computers—we can also just perform a binary search for the biggest deflection and not bother with all this maths stuff.
|
||
</p>
|
||
<p>
|
||
So let's do that instead: we can run a maximum deflection check that just runs through <em>t</em> from 0 to 1 at some coarse interval,
|
||
finds a <em>t</em> value that has "the highest deflection of the bunch", then reruns the same check with a much smaller interval around
|
||
that <em>t</em> value, repeating as many times as necessary to get us an arbitrarily precise value of <em>t</em>:
|
||
</p>
|
||
|
||
<table class="code">
|
||
<tr>
|
||
<td>1</td>
|
||
<td rowspan="13">
|
||
<textarea disabled rows="13" role="doc-example">
|
||
getMostWrongT(radius, bezier, start, end, epsilon=1e-15):
|
||
if end-start < epsilon:
|
||
return (start+end)/2
|
||
worst_t = 0
|
||
max = 0
|
||
stepsize = (end-start)/10
|
||
for t=start to end, using stepsize:
|
||
p = bezier.get(t)
|
||
diff = p.magnitude() - radius
|
||
if diff > max:
|
||
worst_t = t
|
||
max = diff
|
||
return getMostWrongT(radius, bezier, worst_t - stepsize, worst_t + stepsize)</textarea
|
||
>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>7</td>
|
||
</tr>
|
||
<tr>
|
||
<td>8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>9</td>
|
||
</tr>
|
||
<tr>
|
||
<td>10</td>
|
||
</tr>
|
||
<tr>
|
||
<td>11</td>
|
||
</tr>
|
||
<tr>
|
||
<td>12</td>
|
||
</tr>
|
||
<tr>
|
||
<td>13</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p>Plus, how often do you get to write a function with that name?</p>
|
||
<p>
|
||
Using this code, we find that our <em>t</em> values are approximately 0.211325 and 0.788675, so let's pick the lower of the two and see
|
||
what the maximum deflection is across our domain of angles, with the original quadratic error show in green (rocketing off to infinity
|
||
first, and then coming back down as we approach 2π)
|
||
</p>
|
||
<table>
|
||
<tbody>
|
||
<tr>
|
||
<td>
|
||
<img src="images/chapter-assets/circles/image-20210417173811587.png" width="95%" />
|
||
</td>
|
||
<td>
|
||
<img src="images/chapter-assets/circles/image-20210417174019035.png" width="95%" />
|
||
</td>
|
||
<td>
|
||
<img src="images/chapter-assets/circles/image-20210417174100036.png" width="95%" />
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>
|
||
error plotted for 0 ≤ φ ≤ 2π
|
||
</td>
|
||
<td>
|
||
error plotted for 0 ≤ φ ≤ π
|
||
</td>
|
||
<td>
|
||
error plotted for 0 ≤ φ ≤ ½π
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<p>
|
||
That last image is probably not quite clear enough: the cubic approximation of a quarter circle is so incredibly much better that we
|
||
can't even really see it at the same scale of our quadratic curve. Let's scale the y-axis a little, and try that again:
|
||
</p>
|
||
<p style="text-align: center;"><img src="images/chapter-assets/circles/image-20210417174215876.png" height="350px" /></p>
|
||
|
||
<p>
|
||
Yeah... the error of a cubic approximation for a quarter circle turns out to be <em>two orders of magnitude</em> better. At
|
||
approximately 0.00027 (or: just shy of being 2.7 pixels off for a circle with a radius of 10,000 pixels) the increase in precision over
|
||
quadratic curves is quite spectacular - certainly good enough that no one in their right mind should ever use quadratic curves.
|
||
</p>
|
||
</div>
|
||
|
||
<p>So that's it, kappa is <em>4/3 · tan(θ/4)</em> , we're done! ...or are we?</p>
|
||
<h2>Can we do better?</h2>
|
||
<p>
|
||
Technically: yes, we can. But I'm going to prefix this section with "we can, and we should investigate that possibility, but let me warn
|
||
you up front that the result is <em>only</em> better if we're going to hard-code the values". We're about to get into the weeds and the
|
||
standard three-points-of-incidence value is so good already that for most applications, trying to do better won't make any sense at all.
|
||
</p>
|
||
<p>
|
||
So with that said: what we calculated above is an <em>upper bound</em> for a best fit Bézier curve for a circular arc: anywhere we don't
|
||
touch the circular arc in our approximation, we've "overshot" the arc. What if we dropped our value for <em>k</em> just a little, so that
|
||
the curve starts out as an over-estimation, but then crosses the circular arc, yielding an region of underestimation, and then crosses the
|
||
circular arc again, with another region of overestimation. This might give us a lower overall error, so let's see what we can do.
|
||
</p>
|
||
<p>First, let's express the total error (given circular arc angle θ, and some <em>k</em>) using standard calculus notation:</p>
|
||
<!--
|
||
|
||
┌───────────────────────┐
|
||
╭ 1 │ 2 2
|
||
erf (θ, k) = | \| │B (t,θ,k) + B (t,θ,k) - r\|dt
|
||
╯ 0 ⟍│ x y
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/circles_cubic/85c3dc0187f786b37f5fa5a7cc74d642.svg"
|
||
width="331px"
|
||
height="36px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
This says that the error function for a given angle and value of <em>k</em> is equal to the "infinite" sum of differences between our
|
||
curve and the circular arc, as we run <em>t</em> from 0 to 1, using an infinitely small step size. between subsequent <em>t</em> values.
|
||
</p>
|
||
<p>
|
||
Now, since we want to find the minimal error, that means we want to know where along this function things go from "error is getting
|
||
progressively less" to "error is increasing again", which means we want to know where its derivative is zero, which as mathematical
|
||
expression looks like:
|
||
</p>
|
||
<!--
|
||
|
||
╭ ┌───────┐ ╮
|
||
│ ╭ 1 │ 2 2 │ d
|
||
│ | \| │B + B - r\|dt │ ── = 0
|
||
╰ ╯ 0 ⟍│ x y ╯ dt
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/circles_cubic/a7dc2e51b90e89ec62e4a328d2b24635.svg"
|
||
width="209px"
|
||
height="40px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
And here we have the most direct application of the
|
||
<a href="https://en.wikipedia.org/wiki/Fundamental_theorem_of_calculus">Fundamental Theorem of Calculus</a>: the derivative and integral
|
||
are each other's inverse operations, so they cancel out, leaving us with our original function:
|
||
</p>
|
||
<!--
|
||
|
||
┌───────┐
|
||
│ 2 2
|
||
\| │B + B - r\| = 0, t ∈[0,1]
|
||
⟍│ x y
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/circles_cubic/b996b7c1af4c9187004af7d04b3740a5.svg"
|
||
width="207px"
|
||
height="27px"
|
||
loading="lazy"
|
||
/>
|
||
<p>And now we just solve for that... oh wait. We've seen this before. In order to solve this, we'd end up needing to solve this:</p>
|
||
<!--
|
||
|
||
2 2
|
||
B + B = r
|
||
x y
|
||
-->
|
||
<img
|
||
class="LaTeX SVG"
|
||
src="./images/chapters/circles_cubic/610a69d8be6f86744ffb88d12eda701b.svg"
|
||
width="81px"
|
||
height="23px"
|
||
loading="lazy"
|
||
/>
|
||
<p>
|
||
And both of those terms on the left of the equal sign are 6<sup>th</sup> degree polynomials, which means—as we've covered in the section
|
||
on arc lengths—<a href="https://en.wikipedia.org/wiki/Abel%E2%80%93Ruffini_theorem">there is no symbolic solution for this equasion</a>.
|
||
Instead, we'll have to use a numerical approach to find the solutions here, so... to the computer!
|
||
</p>
|
||
<div class="note">
|
||
<h2>Iterating on a solution</h2>
|
||
<p>
|
||
By which I really mean "to the binary search algorithm", because we're dealing with a reasonably well behaved function: depending on the
|
||
value for <em>k</em> , we're either going to end up with a Bézier curve that's on average "not at distance <em>r</em> from the arc's
|
||
center", "exactly distance <em>r</em> from the arc's center", or "more than distance <em>r</em> from the arc's center", so we can just
|
||
binary search our way to the most accurate value for <em>c</em> that gets us that middle case.
|
||
</p>
|
||
<p>First our setup, where we determine our upper and lower bounds, before entering our binary search:</p>
|
||
|
||
<table class="code">
|
||
<tr>
|
||
<td>1</td>
|
||
<td rowspan="4">
|
||
<textarea disabled rows="4" role="doc-example">
|
||
findBest(radius, angle, points[]):
|
||
lowerBound = 0
|
||
upperBound = 4.0/3.0 * Math.tan(abs(angle) / 4)
|
||
return binarySearch(radius, angle, points, lowerBound, upperBound)</textarea
|
||
>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p>
|
||
And then the binary search algorithm, which can be found in pretty much any CS textbook, as well as more online articles, tutorials, and
|
||
blog posts than you can ever read in a life time:
|
||
</p>
|
||
|
||
<table class="code">
|
||
<tr>
|
||
<td>1</td>
|
||
<td rowspan="19">
|
||
<textarea disabled rows="19" role="doc-example">
|
||
binarySearch(radius, angle, points[], lowerBound, upperBound, epsilon=1e-15):
|
||
value = (upperBound + lowerBound)/2
|
||
|
||
if (upperBound - lowerBound < epsilon) return value
|
||
|
||
// recompute the control points, based on our current "value"
|
||
d = (points[3].y < 0 ? -1 : 1) * value * radius
|
||
points[1] = new Point(radius, d)
|
||
points[2] = new Point(
|
||
points[3].x + d * sin(angle)
|
||
points[3].y - d * cos(angle)
|
||
)
|
||
|
||
if radialError(radius, points) > 0:
|
||
// our bezier curve is longer than we want it to be: reduce the upper bound
|
||
return binarySearch(radius, angle, points, lowerBound, value)
|
||
else:
|
||
// our bezier curve is shorter than we want it to be: increase the lower bound
|
||
return binarySearch(radius, angle, points, value, upperBound)</textarea
|
||
>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>7</td>
|
||
</tr>
|
||
<tr>
|
||
<td>8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>9</td>
|
||
</tr>
|
||
<tr>
|
||
<td>10</td>
|
||
</tr>
|
||
<tr>
|
||
<td>11</td>
|
||
</tr>
|
||
<tr>
|
||
<td>12</td>
|
||
</tr>
|
||
<tr>
|
||
<td>13</td>
|
||
</tr>
|
||
<tr>
|
||
<td>14</td>
|
||
</tr>
|
||
<tr>
|
||
<td>15</td>
|
||
</tr>
|
||
<tr>
|
||
<td>16</td>
|
||
</tr>
|
||
<tr>
|
||
<td>17</td>
|
||
</tr>
|
||
<tr>
|
||
<td>18</td>
|
||
</tr>
|
||
<tr>
|
||
<td>19</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p>
|
||
Using the following <code>radialError</code> function, which samples the curve's approximation of the circular arc over several points
|
||
(although the first and last point will never contribute anything, so we skip them):
|
||
</p>
|
||
|
||
<table class="code">
|
||
<tr>
|
||
<td>1</td>
|
||
<td rowspan="7">
|
||
<textarea disabled rows="7" role="doc-example">
|
||
radialError(radius, points[]):
|
||
err = 0
|
||
steps = 5.0
|
||
for (int i=1; i<steps; i++):
|
||
Point p = getOnCurvePoint(points, i/steps)
|
||
err += p.magnitude()/radius - 1
|
||
return err</textarea
|
||
>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>7</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p>
|
||
In this, <code>getOnCurvePoint</code> is just the standard Bézier evaluation function, yielding a point. Treating that point as a
|
||
vector, we can get its length to the origin using a <code>magnitude</code> call.
|
||
</p>
|
||
<h2>Examining the result</h2>
|
||
<p>
|
||
Running the above code we can get a list of <em>k</em> values associated with a list of angles θ from 0 to π, and we can use that to,
|
||
for each angle, plot what the difference between the circular arc and the Bézier approximation looks like:
|
||
</p>
|
||
<p><img src="images/chapter-assets/circles/image-20210419085430711.png" alt="image-20210419085430711" /></p>
|
||
<p>
|
||
Here we see the difference between an arc and its Bézier approximation plotted as we run <em>t</em> from 0 to 1. Just by looking at the
|
||
plot we can tell that there is maximum deflection at <em>t</em> = 0.5, so let's plot the maximum deflection "function", for angles from
|
||
0 to θ:
|
||
</p>
|
||
<p>In fact, let's plot the maximum deflections for both approaches as a functions over θ:</p>
|
||
<table>
|
||
<tbody>
|
||
<tr>
|
||
<td>
|
||
<img src="images/chapter-assets/circles/image-20210418111929371.png" width="95%" />
|
||
</td>
|
||
<td>
|
||
<img src="images/chapter-assets/circles/image-20210418112008676.png" width="95%" />
|
||
</td>
|
||
<td>
|
||
<img src="images/chapter-assets/circles/image-20210418112038613.png" width="95%" />
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>
|
||
max deflection using unit scale
|
||
</td>
|
||
<td>
|
||
max deflection at 10x scale
|
||
</td>
|
||
<td>
|
||
max deflection at 100x scale
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<p>That doesn't actually appear to be all that much better, so let's look at some numbers, to see what the improvement actually is:</p>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>angle</th>
|
||
<th>"improved" deflection</th>
|
||
<th>"upper bound" deflection</th>
|
||
<th>difference</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>1/8 π</td>
|
||
<td>6.202833502388927E-8</td>
|
||
<td>6.657161222278773E-8</td>
|
||
<td>4.5432771988984655E-9</td>
|
||
</tr>
|
||
<tr>
|
||
<td>1/4 π</td>
|
||
<td>3.978021202111215E-6</td>
|
||
<td>4.246252911066506E-6</td>
|
||
<td>2.68231708955291E-7</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3/8 π</td>
|
||
<td>4.547652269037972E-5</td>
|
||
<td>4.8397483513262785E-5</td>
|
||
<td>2.9209608228830675E-6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>1/2 π</td>
|
||
<td>2.569196199214696E-4</td>
|
||
<td>2.7251652752280364E-4</td>
|
||
<td>1.559690760133403E-5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5/8 π</td>
|
||
<td>9.877526288810667E-4</td>
|
||
<td>0.0010444175859711802</td>
|
||
<td>5.666495709011343E-5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3/4 π</td>
|
||
<td>0.00298164978679627</td>
|
||
<td>0.0031455628414580605</td>
|
||
<td>1.6391305466179062E-4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>7/8 π</td>
|
||
<td>0.0076323182807019885</td>
|
||
<td>0.008047777909948373</td>
|
||
<td>4.1545962924638413E-4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>π</td>
|
||
<td>0.017362185964043708</td>
|
||
<td>0.018349016519545902</td>
|
||
<td>9.86830555502194E-4</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<p>
|
||
As we can see, the increase in precision is not particularly big: for a quarter circle (π/2) the traditional <em>k</em> will be off by
|
||
2.75 pixels on a circle with radius 10,000 pixels, whereas this "better" fit will be off by 2.56 pixels. And while that's certainly an
|
||
almost 10% improvement, it's also nowhere near enough of an improvement to make a discernible difference.
|
||
</p>
|
||
</div>
|
||
|
||
<p>
|
||
At this point it should be clear that while, yes, there are improvement to be had, they're essentially insignificant while also being
|
||
<em>much</em> more computationally expensive.
|
||
</p>
|
||
<h2>TL;DR: just tell me which value I should be using</h2>
|
||
<p>
|
||
It depends on what we need to do. If we just want the best value for quarter circles, and we're going to hard code the value for
|
||
<em>k</em>, then there is no reason to hard-code the constant <code>k=4/3*tan(pi/8)</code> when you can just as easily hard-code the
|
||
constant as <code>k=0.551784777779014</code> instead.
|
||
</p>
|
||
<p><strong>If you need "the" value for quarter circles, use 0.551785 instead of 0.55228</strong></p>
|
||
<p>
|
||
However, for dynamic arc approximation, in code that tries to fit circular paths using Bézier paths instead, it should be fairly obvious
|
||
that the simple function involving a tangent computation, two divisions, and one multiplication, is vastly more performant than running
|
||
all the code we ended writing just to get a 25% lower error value, and most certainly worth preferring over getting the "more accurate"
|
||
value.
|
||
</p>
|
||
<p>
|
||
<strong>If you need to fit Béziers to circular arcs on the fly, use <code>4/3 * tan(θ/4)</code></strong>
|
||
</p>
|
||
<p>
|
||
However, always remember that if you're writing for humans, you can typically use the best of both worlds: as the user interacts with
|
||
their curves, you should draw <em>their curves</em> instead of drawing approximations of them. If they need to draw circles or circular
|
||
arcs, draw those, and only approximate them with a Bézier curve when the data needs to be exported to a format that doesn't support those.
|
||
Ideally with a preview mechanism that highlights where the errors will be, and how large they will be.
|
||
</p>
|
||
<p><strong>If you're writing code for graphics design by humans, use circular arcs for circular arcs</strong></p>
|
||
<p>
|
||
And that's it. We have pretty well exhausted this subject. There are different metrics we could use to find "different best
|
||
<em>k</em> values", like trying to match arc length (e.g. when we're optimizing for material cost), or minimizing the area between the
|
||
circular arc and the Bézier curve (e.g. when we're optimizing for inking), or minimizing the rate of change of the Bézier's curvature
|
||
(e.g. when we're optimizing for curve traversal) and they all yield values that are so similar that it's almost certainly not worth it.
|
||
(For instance, for quarter circle approximations those values are 0.551777, 0.5533344, and 0.552184 respectively. Much like the 0.551785
|
||
we get from minimizing the maximum deflection, none of these values are significantly better enough to prefer them over the upper bound
|
||
value).
|
||
</p>
|
||
</section>
|
||
<section id="arcapproximation">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#circles_cubic">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#bsplines">下</a></div>
|
||
<a href="zh-CN/index.html#arcapproximation">Approximating Bézier curves with circular arcs</a>
|
||
</h1>
|
||
<p>
|
||
Let's look at doing the exact opposite of the previous section: rather than approximating circular arc using Bézier curves, let's
|
||
approximate Bézier curves using circular arcs.
|
||
</p>
|
||
<p>
|
||
We already saw in the section on circle approximation that this will never yield a perfect equivalent, but sometimes you need circular
|
||
arcs, such as when you're working with fabrication machinery, or simple vector languages that understand lines and circles, but not much
|
||
else.
|
||
</p>
|
||
<p>
|
||
The approach is fairly simple: pick a starting point on the curve, and pick two points that are further along the curve. Determine the
|
||
circle that goes through those three points, and see if it fits the part of the curve we're trying to approximate. Decent fit? Try spacing
|
||
the points further apart. Bad fit? Try spacing the points closer together. Keep doing this until you've found the "good approximation/bad
|
||
approximation" boundary, record the "good" arc, and then move the starting point up to overlap the end point we previously found. Rinse
|
||
and repeat until we've covered the entire curve.
|
||
</p>
|
||
<p>
|
||
We already saw how to fit a circle through three points in the section on <a href="#pointcurves">creating a curve from three points</a>,
|
||
and finding the arc through those points is straight-forward: pick one of the three points as start point, pick another as an end point,
|
||
and the arc has to necessarily go from the start point, to the end point, over the remaining point.
|
||
</p>
|
||
<p>So, how can we convert a Bézier curve into a (sequence of) circular arc(s)?</p>
|
||
<ul>
|
||
<li>Start at <code>t=0</code></li>
|
||
<li>Pick two points further down the curve at some value <code>m = t + n</code> and <code>e = t + 2n</code></li>
|
||
<li>Find the arc that these points define</li>
|
||
<li>
|
||
Determine how close the found arc is to the curve:
|
||
<ul>
|
||
<li>Pick two additional points <code>e1 = t + n/2</code> and <code>e2 = t + n + n/2</code>.</li>
|
||
<li>
|
||
These points, if the arc is a good approximation of the curve interval chosen, should lie <code>on</code> the circle, so their
|
||
distance to the center of the circle should be the same as the distance from any of the three other points to the center.
|
||
</li>
|
||
<li>
|
||
For point points, determine the (absolute) error between the radius of the circle, and the <code>actual</code> distance from the
|
||
center of the circle to the point on the curve.
|
||
</li>
|
||
<li>If this error is too high, we consider the arc bad, and try a smaller interval.</li>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<p>
|
||
The result of this is shown in the next graphic: we start at a guaranteed failure: s=0, e=1. That's the entire curve. The midpoint is
|
||
simply at <code>t=0.5</code>, and then we start performing a
|
||
<a href="https://en.wikipedia.org/wiki/Binary_search_algorithm">binary search</a>.
|
||
</p>
|
||
<ol>
|
||
<li>We start with <code>low=0</code>, <code>mid=0.5</code> and <code>high=1</code></li>
|
||
<li>
|
||
That'll fail, so we retry with the interval halved: <code>{0, 0.25, 0.5}</code>
|
||
<ul>
|
||
<li>If that arc's good, we move back up by half distance: <code>{0, 0.375, 0.75}</code>.</li>
|
||
<li>However, if the arc was still bad, we move <em>down</em> by half the distance: <code>{0, 0.125, 0.25}</code>.</li>
|
||
</ul>
|
||
</li>
|
||
<li>
|
||
We keep doing this over and over until we have two arcs, in sequence, of which the first arc is good, and the second arc is bad. When we
|
||
find that pair, we've found the boundary between a good approximation and a bad approximation, and we pick the good arc.
|
||
</li>
|
||
</ol>
|
||
<p>
|
||
The following graphic shows the result of this approach, with a default error threshold of 0.5, meaning that if an arc is off by a
|
||
<em>combined</em> half pixel over both verification points, then we treat the arc as bad. This is an extremely simple error policy, but
|
||
already works really well. Note that the graphic is still interactive, and you can use your up and down arrow keys keys to increase or
|
||
decrease the error threshold, to see what the effect of a smaller or larger error threshold is.
|
||
</p>
|
||
<graphics-element
|
||
title="First arc approximation of a Bézier curve"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/arcapproximation/arc.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/arcapproximation/7c9cce8142fa3e85bb124520f40645ff.png" loading="lazy" />
|
||
<label>First arc approximation of a Bézier curve</label>
|
||
</fallback-image>
|
||
<input type="range" min="0.1" max="5" step="0.1" value="0.5" class="slide-control" />
|
||
</graphics-element>
|
||
<p>
|
||
With that in place, all that's left now is to "restart" the procedure by treating the found arc's end point as the new to-be-determined
|
||
arc's starting point, and using points further down the curve. We keep trying this until the found end point is for <em>t=1</em>, at which
|
||
point we are done. Again, the following graphic allows for up and down arrow key input to increase or decrease the error threshold, so you
|
||
can see how picking a different threshold changes the number of arcs that are necessary to reasonably approximate a curve:
|
||
</p>
|
||
<graphics-element
|
||
title="Arc approximation of a Bézier curve"
|
||
width="275"
|
||
height="275"
|
||
src="./chapters/arcapproximation/arcs.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="275px" height="275px" src="./images/chapters/arcapproximation/da76341b841df1af8a39f797e85dfe3c.png" loading="lazy" />
|
||
<label>Arc approximation of a Bézier curve</label>
|
||
</fallback-image>
|
||
<input type="range" min="0.1" max="5" step="0.1" value="0.5" class="slide-control" />
|
||
</graphics-element>
|
||
<p>
|
||
So... what is this good for? Obviously, if you're working with technologies that can't do curves, but can do lines and circles, then the
|
||
answer is pretty straightforward, but what else? There are some reasons why you might need this technique: using circular arcs means you
|
||
can determine whether a coordinate lies "on" your curve really easily (simply compute the distance to each circular arc center, and if any
|
||
of those are close to the arc radii, at an angle between the arc start and end, bingo, this point can be treated as lying "on the curve").
|
||
Another benefit is that this approximation is "linear": you can almost trivially travel along the arcs at fixed speed. You can also
|
||
trivially compute the arc length of the approximated curve (it's a bit like curve flattening). The only thing to bear in mind is that this
|
||
is a lossy equivalence: things that you compute based on the approximation are guaranteed "off" by some small value, and depending on how
|
||
much precision you need, arc approximation is either going to be super useful, or completely useless. It's up to you to decide which,
|
||
based on your application!
|
||
</p>
|
||
</section>
|
||
<section id="bsplines">
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#arcapproximation">前</a><a href="#toc">目录</a><a href="zh-CN/index.html#comments">下</a></div>
|
||
<a href="zh-CN/index.html#bsplines">B-Splines</a>
|
||
</h1>
|
||
<p>
|
||
No discussion on Bézier curves is complete without also giving mention of that other beast in the curve design space: B-Splines. Easily
|
||
confused to mean Bézier splines, that's not actually what they are; they are "basis function" splines, which makes a lot of difference,
|
||
and we'll be looking at those differences in this section. We're not going to dive as deep into B-Splines as we have for Bézier curves
|
||
(that would be an entire primer on its own) but we'll be looking at how B-Splines work, what kind of maths is involved in computing them,
|
||
and how to draw them based on a number of parameters that you can pick for individual B-Splines.
|
||
</p>
|
||
<p>
|
||
First off: B-Splines are <a href="https://en.wikipedia.org/wiki/Piecewise">piecewise</a>,
|
||
<a href="https://en.wikipedia.org/wiki/Spline_(mathematics)">polynomial interpolation curves</a>, where the "single curve" is built by
|
||
performing polynomial interpolation over a set of points, using a sliding window of a fixed number of points. For instance, a "cubic"
|
||
B-Spline defined by twelve points will have its curve built by evaluating the polynomial interpolation of four points, and the curve can
|
||
be treated as a lot of different sections, each controlled by four points at a time, such that the full curve consists of smoothly
|
||
connected sections defined by points {1,2,3,4}, {2,3,4,5}, ..., {8,9,10,11}, and finally {9,10,11,12}, for eight sections.
|
||
</p>
|
||
<p>
|
||
What do they look like? They look like this! Tap on the graphic to add more points, and move points around to see how they map to the
|
||
spline curve drawn.
|
||
</p>
|
||
<graphics-element
|
||
title="A B-Spline example"
|
||
width="600"
|
||
height="300"
|
||
src="./chapters/bsplines/basic.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="600px" height="300px" src="./images/chapters/bsplines/fe3a8ca5706f286d916e36699e237e51.png" loading="lazy" />
|
||
<label></label> </fallback-image
|
||
></graphics-element>
|
||
<p>
|
||
The important part to notice here is that we are <strong>not</strong> doing the same thing with B-Splines that we do for poly-Béziers or
|
||
Catmull-Rom curves: both of the latter simply define new sections as literally "new sections based on new points", so a 12 point cubic
|
||
poly-Bézier curve is actually impossible, because we start with a four point curve, and then add three more points for each section that
|
||
follows, so we can only have 4, 7, 10, 13, 16, etc. point Poly-Béziers. Similarly, while Catmull-Rom curves can grow by adding single
|
||
points, this addition of a single point introduces three implicit Bézier points. Cubic B-Splines, on the other hand, are smooth
|
||
interpolations of <em>each possible curve involving four consecutive points</em>, such that at any point along the curve except for our
|
||
start and end points, our on-curve coordinate is defined by four control points.
|
||
</p>
|
||
<p>Consider the difference to be this:</p>
|
||
<ul>
|
||
<li>for Bézier curves, the curve is defined as an interpolation of points, but:</li>
|
||
<li>for B-Splines, the curve is defined as an interpolation of <em>curves</em>.</li>
|
||
</ul>
|
||
<p>In fact, let's look at that again, but this time with the base curves shown, too. Each consecutive four points define one curve:</p>
|
||
<graphics-element
|
||
title="The components of a B-Spline "
|
||
width="600"
|
||
height="300"
|
||
src="./chapters/bsplines/basic.js"
|
||
data-show-curves="true"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="600px" height="300px" src="./images/chapters/bsplines/41167c64c51386414c6e62f0b45e6295.png" loading="lazy" />
|
||
<label></label>
|
||
</fallback-image>
|
||
<!-- basis curve highlighter goes here -->
|
||
</graphics-element>
|
||
<p>
|
||
In order to make this interpolation of curves work, the maths is necessarily more complex than the maths for Bézier curves, so let's have
|
||
a look at how things work.
|
||
</p>
|
||
<h2>How to compute a B-Spline curve: some maths</h2>
|
||
<p>
|
||
Given a B-Spline of degree <code>d</code> and thus order <code>k=d+1</code> (so a quadratic B-Spline is degree 2 and order 3, a cubic
|
||
B-Spline is degree 3 and order 4, etc) and <code>n</code> control points <code>P<sub>0</sub></code> through <code>P<sub>n-1</sub></code
|
||
>, we can compute a point on the curve for some value <code>t</code> in the interval [0,1] (where 0 is the start of the curve, and 1 the
|
||
end, just like for Bézier curves), by evaluating the following function:
|
||
</p>
|
||
<!--
|
||
|
||
__ n
|
||
Point(t) = ❯ P · N (t)
|
||
‾‾ i=0 i i,k
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/bsplines/89f8e37237d066fa70ccf6d37b3a4922.svg" width="169px" height="41px" loading="lazy" />
|
||
<p>
|
||
Which, honestly, doesn't tell us all that much. All we can see is that a point on a B-Spline curve is defined as "a mix of all the control
|
||
points, weighted somehow", where the weighting is achieved through the <em>N(...)</em> function, subscripted with an obvious parameter
|
||
<code>i</code>, which comes from our summation, and some magical parameter <code>k</code>. So we need to know two things: 1. what does
|
||
N(t) do, and 2. what is that <code>k</code>? Let's cover both, in reverse order.
|
||
</p>
|
||
<p>
|
||
The parameter <code>k</code> represents the "knot interval" over which a section of curve is defined. As we learned earlier, a B-Spline
|
||
curve is itself an interpolation of curves, and we can treat each transition where a control point starts or stops influencing the total
|
||
curvature as a "knot on the curve". Doing so for a degree <code>d</code> B-Spline with <code>n</code> control point gives us
|
||
<code>d + n + 1</code> knots, defining <code>d + n</code> intervals along the curve, and it is these intervals that the above
|
||
<code>k</code> subscript to the N() function applies to.
|
||
</p>
|
||
<p>Then the N() function itself. What does it look like?</p>
|
||
<!--
|
||
|
||
╭ t- knot ╮ ╭ knot -t ╮
|
||
│ i │ │ (i+k) │
|
||
N (t) = │ ───────────────────── │ · N (t) + │ ─────────────────────── │ · N (t)
|
||
i,k │ knot - knot │ i,k-1 │ knot - knot │ i+1,k-1
|
||
╰ (i+k-1) i ╯ ╰ (i+k) (i+1) ╯
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/bsplines/2421f47aa4fe1c0d830d53b2e6563c04.svg" width="556px" height="43px" loading="lazy" />
|
||
<p>
|
||
So this is where we see the interpolation: N(t) for an <code>(i,k)</code> pair (that is, for a step in the above summation, on a specific
|
||
knot interval) is a mix between N(t) for <code>(i,k-1)</code> and N(t) for <code>(i+1,k-1)</code>, so we see that this is a recursive
|
||
iteration where <code>i</code> goes up, and <code>k</code> goes down, so it seem reasonable to expect that this recursion has to stop at
|
||
some point; obviously, it does, and specifically it does so for the following <code>i</code>/<code>k</code> values:
|
||
</p>
|
||
<!--
|
||
|
||
╭ 1 if t ∈[ knot , knot )
|
||
N (t) = ╡ i i+1
|
||
i,1 ╰ 0 otherwise
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/bsplines/2514e1aa0565840e33fde0b146e3efe2.svg" width="237px" height="40px" loading="lazy" />
|
||
<p>
|
||
And this function finally has a straight up evaluation: if a <code>t</code> value lies within a knot-specific interval once we reach a
|
||
<code>k=1</code> value, it "counts", otherwise it doesn't. We did cheat a little, though, because for all these values we need to scale
|
||
our <code>t</code> value first, so that it lies in the interval bounded by <code>knots[d]</code> and <code>knots[n]</code>, which are the
|
||
start point and end point where curvature is controlled by exactly <code>order</code> control points. For instance, for degree 3 (=order
|
||
4) and 7 control points, with knot vector [1,2,3,4,5,6,7,8,9,10,11], we map <code>t</code> from [the interval 0,1] to the interval [4,8],
|
||
and then use that value in the functions above, instead.
|
||
</p>
|
||
<h2>Can we simplify that?</h2>
|
||
<p>We can, yes.</p>
|
||
<p>
|
||
People far smarter than us have looked at this work, and two in particular —
|
||
<a href="https://www.npl.co.uk/people/maurice-cox">Maurice Cox</a> and
|
||
<a href="https://en.wikipedia.org/wiki/Carl_R._de_Boor">Carl de Boor</a> — came to a mathematically pleasing solution: to compute a point
|
||
P(t), we can compute this point by evaluating <em>d(t)</em> on a curve section between knots <code>i</code> and <code>i+1</code>:
|
||
</p>
|
||
<!--
|
||
|
||
k k-1 k-1
|
||
d (t) = α · d (t) + (1-α ) · d (t)
|
||
i i,k i i,k i-1
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/bsplines/f0bf7d0f1931060cd801ff707f482c16.svg" width="281px" height="21px" loading="lazy" />
|
||
<p>
|
||
This is another recursive function, with <em>k</em> values decreasing from the curve order to 1, and the value <em>α</em> (alpha) defined
|
||
by:
|
||
</p>
|
||
<!--
|
||
|
||
t - knots[i]
|
||
α = ───────────────────────────
|
||
i,k knots[i+1+n-k] - knots[i]
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/bsplines/e62558cdfd8abaf22511e8e68c7afb4a.svg" width="252px" height="39px" loading="lazy" />
|
||
<p>
|
||
That looks complicated, but it's not. Computing alpha is just a fraction involving known, plain numbers. And, once we have our alpha
|
||
value, we also have <code>(1-alpha)</code> because it's a trivial subtraction. Computing the <code>d()</code> function is thus mostly a
|
||
matter of computing pretty simple arithmetical statements, with some caching of results so we can refer to them as we recurve. While the
|
||
recursion might see computationally expensive, the total algorithm is cheap, as each step only involves very simple maths.
|
||
</p>
|
||
<p>Of course, the recursion does need a stop condition:</p>
|
||
<!--
|
||
|
||
k 0 ╭ 1 if t ∈[ knot , knot )
|
||
d (t) = 0, d (t) = N (t) = ╡ i i+1
|
||
0 i i,1 ╰ 0 otherwise
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/bsplines/49af474c33ce0ee0733626ea3d988570.svg" width="367px" height="40px" loading="lazy" />
|
||
<p>
|
||
So, we actually see two stopping conditions: either <code>i</code> becomes 0, in which case <code>d()</code> is zero, or
|
||
<code>k</code> becomes zero, in which case we get the same "either 1 or 0" that we saw in the N() function above.
|
||
</p>
|
||
<p>
|
||
Thanks to Cox and de Boor, we can compute points on a B-Spline pretty easily using the same kind of linear interpolation we saw in de
|
||
Casteljau's algorithm. For instance, if we write out <code>d()</code> for <code>i=3</code> and <code>k=3</code>, we get the following
|
||
recursion diagram:
|
||
</p>
|
||
<!--
|
||
|
||
╭ ╭ ╭ 1 0 0
|
||
│ │ │ α × d , with d either 0 or 1
|
||
│ │ 2 1 1 │ 3 3 3
|
||
│ │ α × d , with d = ╡ +
|
||
│ │ 3 3 3 │ ╭ 1 ╮ 0 0
|
||
│ │ │ │ 1 - α │ × d , with d either 0 or 1
|
||
│ 3 2 2 │ ╰ ╰ 3 ╯ 2 2
|
||
│ α × d , with d = ╡ +
|
||
│ 3 3 3 │ ╭ 1 0
|
||
│ │ │ α × d
|
||
│ │ ╭ 2 ╮ 1 1 │ 2 2
|
||
│ │ │ 1 - α │ × d , with d = ╡ +
|
||
3 │ │ ╰ 3 ╯ 2 2 │ ╭ 1 ╮ 0 0
|
||
d = ╡ │ │ │ 1 - α │ × d , with d either 0 or 1
|
||
3 │ ╰ ╰ ╰ 2 ╯ 1 1
|
||
│ +
|
||
│ ╭ 2 1
|
||
│ │ α × d
|
||
│ │ 2 2
|
||
│ │
|
||
│ ╭ 3 ╮ 2 2 │ +
|
||
│ │ 1 - α │ × d , with d = ╡ ╭ 1 0
|
||
│ ╰ 3 ╯ 2 2 │ │ α × d
|
||
│ │ ╭ 2 ╮ 1 1 │ 1 1
|
||
│ │ │ 1 - α │ × d , with d = ╡ +
|
||
│ │ ╰ 2 ╯ 1 1 │ ╭ 1 ╮ 0 0
|
||
│ │ │ │ 1 - α │ × d , with d either 0 or 1
|
||
╰ ╰ ╰ ╰ 1 ╯ 0 0
|
||
-->
|
||
<img class="LaTeX SVG" src="./images/chapters/bsplines/20e910bbea2e6eff511cb13cef18ef3b.svg" width="641px" height="336px" loading="lazy" />
|
||
<p>
|
||
That is, we compute <code>d(3,3)</code> as a mixture of <code>d(2,3)</code> and <code>d(2,2)</code>, where those two are themselves a
|
||
mixture of <code>d(1,3)</code> and <code>d(1,2)</code>, and <code>d(1,2)</code> and <code>d(1,1)</code>, respectively, which are
|
||
themselves a mixture of etc. etc. We simply keep expanding our terms until we reach the stop conditions, and then sum everything back up.
|
||
It's really quite elegant.
|
||
</p>
|
||
<p>
|
||
One thing we need to keep in mind is that we're working with a spline that is constrained by its control points, so even though the
|
||
<code>d(..., k)</code> values are zero or one at the lowest level, they are really "zero or one, times their respective control point", so
|
||
in the next section you'll see the algorithm for running through the computation in a way that starts with a copy of the control point
|
||
vector and then works its way up to that single point, rather than first starting "on the left", working our way "to the right" and then
|
||
summing back up "to the left". We can just start on the right and work our way left immediately.
|
||
</p>
|
||
<h2>Running the computation</h2>
|
||
<p>
|
||
Unlike the de Casteljau algorithm, where the <code>t</code> value stays the same at every iteration, for B-Splines that is not the case,
|
||
and so we end having to (for each point we evaluate) run a fairly involving bit of recursive computation. The algorithm is discussed on
|
||
<a href="https://pages.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/de-Boor.html">this Michigan Tech</a> page, but an easier to read version
|
||
is implemented by <a href="https://github.com/thibauts/b-spline/blob/master/index.js#L59-L71">b-spline.js</a>, so we'll look at its code.
|
||
</p>
|
||
<p>
|
||
Given an input value <code>t</code>, we first map the input to a value from the domain <code>[0,1]</code> to the domain
|
||
<code>[knots[degree], knots[knots.length - 1 - degree]</code>. Then, we find the section number <code>s</code> that this mapped
|
||
<code>t</code> value lies on:
|
||
</p>
|
||
|
||
<table class="code">
|
||
<tr>
|
||
<td>1</td>
|
||
<td rowspan="3">
|
||
<textarea disabled rows="3" role="doc-example">
|
||
for(s=domain[0]; s < domain[1]; s++) {
|
||
if(knots[s] <= t && t <= knots[s+1]) break;
|
||
}</textarea
|
||
>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p>
|
||
after running this code, <code>s</code> is the index for the section the point will lie on. We then run the algorithm mentioned on the MU
|
||
page (updated to use this description's variable names):
|
||
</p>
|
||
|
||
<table class="code">
|
||
<tr>
|
||
<td>1</td>
|
||
<td rowspan="10">
|
||
<textarea disabled rows="10" role="doc-example">
|
||
let v = copy of control points
|
||
|
||
for(let L = 1; L <= order; L++) {
|
||
for(let i=s; i > s + L - order; i--) {
|
||
let numerator = t - knots[i]
|
||
let denominator = knots[i - L + order] - knots[i]
|
||
let alpha = numerator / denominator
|
||
let v[i] = alpha * v[i] + (1-alpha) * v[i-1]
|
||
}
|
||
}</textarea
|
||
>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>7</td>
|
||
</tr>
|
||
<tr>
|
||
<td>8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>9</td>
|
||
</tr>
|
||
<tr>
|
||
<td>10</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p>
|
||
(A nice bit of behaviour in this code is that we work the interpolation "backwards", starting at <code>i=s</code> at each level of the
|
||
interpolation, and we stop when <code>i = s - order + level</code>, so we always end up with a value for <code>i</code> such that those
|
||
<code>v[i-1]</code> don't try to use an array index that doesn't exist)
|
||
</p>
|
||
<h2>Open vs. closed paths</h2>
|
||
<p>
|
||
Much like poly-Béziers, B-Splines can be either open, running from the first point to the last point, or closed, where the first and last
|
||
point are the same coordinate. However, because B-Splines are an interpolation of curves, not just points, we can't simply make the first
|
||
and last point the same, we need to link as many points as are necessary to form "a curve" that the spline performs interpolation with. As
|
||
such, for an order <code>d</code> B-Spline, we need to make the first and last <code>d</code> points the same. This is of course hardly
|
||
more work than before (simply append <code>points.splice(0,d)</code> to <code>points</code>) but it's important to remember that you need
|
||
more than just a single point.
|
||
</p>
|
||
<p>
|
||
Of course if we want to manipulate these kind of curves we need to make sure to mark them as "closed" so that we know the coordinate for
|
||
<code>points[0]</code> and <code>points[n-k]</code> etc. don't just happen to have the same x/y values, but really are the same
|
||
coordinate, so that manipulating one will equally manipulate the other, but programming generally makes this really easy by storing
|
||
references to points, rather than copies (or other linked values such as coordinate weights, discussed in the NURBS section) rather than
|
||
separate coordinate objects.
|
||
</p>
|
||
<h2>Manipulating the curve through the knot vector</h2>
|
||
<p>
|
||
The most important thing to understand when it comes to B-Splines is that they work <em>because</em> of the concept of a knot vector. As
|
||
mentioned above, knots represent "where individual control points start/stop influencing the curve", but we never looked at the
|
||
<em>values</em> that go in the knot vector. If you look back at the N() and a() functions, you see that interpolations are based on
|
||
intervals in the knot vector, rather than the actual values in the knot vector, and we can exploit this to do some pretty interesting
|
||
things with clever manipulation of the knot vector. Specifically there are four things we can do that are worth looking at:
|
||
</p>
|
||
<ol>
|
||
<li>we can use a uniform knot vector, with equally spaced intervals,</li>
|
||
<li>we can use a non-uniform knot vector, without enforcing equally spaced intervals,</li>
|
||
<li>we can collapse sequential knots to the same value, locally lowering curve complexity using "null" intervals, and</li>
|
||
<li>
|
||
we can form a special case non-uniform vector, by combining (1) and (3) to for a vector with collapsed start and end knots, with a
|
||
uniform vector in between.
|
||
</li>
|
||
</ol>
|
||
<h3>Uniform B-Splines</h3>
|
||
<p>
|
||
The most straightforward type of B-Spline is the uniform spline. In a uniform spline, the knots are distributed uniformly over the entire
|
||
curve interval. For instance, if we have a knot vector of length twelve, then a uniform knot vector would be [0,1,2,3,...,9,10,11]. Or
|
||
[4,5,6,...,13,14,15], which defines <em>the same intervals</em>, or even [0,2,3,...,18,20,22], which also defines
|
||
<em>the same intervals</em>, just scaled by a constant factor, which becomes normalised during interpolation and so does not contribute to
|
||
the curvature.
|
||
</p>
|
||
<graphics-element
|
||
title="A uniform B-Spline"
|
||
width="400"
|
||
height="400"
|
||
src="./chapters/bsplines/uniform.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="400px" height="400px" src="./images/chapters/bsplines/48a30189e74658737b3a8b28bb816f8a.png" loading="lazy" />
|
||
<label></label>
|
||
</fallback-image>
|
||
<!-- knot sliders go here -->
|
||
</graphics-element>
|
||
<p>
|
||
This is an important point: the intervals that the knot vector defines are <em>relative</em> intervals, so it doesn't matter if every
|
||
interval is size 1, or size 100 - the relative differences between the intervals is what shapes any particular curve.
|
||
</p>
|
||
<p>
|
||
The problem with uniform knot vectors is that, as we need <code>order</code> control points before we have any curve with which we can
|
||
perform interpolation, the curve does not "start" at the first point, nor "ends" at the last point. Instead there are "gaps". We can get
|
||
rid of these, by being clever about how we apply the following uniformity-breaking approach instead...
|
||
</p>
|
||
<h3>Reducing local curve complexity by collapsing intervals</h3>
|
||
<p>
|
||
Collapsing knot intervals, by making two or more consecutive knots have the same value, allows us to reduce the curve complexity in the
|
||
sections that are affected by the knots involved. This can have drastic effects: for every interval collapse, the curve order goes down,
|
||
and curve continuity goes down, to the point where collapsing <code>order</code> knots creates a situation where all continuity is lost
|
||
and the curve "kinks".
|
||
</p>
|
||
<graphics-element
|
||
title="A reduced uniform B-Spline"
|
||
width="400"
|
||
height="400"
|
||
src="./chapters/bsplines/reduced.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="400px" height="400px" src="./images/chapters/bsplines/ceaef2fbee05a58aa11835925403b4cd.png" loading="lazy" />
|
||
<label></label>
|
||
</fallback-image>
|
||
<!-- knot sliders go here -->
|
||
</graphics-element>
|
||
<h3>Open-Uniform B-Splines</h3>
|
||
<p>
|
||
By combining knot interval collapsing at the start and end of the curve, with uniform knots in between, we can overcome the problem of the
|
||
curve not starting and ending where we'd kind of like it to:
|
||
</p>
|
||
<p>
|
||
For any curve of degree <code>D</code> with control points <code>N</code>, we can define a knot vector of length <code>N+D+1</code> in
|
||
which the values <code>0 ... D+1</code> are the same, the values <code>D+1 ... N+1</code> follow the "uniform" pattern, and the values
|
||
<code>N+1 ... N+D+1</code> are the same again. For example, a cubic B-Spline with 7 control points can have a knot vector
|
||
[0,0,0,0,1,2,3,4,4,4,4], or it might have the "identical" knot vector [0,0,0,0,2,4,6,8,8,8,8], etc. Again, it is the relative differences
|
||
that determine the curve shape.
|
||
</p>
|
||
<graphics-element
|
||
title="An open, uniform B-Spline"
|
||
width="400"
|
||
height="400"
|
||
src="./chapters/bsplines/uniform.js"
|
||
data-open="true"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="400px" height="400px" src="./images/chapters/bsplines/0215dc106e4ad51afe043c0176a595f6.png" loading="lazy" />
|
||
<label></label>
|
||
</fallback-image>
|
||
<!-- knot sliders go here -->
|
||
</graphics-element>
|
||
<h3>Non-uniform B-Splines</h3>
|
||
<p>
|
||
This is essentially the "free form" version of a B-Spline, and also the least interesting to look at, as without any specific reason to
|
||
pick specific knot intervals, there is nothing particularly interesting going on. There is one constraint to the knot vector, other than
|
||
that any value <code>knots[k+1]</code> should be greater than or equal to <code>knots[k]</code>.
|
||
</p>
|
||
<h2>One last thing: Rational B-Splines</h2>
|
||
<p>
|
||
While it is true that this section on B-Splines is running quite long already, there is one more thing we need to talk about, and that's
|
||
"Rational" splines, where the rationality applies to the "ratio", or relative weights, of the control points themselves. By introducing a
|
||
ratio vector with weights to apply to each control point, we greatly increase our influence over the final curve shape: the more weight a
|
||
control point carries, the closer to that point the spline curve will lie, a bit like turning up the gravity of a control point, just like
|
||
for rational Bézier curves.
|
||
</p>
|
||
<graphics-element
|
||
title="A (closed) rational, uniform B-Spline"
|
||
width="400"
|
||
height="400"
|
||
src="./chapters/bsplines/rational-uniform.js"
|
||
reset="重启"
|
||
viewSource="view source"
|
||
>
|
||
<fallback-image>
|
||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||
<img width="400px" height="400px" src="./images/chapters/bsplines/0d9c2186423466a32bb8fbd187409f82.png" loading="lazy" />
|
||
<label></label>
|
||
</fallback-image>
|
||
<!-- knot sliders go here -->
|
||
</graphics-element>
|
||
<p>
|
||
Of course this brings us to the final topic that any text on B-Splines must touch on before calling it a day: the
|
||
<a href="https://en.wikipedia.org/wiki/Non-uniform_rational_B-spline">NURBS</a>, or Non-Uniform Rational B-Spline (NURBS is not a plural,
|
||
the capital S actually just stands for "spline", but a lot of people mistakenly treat it as if it is, so now you know better). NURBS is an
|
||
important type of curve in computer-facilitated design, used a lot in 3D modelling (typically as NURBS surfaces) as well as in
|
||
arbitrary-precision 2D design due to the level of control a NURBS curve offers designers.
|
||
</p>
|
||
<p>
|
||
While a true non-uniform rational B-Spline would be hard to work with, when we talk about NURBS we typically mean the Open-Uniform
|
||
Rational B-Spline, or OURBS, but that doesn't roll off the tongue nearly as nicely, and so remember that when people talk about NURBS,
|
||
they typically mean open-uniform, which has the useful property of starting the curve at the first control point, and ending it at the
|
||
last.
|
||
</p>
|
||
<h2>Extending our implementation to cover rational splines</h2>
|
||
<p>
|
||
The algorithm for working with Rational B-Splines is virtually identical to the regular algorithm, and the extension to work in the
|
||
control point weights is fairly simple: we extend each control point from a point in its original number of dimensions (2D, 3D, etc.) to
|
||
one dimension higher, scaling the original dimensions by the control point's weight, and then assigning that weight as its value for the
|
||
extended dimension.
|
||
</p>
|
||
<p>For example, a 2D point <code>(x,y)</code> with weight <code>w</code> becomes a 3D point <code>(w * x, w * y, w)</code>.</p>
|
||
<p>
|
||
We then run the same algorithm as before, which will automatically perform weight interpolation in addition to regular coordinate
|
||
interpolation, because all we've done is pretended we have coordinates in a higher dimension. The algorithm doesn't really care about how
|
||
many dimensions it needs to interpolate.
|
||
</p>
|
||
<p>
|
||
In order to recover our "real" curve point, we take the final result of the point generation algorithm, and "unweigh" it: we take the
|
||
final point's derived weight <code>w'</code> and divide all the regular coordinate dimensions by it, then throw away the weight
|
||
information.
|
||
</p>
|
||
<p>
|
||
Based on our previous example, we take the final 3D point <code>(x', y', w')</code>, which we then turn back into a 2D point by computing
|
||
<code>(x'/w', y'/w')</code>. And that's it, we're done!
|
||
</p>
|
||
</section>
|
||
<section id="comments">
|
||
<script src="./js/site/disqus.js" async defer>
|
||
/* ----------------------------------------------------------------------------- *
|
||
*
|
||
* PLEASE DO NOT LOCALISE THIS FILE
|
||
*
|
||
* I can't respond to questions that aren't asked in English, so this is one of
|
||
* the few cases where there is a content.en-GB.md but you should not localize it.
|
||
*
|
||
* ----------------------------------------------------------------------------- */
|
||
</script>
|
||
|
||
<h1>
|
||
<div class="nav"><a href="zh-CN/index.html#bsplines">前</a><a href="#toc">目录</a></div>
|
||
<a href="zh-CN/index.html#comments">Comments and questions</a>
|
||
</h1>
|
||
<p>
|
||
First off, if you enjoyed this book, or you simply found it useful for something you were trying to get done, and you were wondering how
|
||
to let me know you appreciated this book, you have two options: you can either head on over to the
|
||
<a href="https://www.patreon.com/bezierinfo">Patreon page</a> for this book, or if you prefer to make a one-time donation, head on over to
|
||
the <a href="https://www.paypal.com/donate/?cmd=_s-xclick&hosted_button_id=3BNHGHZAS3DP6&locale.x=en_CA">buy Pomax a coffee</a> page. This
|
||
work has grown from a small primer to a 70-plus print-page-equivalent reader on the subject of Bézier curves over the years, and a lot of
|
||
coffee went into the making of it. I don't regret a minute I spent on writing it, but I can always do with some more coffee to keep on
|
||
writing.
|
||
</p>
|
||
<p>With that said, on to the comments!</p>
|
||
<div id="disqus_thread" />
|
||
</section>
|
||
</section>
|
||
</main>
|
||
|
||
<hr />
|
||
|
||
<footer class="copyright">
|
||
This article is © 2011-2020 to me, Mike "Pomax" Kamermans, but the text, code, and images are
|
||
<a href="https://github.com/Pomax/bezierinfo/blob/master/LICENSE.md">almost no rights reserved</a>. Go do something cool with it!
|
||
</footer>
|
||
|
||
<script>
|
||
if (window.location.hash.includes(`#comment-`)) {
|
||
const baseHash = window.location.hash;
|
||
document.addEventListener(`disqus:ready`, () => {
|
||
console.log(`setting location`);
|
||
window.location.hash = ``;
|
||
window.location.hash = baseHash;
|
||
});
|
||
document.getElementById(`disqus_thread`).scrollIntoView();
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|