diff --git a/404.html b/404.html index 703a976..2bd4451 100644 --- a/404.html +++ b/404.html @@ -4,13 +4,13 @@ Page Not Found | Brush Rendering Tutorial - +
Skip to main content

Page Not Found

We could not find what you were looking for.

Please contact the owner of the site that linked you to the original URL and let them know their link is broken.

- + \ No newline at end of file diff --git a/About/index.html b/About/index.html index 2f1dcdd..1f2b076 100644 --- a/About/index.html +++ b/About/index.html @@ -4,7 +4,7 @@ About | Brush Rendering Tutorial - + @@ -40,7 +40,7 @@ That's why I want to guide and inspire more researchers and engineers to work on them during my academic career. To begin with, I've created this tutorial series to help everybody learn my research. I'm looking forward to assisting my collaborators working on the same research topic in the future.

- + \ No newline at end of file diff --git a/Appendix/Vector-fill/index.html b/Appendix/Vector-fill/index.html index dffa3d3..06854eb 100644 --- a/Appendix/Vector-fill/index.html +++ b/Appendix/Vector-fill/index.html @@ -4,7 +4,7 @@ Pre-introduction to Vector Fill | Brush Rendering Tutorial - + @@ -23,7 +23,7 @@ algorithms to locate a point better than the naive solution. To dive deeper, you may try using the CGAL 2D arrangement library and learn the arrangement constructed from 2D polylines, which is slightly different with a naive polygon mesh.

I hope this "2D mesh metaphor" will help you better understand the problems.

- + \ No newline at end of file diff --git a/Basics/Vanilla/index.html b/Basics/Vanilla/index.html index 1a40ad0..861c6b8 100644 --- a/Basics/Vanilla/index.html +++ b/Basics/Vanilla/index.html @@ -4,13 +4,13 @@ Vanilla | Brush Rendering Tutorial - +
Skip to main content

Vanilla

note

Shader code will be introduced in this article. Feel free to play with.

Loading...

TODO

- + \ No newline at end of file diff --git a/Introduction/index.html b/Introduction/index.html index 48efa7f..49f9097 100644 --- a/Introduction/index.html +++ b/Introduction/index.html @@ -3,53 +3,30 @@ -Introduction | Brush Rendering Tutorial - +Introduction | Brush Rendering Tutorial +
-
Skip to main content

Introduction

This tutorial series will teach you how to use the modern GPU graphics pipeline to render brush strokes on vector curves. -The contents mainly come from my research work Ciallo: The next generation vector paint program. -I will introduce this tutorial from the three aspects above.

Modern GPU

sketchpad

Draw lines in Ivan Sutherland's Sketchpad.

Drawing lines or rendering strokes is one of the oldest topics in Computer Graphics. -You can easily find a lot of pioneering works, for example, Bresenham's line algorithm. -They emerged from an era with certain conditions:

  • Programs ran without the benefit of parallelization.
  • Programs could access framebuffer directly without significant performance penalty.

But time has changed, now we have modern GPU hardware crafted for graphics and parallel computing, -and directly accessing a GPU framebuffer from a CPU can significantly hurt the performance. -So old algorithms may not satisfy your needs for real-time rendering.

In this tutorial, you will learn about the brush stroke rendering algorithms designed for the GPU graphics pipeline. -We (I and my mentor Liyi-Wei) call these algorithms Articulated in our paper, because they look like drawing an articulated arm. -I assume our readers are already familiar with a graphics API like OpenGL or D3D. -This tutorial will concentrate more on the high-level algorithms than the implementation details.

Although graphics APIs provide us line primitives, including LINES, LINE_STRIP, and LINE_LOOP, -there are several well-known issues when using these primitives directly. -Check out Matt Deslauries' article Drawing Lines is Hard if you know nothing about them. -As for our brush rendering, the most significant issue is the limitation on the maximum line width or stroke radius (half width). -We must be able to fully control the radius values when rendering brush strokes.

Brush strokes

Brush strokes refer to strokes drawn with the paint tool in graphics software such as Photoshop or Krita. -Artists configure their digital brushes to control stroke properties like radius or stylization, -then stroke on the canvas with dedicated input devices: Tablet and Stylus. -If you're unfamiliar with tablets and styluses, you can watch the video below for more information:

Tablet

While you may recognize a brush stroke by its stylization, another crucial property could be ignored: the variable radius along the stroke. -(I ignored it in my paper too.) -The radii are typically generated from the pressure values as a stylus presses and moves on a tablet. -For experienced artists after installing a painting program, one of the highest priorities is to configure the mapping function from pen pressure to brush radius.

In this tutorial, you will learn to render a stroke with variable radius, and the most popular way to stylize it called "Stamp." -More than 90 percent of brushes in popular paint software are the stamp brushes. -Additionally, GPU brush stroke rendering a newly emerged topic. -Researchers will develop more novel methods in the future. -So I will continuously update this tutorial series to teach them. -Make sure to star our code repository for easy access to the latest updates.

Vector curves

Variable radius is imperative for the most artists working on digital painting, -but it's not included in public vector standards like SVG. -And since that, configuring the variable width value of vector lines is commonly underdeveloped in popular graphics design software. -This limitation is one of the primary reasons that lots of digital artists don't use vector workflow. -(Another one is filling color.)

To support the variable radius, we will render a unique type of vector curve: -An ordered list of points (polyline) with radius values assigned to each point. -As a stylus is pressed and moved on a tablet, the program generate a sequence of points to record the trace of movement. -Additionally, the pen pressure is transformed into the radius value assigned to each point.

We can approximate any type of curve by increasing the number of points in a polyline, whether freehand-drawn or mathematically defined. -Try to change the maxRadius and segmentCount value in the code editor below to see how the stroke changes. -Feel free to change any other parts of the code as long as the function return the position and radius array correctly.

Loading...
code editor & canvas

The development environment is inspired by The Book of Shader. -You can watch the rendering result in real time after modifying the code.

When hovering your mouse on the canvas you can:

  • Pan: Left-click and drag the mouse.
  • Zoom: Scroll or drag the mouse wheel.

If there are bugs for common usages, tell me at the issue page.

Structure

Although the algorithms are very straightforward, I know it's hard to learn and reproduce a research work. -That's why I created this tutorial, designed with a smooth learning curve and providing the seamless development environment.

You should start with the Basic part, which covers the basics of the rendering methods. -Remember to read the articles in the Basic part in its original order, or you may miss something important. -Next, select your favorite topics to learn. -I will list extra prerequisites at the very beginning of each article.

Wish you happy learning!

Citation

@inproceedings{Ciallo2023,
author = {Ciao, Shen and Wei, Li-Yi},
title = {Ciallo: The next-Generation Vector Paint Program},
year = {2023},
isbn = {9798400701436},
publisher = {Association for Computing Machinery},
address = {New York, NY, USA},
url = {https://doi.org/10.1145/3587421.3595418},
doi = {10.1145/3587421.3595418},
booktitle = {ACM SIGGRAPH 2023 Talks},
articleno = {67},
numpages = {2},
keywords = {Digital painting, stylized stroke, arrangement, vector graphics. coloring, graphics processing unit (GPU)},
location = {Los Angeles, CA, USA},
series = {SIGGRAPH '23}
}
Research Tip

To demonstrate your research work about brush rendering, select vector drawings have variable radius or pen pressure data. -Regular vector drawing datasets don't contain them.

- +
Skip to main content

Introduction

This tutorial series will teach you how to use the modern GPU graphics pipeline to render brush strokes, +those are commonly seen with a paint tool in graphics design software like Photoshop.

Loading...
Vanilla
Loading...
Pencil (Stamp)

note

When hovering your mouse on the canvas you can:

  • Pan: Left-click and drag the mouse.
  • Zoom: Scroll or drag the mouse wheel.

The contents mainly come from my research work Ciallo: The next generation vector paint program. +Since there will be more research work on GPU brush stroke rendering, +I will continuously update this tutorial series to teach you related techniques in (potentially) influential research works.

Prerequisites

Decent experience in one of the GPU graphics APIs like OpenGL and D3D is required. +If you were relatively new to computer graphics, you should at least have rendered your first 3D scene and practiced instanced rendering. +In this tutorial, we will learn techniques to render and stylize curves.

Though I create all the demos in the web environment, you don't need to know about WebGL or WebGPU. +We will concentrate on high-level techniques rather than the implementation details. +No matter which GPU API you are familiar with, utilizing them to render a stroke will be easy after this tutorial series.

Structure

Content

The Basic section covers the basics of the rendering and stylization methods. +Articles in the Basic part are organized in a linear fashion. +You may miss something important if skip one of them. +After learning all stuffs in the Basic section, you can select your favorite topics in the TOC to learn. +I will list extra prerequisites at the very beginning of each article.

Live coding

You will find live code editors like the one below. which is inspired by The Book of Shader. +The rendering result is updated in real time after modifying the code. +Try it out by changing the variable maxRadius and segmentCount and watching the changes in the canvas below.

Loading...
code editor

If there are bugs for common usages, tell me at the issue page.

Feel free to change any other parts of the code as long as it returns the position and radius array correctly.

Only geometry generation code "geometry.js" is demonstrated here. +You will find "vertex.glsl" and "fragment.glsl" for vertex and fragment shader code. +Whether they are hidden or shown will depend on the context.

Citation

@inproceedings{Ciallo2023,
author = {Ciao, Shen and Wei, Li-Yi},
title = {Ciallo: The next-Generation Vector Paint Program},
year = {2023},
isbn = {9798400701436},
publisher = {Association for Computing Machinery},
address = {New York, NY, USA},
url = {https://doi.org/10.1145/3587421.3595418},
doi = {10.1145/3587421.3595418},
booktitle = {ACM SIGGRAPH 2023 Talks},
articleno = {67},
numpages = {2},
keywords = {Digital painting, stylized stroke, arrangement, vector graphics. coloring, graphics processing unit (GPU)},
location = {Los Angeles, CA, USA},
series = {SIGGRAPH '23}
}
Research Tip

To demonstrate your research work about brush rendering, select vector drawings have variable radius or pen pressure data. +Regular vector drawing datasets don't contain them.

+ \ No newline at end of file diff --git a/Tessellation/index.html b/Tessellation/index.html index 50ed28e..6342564 100644 --- a/Tessellation/index.html +++ b/Tessellation/index.html @@ -4,16 +4,16 @@ Tessellation-based Rendering | Brush Rendering Tutorial - +
Skip to main content

Tessellation-based Rendering

There were works trying to tessellate a stroke and render it with GPU. You can find them in several papers and online articles.

Paper and article list

They may inspire some approaches to optimize the performance of the articulated algorithms in the future. -To learn about them, I would recommend Rye Terrell's Instanced Line Rendering, -whose methods are simple enough to learn.

As for this article, I will compare the tessellation-based algorithms with articulated algorithms in detail.

TODO

spoiler

The key benefit of the articulated is its self-overlapping in acute angle, which is critical for brush strokes and digital painting.

- +To learn about them, I would recommend starting with Rye Terrell's Instanced Line Rendering, +whose methods are simple enough to learn.

As for this article, I will compare the tessellation-based algorithms with articulated algorithms in detail.

WIP

spoiler

The key benefit of the articulated is its self-overlapping in acute angle, which is critical for brush strokes and digital painting.

+ \ No newline at end of file diff --git a/assets/images/sketchpad-be46fe81f29f99371fdce79d1452ae85.gif b/assets/images/sketchpad-be46fe81f29f99371fdce79d1452ae85.gif deleted file mode 100644 index fbbd274..0000000 Binary files a/assets/images/sketchpad-be46fe81f29f99371fdce79d1452ae85.gif and /dev/null differ diff --git a/assets/js/26251b8b.2343b74d.js b/assets/js/26251b8b.088d1b64.js similarity index 87% rename from assets/js/26251b8b.2343b74d.js rename to assets/js/26251b8b.088d1b64.js index 5491c43..00e43d2 100644 --- a/assets/js/26251b8b.2343b74d.js +++ b/assets/js/26251b8b.088d1b64.js @@ -1 +1 @@ -"use strict";(self.webpackChunkbrush_stroke_tutorial=self.webpackChunkbrush_stroke_tutorial||[]).push([[147],{3905:(e,t,r)=>{r.d(t,{Zo:()=>c,kt:()=>h});var n=r(7294);function a(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function i(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function l(e){for(var t=1;t=0||(a[r]=e[r]);return a}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,r)&&(a[r]=e[r])}return a}var s=n.createContext({}),p=function(e){var t=n.useContext(s),r=t;return e&&(r="function"==typeof e?e(t):l(l({},t),e)),r},c=function(e){var t=p(e.components);return n.createElement(s.Provider,{value:t},e.children)},u="mdxType",d={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},m=n.forwardRef((function(e,t){var r=e.components,a=e.mdxType,i=e.originalType,s=e.parentName,c=o(e,["components","mdxType","originalType","parentName"]),u=p(r),m=a,h=u["".concat(s,".").concat(m)]||u[m]||d[m]||i;return r?n.createElement(h,l(l({ref:t},c),{},{components:r})):n.createElement(h,l({ref:t},c))}));function h(e,t){var r=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var i=r.length,l=new Array(i);l[0]=m;var o={};for(var s in t)hasOwnProperty.call(t,s)&&(o[s]=t[s]);o.originalType=e,o[u]="string"==typeof e?e:a,l[1]=o;for(var p=2;p{r.r(t),r.d(t,{assets:()=>s,contentTitle:()=>l,default:()=>d,frontMatter:()=>i,metadata:()=>o,toc:()=>p});var n=r(7462),a=(r(7294),r(3905));const i={sidebar_position:4,sidebar_label:"Tessellation",title:"Tessellation-based Rendering"},l=void 0,o={unversionedId:"Tessellation/Tessellation",id:"Tessellation/Tessellation",title:"Tessellation-based Rendering",description:"There were works trying to tessellate a stroke and render it with GPU.",source:"@site/docs/Tessellation/Tessellation.mdx",sourceDirName:"Tessellation",slug:"/Tessellation/",permalink:"/brush-rendering-tutorial/Tessellation/",draft:!1,editUrl:"https://github.com/ShenCiao/brush-rendering-tutorial/tree/main/docs/Tessellation/Tessellation.mdx",tags:[],version:"current",sidebarPosition:4,frontMatter:{sidebar_position:4,sidebar_label:"Tessellation",title:"Tessellation-based Rendering"},sidebar:"tutorialSidebar",previous:{title:"Vanilla",permalink:"/brush-rendering-tutorial/Basics/Vanilla/"},next:{title:"Appendix",permalink:"/brush-rendering-tutorial/category/appendix"}},s={},p=[],c={toc:p},u="wrapper";function d(e){let{components:t,...r}=e;return(0,a.kt)(u,(0,n.Z)({},c,r,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("p",null,"There were works trying to tessellate a stroke and render it with GPU.\nYou can find them in several papers and online articles."),(0,a.kt)("details",null,(0,a.kt)("summary",null,"Paper and article list"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("a",{parentName:"li",href:"https://dl.acm.org/doi/abs/10.1145/3386569.3392458"},"Polar Stroking: New Theory and Methods for Stroking Paths")," (Very Hard)"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("a",{parentName:"li",href:"https://mattdesl.svbtle.com/drawing-lines-is-hard"},"Drawing Lines is Hard")),(0,a.kt)("li",{parentName:"ul"},"Instanced Line Rendering: ",(0,a.kt)("a",{parentName:"li",href:"https://wwwtyro.net/2019/11/18/instanced-lines.html"},"part1")," | ",(0,a.kt)("a",{parentName:"li",href:"https://wwwtyro.net/2021/10/01/instanced-lines-part-2.html"},"part2")),(0,a.kt)("li",{parentName:"ul"},"... Tell me more tutorials in the ",(0,a.kt)("a",{parentName:"li",href:"https://github.com/ShenCiao/brush-rendering-tutorial/discussions/2"},"discussion")))),(0,a.kt)("p",null,"They may inspire some approaches to optimize the performance of the articulated algorithms in the future.\nTo learn about them, I would recommend Rye Terrell's ",(0,a.kt)("a",{parentName:"p",href:"https://wwwtyro.net/2019/11/18/instanced-lines.html"},"Instanced Line Rendering"),",\nwhose methods are simple enough to learn."),(0,a.kt)("p",null,"As for this article, I will compare the tessellation-based algorithms with articulated algorithms in detail."),(0,a.kt)("h1",{id:"todo"},"TODO"),(0,a.kt)("admonition",{title:"spoiler",type:"note"},(0,a.kt)("p",{parentName:"admonition"},"The key benefit of the articulated is its self-overlapping in acute angle, which is critical for brush strokes and digital painting.")))}d.isMDXComponent=!0}}]); \ No newline at end of file +"use strict";(self.webpackChunkbrush_stroke_tutorial=self.webpackChunkbrush_stroke_tutorial||[]).push([[147],{3905:(e,t,r)=>{r.d(t,{Zo:()=>c,kt:()=>h});var n=r(7294);function a(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function i(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function l(e){for(var t=1;t=0||(a[r]=e[r]);return a}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,r)&&(a[r]=e[r])}return a}var s=n.createContext({}),p=function(e){var t=n.useContext(s),r=t;return e&&(r="function"==typeof e?e(t):l(l({},t),e)),r},c=function(e){var t=p(e.components);return n.createElement(s.Provider,{value:t},e.children)},u="mdxType",d={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},m=n.forwardRef((function(e,t){var r=e.components,a=e.mdxType,i=e.originalType,s=e.parentName,c=o(e,["components","mdxType","originalType","parentName"]),u=p(r),m=a,h=u["".concat(s,".").concat(m)]||u[m]||d[m]||i;return r?n.createElement(h,l(l({ref:t},c),{},{components:r})):n.createElement(h,l({ref:t},c))}));function h(e,t){var r=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var i=r.length,l=new Array(i);l[0]=m;var o={};for(var s in t)hasOwnProperty.call(t,s)&&(o[s]=t[s]);o.originalType=e,o[u]="string"==typeof e?e:a,l[1]=o;for(var p=2;p{r.r(t),r.d(t,{assets:()=>s,contentTitle:()=>l,default:()=>d,frontMatter:()=>i,metadata:()=>o,toc:()=>p});var n=r(7462),a=(r(7294),r(3905));const i={sidebar_position:4,sidebar_label:"Tessellation",title:"Tessellation-based Rendering"},l=void 0,o={unversionedId:"Tessellation/Tessellation",id:"Tessellation/Tessellation",title:"Tessellation-based Rendering",description:"There were works trying to tessellate a stroke and render it with GPU.",source:"@site/docs/Tessellation/Tessellation.mdx",sourceDirName:"Tessellation",slug:"/Tessellation/",permalink:"/brush-rendering-tutorial/Tessellation/",draft:!1,editUrl:"https://github.com/ShenCiao/brush-rendering-tutorial/tree/main/docs/Tessellation/Tessellation.mdx",tags:[],version:"current",sidebarPosition:4,frontMatter:{sidebar_position:4,sidebar_label:"Tessellation",title:"Tessellation-based Rendering"},sidebar:"tutorialSidebar",previous:{title:"Vanilla",permalink:"/brush-rendering-tutorial/Basics/Vanilla/"},next:{title:"Appendix",permalink:"/brush-rendering-tutorial/category/appendix"}},s={},p=[],c={toc:p},u="wrapper";function d(e){let{components:t,...r}=e;return(0,a.kt)(u,(0,n.Z)({},c,r,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("p",null,"There were works trying to tessellate a stroke and render it with GPU.\nYou can find them in several papers and online articles."),(0,a.kt)("details",null,(0,a.kt)("summary",null,"Paper and article list"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("a",{parentName:"li",href:"https://dl.acm.org/doi/abs/10.1145/3386569.3392458"},"Polar Stroking: New Theory and Methods for Stroking Paths")," (Very Hard)"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("a",{parentName:"li",href:"https://mattdesl.svbtle.com/drawing-lines-is-hard"},"Drawing Lines is Hard")),(0,a.kt)("li",{parentName:"ul"},"Instanced Line Rendering: ",(0,a.kt)("a",{parentName:"li",href:"https://wwwtyro.net/2019/11/18/instanced-lines.html"},"part1")," | ",(0,a.kt)("a",{parentName:"li",href:"https://wwwtyro.net/2021/10/01/instanced-lines-part-2.html"},"part2")),(0,a.kt)("li",{parentName:"ul"},"... Tell me more tutorials in the ",(0,a.kt)("a",{parentName:"li",href:"https://github.com/ShenCiao/brush-rendering-tutorial/discussions/2"},"discussion")))),(0,a.kt)("p",null,"They may inspire some approaches to optimize the performance of the articulated algorithms in the future.\nTo learn about them, I would recommend starting with Rye Terrell's ",(0,a.kt)("a",{parentName:"p",href:"https://wwwtyro.net/2019/11/18/instanced-lines.html"},"Instanced Line Rendering"),",\nwhose methods are simple enough to learn."),(0,a.kt)("p",null,"As for this article, I will compare the tessellation-based algorithms with articulated algorithms in detail."),(0,a.kt)("h1",{id:"wip"},"WIP"),(0,a.kt)("admonition",{title:"spoiler",type:"note"},(0,a.kt)("p",{parentName:"admonition"},"The key benefit of the articulated is its self-overlapping in acute angle, which is critical for brush strokes and digital painting.")))}d.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/6ae0415c.aa8a3689.js b/assets/js/6ae0415c.07c5054e.js similarity index 72% rename from assets/js/6ae0415c.aa8a3689.js rename to assets/js/6ae0415c.07c5054e.js index 058c28f..229f3be 100644 --- a/assets/js/6ae0415c.aa8a3689.js +++ b/assets/js/6ae0415c.07c5054e.js @@ -1 +1 @@ -(self.webpackChunkbrush_stroke_tutorial=self.webpackChunkbrush_stroke_tutorial||[]).push([[613],{1410:(e,t,n)=>{const a=n(7694),o=n(3618),r={title:"Brush Rendering Tutorial",tagline:"Learn brush stroke rendering.",url:"https://shenciao.github.io",baseUrl:"/brush-rendering-tutorial/",organizationName:"ShenCiao",projectName:"brush-rendering-tutorial",onBrokenLinks:"throw",onBrokenMarkdownLinks:"warn",i18n:{defaultLocale:"en",locales:["en"]},presets:[["classic",{docs:{routeBasePath:"/",sidebarPath:6679,editUrl:"https://github.com/ShenCiao/brush-rendering-tutorial/tree/main"},blog:!1,theme:{customCss:2295}}]],themeConfig:{colorMode:{disableSwitch:!0},image:"img/vanilla-stroke.png",navbar:{title:"Brush Rendering Tutorial",logo:{alt:"logo",src:"img/vanilla-stroke.png"},items:[{type:"docSidebar",sidebarId:"tutorialSidebar",position:"right",label:"Tutorial"},{href:"https://github.com/ShenCiao/brush-stroke-tutorial",label:"GitHub",position:"right"}]},footer:{style:"light",copyright:`Copyright \xa9 ${(new Date).getFullYear()} Brush Rendering Tutorial, under CC BY-SA 4.0 License`},prism:{theme:a,darkTheme:o},docs:{sidebar:{hideable:!0}}},plugins:["raw-loaders"],trailingSlash:!0};e.exports=r},6679:e=>{e.exports={tutorialSidebar:[{type:"autogenerated",dirName:"."}]}},5632:(e,t,n)=>{"use strict";n.d(t,{ij:()=>v,en:()=>k,Sw:()=>T,rL:()=>L,PQ:()=>y});var a=n(7294),o=n(9477),r=n(5452),i=n(4866),l=n(5162),s=n(3764),c=n(5034),u=n(9279);const d="precision mediump float;\nprecision mediump int;\n\nuniform mat4 modelViewMatrix;\nuniform mat4 projectionMatrix;\n\nin vec2 position0;\nin float radius0;\nin float summedLength0;\nin vec2 position1;\nin float radius1;\nin float summedLength1;\n\nout vec2 p; // position of the current pixel\nflat out vec2 p0;\nflat out float r0;\nflat out float l0;\nflat out vec2 p1;\nflat out float r1;\nflat out float l1;\n\nvoid main()\t{\n r0 = radius0;\n r1 = radius1;\n p0 = position0;\n p1 = position1;\n l0 = summedLength0;\n l1 = summedLength1;\n\n vec2 tangent = normalize(position1 - position0);\n vec2 normal = vec2(-tangent.y, tangent.x);\n float cosTheta = (r0 - r1)/distance(p0, p1); // theta is the angle stroke tilt, there is a diagram in README to explain this.\n // the vertex1 with radius is fully inside the vertex0.\n if(abs(cosTheta) >= 1.0) return;\n\n // Each instance is a trapzoid, whose vertices' positions are determined here.\n // Use gl_VertexID {0, 1, 2, 3} to index and get the desired parameters.\n // Be careful with the backface culling! We are ignoring it here.\n vec2 offsetSign = vec2[](\n vec2(-1.0,-1.0),\n vec2(-1.0, 1.0),\n vec2( 1.0, 1.0),\n vec2( 1.0,-1.0)\n )[gl_VertexID];\n vec2 position = vec2[](position0, position0, position1, position1)[gl_VertexID];\n float radius = vec4(radius0, radius0, radius1, radius1)[gl_VertexID];\n\n float tanHalfTheta = sqrt((1.0+cosTheta) / (1.0-cosTheta));\n float cotHalfTheta = 1.0 / tanHalfTheta;\n float normalTanValue = vec4(tanHalfTheta, tanHalfTheta, cotHalfTheta, cotHalfTheta)[gl_VertexID];\n if(normalTanValue > 10.0 || normalTanValue < 0.1) return;\n\n vec2 trapzoidVertexPosition = position +\n offsetSign.x * radius * tangent +\n offsetSign.y * radius * normal * normalTanValue;\n p = trapzoidVertexPosition;\n\n gl_Position = projectionMatrix * modelViewMatrix * vec4(trapzoidVertexPosition, 0.0, 1.0);\n}\n",p="precision mediump float;\nprecision mediump int;\n\nin vec2 p;\nflat in vec2 p0;\nflat in float r0;\nflat in float l0;\nflat in vec2 p1;\nflat in float r1;\nflat in float l1;\n\n// Common\nuniform int type;\nconst int Vanilla = 0, Stamp = 1, Airbrush = 2;\nuniform vec4 color;\n// Stamp\nuniform mediump sampler2D footprint;\nuniform float stampIntervalRatio;\nuniform float noiseFactor;\nuniform float rotationFactor;\nfloat x2n(float x); // from distance to stamp index.\nfloat n2x(float n); // from stamp index to distance.\nmat2 rotate(float angle);\n// Airbrush\nuniform mediump sampler2D gradient;\nfloat sampleGraident(float distance){ return texture(gradient, vec2(distance, 0.0)).r; }\n\n// Noise helper functions from _The Book of Shader_.\nfloat random (in vec2 st);\nfloat noise (in vec2 st);\nfloat fbm (in vec2 st);\n\nout vec4 outColor;\n\nvoid main() {\n vec2 tangent = normalize(p1 - p0);\n vec2 normal = vec2(-tangent.y, tangent.x);\n\n // The local coordinate orgin at p0, x axis along the tangent direct.\n float len = distance(p1, p0);\n vec2 pLocal = vec2(dot(p-p0, tangent), dot(p-p0, normal));\n vec2 p0Local = vec2(0, 0);\n vec2 p1Local = vec2(len, 0);\n\n float cosTheta = (r0 - r1)/len;\n float d0 = distance(p, p0);\n float d0cos = pLocal.x / d0;\n float d1 = distance(p, p1);\n float d1cos = (pLocal.x - len) / d1;\n\n // Remove corners\n if(d0cos < cosTheta && d0 > r0) discard;\n if(d1cos > cosTheta && d1 > r1) discard;\n\n if(type == Vanilla){\n if(d0 < r0 && d1 < r1) discard;\n float A = (d0 < r0 || d1 < r1) ? 1.0 - sqrt(1.0 - color.a) : color.a;\n outColor = vec4(color.rgb, A);\n return;\n }\n\n if(type == Stamp){\n // The method here is not published yet, it should be explained in a 10min video.\n // The footprint is a disk instead of a square.\n // We set a quadratic polynomial to calculate the effect range, the range on polyline edge footprint can touch the current pixel.\n // Two roots of the quadratic polynomial are the effectRangeFront and effectRangeBack.\n // Formulas from SIGGRAPH 2022 Talk - A Fast & Robust Solution for Cubic & Higher-Order Polynomials\n float a, b, c, delta;\n a = 1.0 - pow(cosTheta, 2.0);\n b = 2.0 * (r0 * cosTheta - pLocal.x);\n c = pow(pLocal.x, 2.0) + pow(pLocal.y, 2.0) - pow(r0, 2.0);\n delta = pow(b, 2.0) - 4.0*a*c;\n if(delta <= 0.0) discard; // This should never happen.\n\n float tempMathBlock = b + sign(b) * sqrt(delta);\n float x1 = -2.0 * c / tempMathBlock;\n float x2 = -tempMathBlock / (2.0*a);\n float effectRangeFront = x1 <= x2 ? x1 : x2;\n float effectRangeBack = x1 > x2 ? x1 : x2;\n\n // We stamp on polyline every time the stamp index comes to an integer.\n float index0 = l0/stampIntervalRatio; // The stamp index of vertex0.\n float startIndex, endIndex;\n if (effectRangeFront <= 0.0){\n startIndex = ceil(index0);\n }\n else{\n startIndex = ceil(index0 + x2n(effectRangeFront));\n }\n float index1 = l1/stampIntervalRatio;\n float backIndex = x2n(effectRangeBack) + index0;\n endIndex = index1 < backIndex ? index1 : backIndex;\n if(startIndex > endIndex) discard;\n\n // The main loop to sample and blend color from the footprint.\n int MAX_i = 128; float currIndex = startIndex;\n float A = 0.0;\n for(int i = 0; i < MAX_i; i++){\n float currStampLocalX = n2x(currIndex - index0);\n // Apply roation and sample the footprint.\n vec2 pToCurrStamp = pLocal - vec2(currStampLocalX, 0.0);\n float currStampRadius = r0 - cosTheta * currStampLocalX;\n float angle = rotationFactor*radians(360.0*fract(sin(currIndex)*1.0));\n pToCurrStamp *= rotate(angle);\n vec2 textureCoordinate = (pToCurrStamp/currStampRadius + 1.0)/2.0;\n float opacity = texture(footprint, textureCoordinate).a;\n // Blend opacity.\n float opacityNoise = noiseFactor*fbm(textureCoordinate*50.0);\n opacity = clamp(opacity - opacityNoise, 0.0, 1.0) * color.a;\n A = A * (1.0-opacity) + opacity;\n\n currIndex += 1.0;\n if(currIndex > endIndex) break;\n }\n if(A < 1e-4) discard;\n outColor = vec4(color.rgb, A);\n return;\n }\n\n if(type == Airbrush){\n // The method here is not published yet. Shen is not fully satisfied with the current solution.\n float tanTheta = sqrt(1.0 - cosTheta*cosTheta)/cosTheta;\n float mid = pLocal.x - abs(pLocal.y)/tanTheta;\n float A = color.a;\n float transparency0 = d0 > r0 ? 1.0:sqrt(1.0 - A*sampleGraident(d0/r0));\n float transparency1 = d1 > r1 ? 1.0:sqrt(1.0 - A*sampleGraident(d1/r1));\n float transparency;\n\n // A bunch of math derived with the continuous form of airbrush here.\n if(mid <= 0.0){\n transparency = transparency0/transparency1;\n }\n if(mid > 0.0 && mid < len){\n float r = (mid * r1 + (len - mid) * r0)/len;\n float dr = distance(pLocal, vec2(mid, 0))/r;\n transparency = (1.0 - A*sampleGraident(dr))/transparency0/transparency1;\n }\n if(mid >= len){\n transparency = transparency1/transparency0;\n }\n\n outColor = vec4(color.rgb, 1.0 - transparency);\n }\n}\n\nfloat x2n(float x){\n float L = distance(p0, p1);\n if(r0 == r1) return x/(stampIntervalRatio*r0);\n else return -L / stampIntervalRatio / (r0 - r1) * log(1.0 - (1.0 - r1/r0)/L * x);\n}\n\nfloat n2x(float n){\n float L = distance(p0, p1);\n if(r0 == r1) return n * stampIntervalRatio * r0;\n else return L * (1.0-exp(-(r0-r1)*n*stampIntervalRatio/L)) / (1.0-r1/r0);\n}\n\n// Helper functions----------------------------------------------------------------------------------\nmat2 rotate(float angle){\n return mat2(cos(angle), -sin(angle), sin(angle), cos(angle));\n}\n\nfloat random (in vec2 st) {\n return fract(sin(dot(st.xy,\n vec2(12.9898,78.233)))*\n 43758.5453123);\n}\n\nfloat noise (in vec2 st) {\n vec2 i = floor(st);\n vec2 f = fract(st);\n\n // Four corners in 2D of a tile\n float a = random(i);\n float b = random(i + vec2(1.0, 0.0));\n float c = random(i + vec2(0.0, 1.0));\n float d = random(i + vec2(1.0, 1.0));\n\n vec2 u = f * f * (3.0 - 2.0 * f);\n\n return mix(a, b, u.x) +\n (c - a)* u.y * (1.0 - u.x) +\n (d - b) * u.x * u.y;\n}\n\n#define OCTAVES 6\nfloat fbm (in vec2 st) {\n // Initial values\n float value = 0.0;\n float amplitude = .5;\n float frequency = 0.;\n //\n // Loop of octaves\n for (int i = 0; i < OCTAVES; i++) {\n value += amplitude * noise(st);\n st *= 2.;\n amplitude *= .5;\n }\n return value;\n}\n";var m=n(1410),f=n.n(m),h=n(412);let g=function(e){return e[e.Vanilla=0]="Vanilla",e[e.Stamp=1]="Stamp",e[e.Airbrush=2]="Airbrush",e}({});function v(e){let{uniforms:t=null,showEditor:n=null}=e;const m=(0,a.useRef)(),f=(0,a.useRef)(),h=(0,a.useRef)();function v(e,t,n){const a=[...t],r=[...t.slice(2)],i=[...n],l=[...n.slice(1)],s=[];let c=0;for(let p=0;p{const e=(1+Math.sqrt(5))/2,n=m.current.clientWidth,a=n*(.5/e),i=4*e,l=i*(.5/e),s=new o.iKG(i/-2,i/2,l/2,l/-2,-1e3,1e3);s.position.z=5;const c=new o.CP7({antialias:!0,alpha:!0,premultipliedAlpha:!1,powerPreference:"high-performance"});function x(){const t=m.current.clientWidth,n=.5*t/e;c.setSize(t,n)}c.setClearColor(new o.Ilk(1,1,1),0),c.setSize(n,a),window.addEventListener("resize",x),m.current.appendChild(c.domElement);const b=new o.xsS,y=new r.o(s,c.domElement);y.enableRotate=!1,y.enableDamping=!1,y.screenSpacePanning=!0,y.addEventListener("change",(()=>{c.render(b,s)})),f.current=()=>c.render(b,s),window.addEventListener("TextureLoaded",f.current);const w=new o.u9r;w.setIndex([0,1,2,2,3,0]);const k=new Function(u.Z),[T,L]=k();v(w,T,L);const A={type:{value:g.Vanilla},color:{value:[0,0,0,1]},footprint:{value:new o.xEZ},stampIntervalRatio:{value:1},noiseFactor:{value:0},rotationFactor:{value:0},gradient:{value:new o.IEO}},S=new o.FIo({uniforms:t||A,vertexShader:d,fragmentShader:p,side:o.ehD,transparent:!0,glslVersion:o.LSk});return h.current=new o.SPe(w,S,T.length-1),h.current.frustumCulled=!1,b.add(h.current),f.current(),()=>{c.dispose(),window.removeEventListener("resize",x),window.removeEventListener("TextureLoaded",f.current)}}),[]);const b=(0,a.useCallback)(((e,t)=>{let n=[],a=[];try{const t=new Function(e);[n,a]=t()}catch(r){return void console.log(r.toString())}function o(e){if(Array.isArray(e)){for(let t=0;t{x(e,"")}})),T&&a.createElement(l.Z,{value:"fragment.glsl"},a.createElement(c.r,{height:y,defaultValue:p,onChange:e=>{x("",e)}})))),a.createElement("div",{ref:m,style:{width:"100%"},onMouseDown:e=>e.preventDefault()}))}let x=new o.xEZ;h.Z.canUseDOM&&(x=(new o.dpR).load(`/${f().projectName}/img/stamp2.png`,(e=>{window.dispatchEvent(new CustomEvent("TextureLoaded"))}),void 0,void 0));let b=new o.xEZ;h.Z.canUseDOM&&(b=(new o.dpR).load(`/${f().projectName}/img/dot.png`,(e=>{window.dispatchEvent(new CustomEvent("TextureLoaded"))}),void 0,void 0));const y={type:{value:g.Stamp},color:{value:[0,0,0,1]},footprint:{value:x},stampIntervalRatio:{value:.4},noiseFactor:{value:1.2},rotationFactor:{value:.75}},w=((e,t)=>{let n=new o.AXT(new o.FM8(0,1),e,t,new o.FM8(1,0));const a=256,r=new Uint8Array(1024),i=n.getPoints(512);for(let o=0;o=n.x&&e<=a.x){let t=(n.y*(a.x-e)+a.y*(e-n.x))/(a.x-n.x);r[4*o]=Math.floor(255*t)}}}const l=new o.IEO(r,a,1);return l.needsUpdate=!0,l})(new o.FM8(.33,1),new o.FM8(.66,0)),k={type:{value:g.Airbrush},color:{value:[0,0,0,1]},gradient:{value:w}},T={type:{value:g.Stamp},color:{value:[0,0,0,.5]},footprint:{value:b},stampIntervalRatio:{value:2},noiseFactor:{value:0},rotationFactor:{value:0}},L={type:{value:g.Stamp},color:{value:[0,0,0,.5]},footprint:{value:b},stampIntervalRatio:{value:1},noiseFactor:{value:0},rotationFactor:{value:0}}},5034:(e,t,n)=>{"use strict";n.d(t,{r:()=>s});var a=n(7462),o=n(7294);const r={comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"[",close:"]"},{open:"{",close:"}"},{open:"(",close:")"},{open:"'",close:"'",notIn:["string","comment"]},{open:'"',close:'"',notIn:["string"]}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}]},i={tokenPostfix:".glsl",defaultToken:"invalid",keywords:["const","uniform","break","continue","do","for","while","if","else","switch","case","in","out","inout","true","false","invariant","discard","return","sampler2D","samplerCube","sampler3D","struct","radians","degrees","sin","cos","tan","asin","acos","atan","pow","sinh","cosh","tanh","asinh","acosh","atanh","exp","log","exp2","log2","sqrt","inversesqrt","abs","sign","floor","ceil","round","roundEven","trunc","fract","mod","modf","min","max","clamp","mix","step","smoothstep","length","distance","dot","cross ","determinant","inverse","normalize","faceforward","reflect","refract","matrixCompMult","outerProduct","transpose","lessThan ","lessThanEqual","greaterThan","greaterThanEqual","equal","notEqual","any","all","not","packUnorm2x16","unpackUnorm2x16","packSnorm2x16","unpackSnorm2x16","packHalf2x16","unpackHalf2x16","dFdx","dFdy","fwidth","textureSize","texture","textureProj","textureLod","textureGrad","texelFetch","texelFetchOffset","textureProjLod","textureLodOffset","textureGradOffset","textureProjLodOffset","textureProjGrad","intBitsToFloat","uintBitsToFloat","floatBitsToInt","floatBitsToUint","isnan","isinf","vec2","vec3","vec4","ivec2","ivec3","ivec4","uvec2","uvec3","uvec4","bvec2","bvec3","bvec4","mat2","mat3","mat2x2","mat2x3","mat2x4","mat3x2","mat3x3","mat3x4","mat4x2","mat4x3","mat4x4","mat4","float","int","uint","void","bool"],operators:["=",">","<","!","~","?",":","==","<=",">=","!=","&&","||","++","--","+","-","*","/","&","|","^","%","<<",">>",">>>","+=","-=","*=","/=","&=","|=","^=","%=","<<=",">>=",">>>="],symbols:/[=>{n.languages.register({id:"glsl"}),n.languages.setMonarchTokensProvider("glsl",i),n.languages.setLanguageConfiguration("glsl",r),"function"==typeof e.onMount&&e.onMount(t,n)}}))}},9279:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});const a="// Generate sinewave geometry \nconst maxRadius = 1/3;\nconst segmentCount = 32;\n\nconst position = [];\nconst radius = [];\n\nconst gr = (1 + Math.sqrt(5)) / 2; // golden ratio\nconst pi = Math.PI;\n\nfor(let i = 0; i <= segmentCount; ++i){\n let a = i / segmentCount\n let x = -pi + (2 * pi * a);\n let y = Math.sin(x) / gr;\n let r = Math.cos(x / 2.0) * maxRadius;\n\n position.push(x, y);\n radius.push(r);\n}\n\nreturn [position, radius];\n"},2919:(e,t,n)=>{"use strict";n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>l,default:()=>m,frontMatter:()=>i,metadata:()=>s,toc:()=>u});var a=n(7462),o=(n(7294),n(3905)),r=n(5632);const i={title:"Table of Contents",hide_title:!0,sidebar_position:1,slug:"/"},l=void 0,s={unversionedId:"toc",id:"toc",title:"Table of Contents",description:"Vanilla",source:"@site/docs/toc.mdx",sourceDirName:".",slug:"/",permalink:"/brush-rendering-tutorial/",draft:!1,editUrl:"https://github.com/ShenCiao/brush-rendering-tutorial/tree/main/docs/toc.mdx",tags:[],version:"current",sidebarPosition:1,frontMatter:{title:"Table of Contents",hide_title:!0,sidebar_position:1,slug:"/"},sidebar:"tutorialSidebar",next:{title:"Introduction",permalink:"/brush-rendering-tutorial/Introduction/"}},c={},u=[{value:"Table of Contents",id:"table-of-contents",level:2},{value:"Future Contents",id:"future-contents",level:2},{value:"Airbrush",id:"airbrush",level:3},{value:"Stamp density and "ratio-distance"",id:"stamp-density-and-ratio-distance",level:3},{value:"3D stroke",id:"3d-stroke",level:3}],d={toc:u},p="wrapper";function m(e){let{components:t,...n}=e;return(0,o.kt)(p,(0,a.Z)({},d,n,{components:t,mdxType:"MDXLayout"}),(0,o.kt)("div",{className:"row row--no-gutters margin-left--xs"},(0,o.kt)("div",{className:"col col--6"},(0,o.kt)(r.ij,{mdxType:"ArticulatedLine2D"}),(0,o.kt)("center",null,(0,o.kt)("em",null," Vanilla "))),(0,o.kt)("div",{className:"col col--6"},(0,o.kt)(r.ij,{uniforms:r.PQ,mdxType:"ArticulatedLine2D"}),(0,o.kt)("center",null,(0,o.kt)("em",null," Pencil ")))),(0,o.kt)("br",null),(0,o.kt)("p",null,"This tutorial series will teach you how to render brush strokes with the modern GPU graphics pipeline."),(0,o.kt)("p",null,"If you like this series, please star the ",(0,o.kt)("a",{parentName:"p",href:"https://github.com/ShenCiao/brush-rendering-tutorial"},"code repository")," instead of bookmark this website since the domain might be changed."),(0,o.kt)("h2",{id:"table-of-contents"},"Table of Contents"),(0,o.kt)("ul",null,(0,o.kt)("li",{parentName:"ul"},(0,o.kt)("a",{parentName:"li",href:"./introduction"},"Introduction")),(0,o.kt)("li",{parentName:"ul"},"Basics",(0,o.kt)("ul",{parentName:"li"},(0,o.kt)("li",{parentName:"ul"},(0,o.kt)("a",{parentName:"li",href:"./Basics/Vanilla"},"Vanilla")),(0,o.kt)("li",{parentName:"ul"},"Vanilla with variable radius"),(0,o.kt)("li",{parentName:"ul"},"Stamp"),(0,o.kt)("li",{parentName:"ul"},"Stamp with variable radius 1"),(0,o.kt)("li",{parentName:"ul"},"Stamp with variable radius 2"))),(0,o.kt)("li",{parentName:"ul"},"An interleave")),(0,o.kt)("h2",{id:"future-contents"},"Future Contents"),(0,o.kt)("h3",{id:"airbrush"},"Airbrush"),(0,o.kt)(r.ij,{uniforms:r.en,mdxType:"ArticulatedLine2D"}),(0,o.kt)("p",null,'Airbrush is a special type of stamp brush.\nHere I\'m demonstrating a "continuous airbrush", which is mathematically continuous and needs a little bit of calculus to develop.\nYou will learn how to generalize a stamp brush into a continuous form.'),(0,o.kt)("h3",{id:"stamp-density-and-ratio-distance"},'Stamp density and "ratio-distance"'),(0,o.kt)("div",{className:"row row--no-gutters margin-left--xs"},(0,o.kt)("div",{className:"col col--6"},(0,o.kt)(r.ij,{uniforms:r.Sw,mdxType:"ArticulatedLine2D"}),(0,o.kt)("center",null,(0,o.kt)("em",null," Adjacent Dots "))),(0,o.kt)("div",{className:"col col--6"},(0,o.kt)(r.ij,{uniforms:r.rL,mdxType:"ArticulatedLine2D"}),(0,o.kt)("center",null,(0,o.kt)("em",null," Adjacent with one dot interleaved ")))),(0,o.kt)("br",null),(0,o.kt)("p",null,"You can see dots are adjacent to each other instead of equidistantly distributed.\nThe pattern is achieved by setting the intervals between dots proportional to their radii.\nYou will learn how to freely control stamp density along a stamp\nstroke. Very important for a serious project."),(0,o.kt)("h3",{id:"3d-stroke"},"3D stroke"),(0,o.kt)("p",null,"Learn how to extend the algorithms to 3D space."),(0,o.kt)("p",null,"I'm integrating it into the Blender Grease Pencil:"),(0,o.kt)("iframe",{width:"100%",height:"500",src:"https://www.youtube.com/embed/Q7_3IhgHOZM?start=30",title:"Blender Grease Pencil Stamp Brush Demo",allow:"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",allowFullScreen:!0}))}m.isMDXComponent=!0},3618:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>a});const a={plain:{color:"#F8F8F2",backgroundColor:"#282A36"},styles:[{types:["prolog","constant","builtin"],style:{color:"rgb(189, 147, 249)"}},{types:["inserted","function"],style:{color:"rgb(80, 250, 123)"}},{types:["deleted"],style:{color:"rgb(255, 85, 85)"}},{types:["changed"],style:{color:"rgb(255, 184, 108)"}},{types:["punctuation","symbol"],style:{color:"rgb(248, 248, 242)"}},{types:["string","char","tag","selector"],style:{color:"rgb(255, 121, 198)"}},{types:["keyword","variable"],style:{color:"rgb(189, 147, 249)",fontStyle:"italic"}},{types:["comment"],style:{color:"rgb(98, 114, 164)"}},{types:["attr-name"],style:{color:"rgb(241, 250, 140)"}}]}},7694:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>a});const a={plain:{color:"#393A34",backgroundColor:"#f6f8fa"},styles:[{types:["comment","prolog","doctype","cdata"],style:{color:"#999988",fontStyle:"italic"}},{types:["namespace"],style:{opacity:.7}},{types:["string","attr-value"],style:{color:"#e3116c"}},{types:["punctuation","operator"],style:{color:"#393A34"}},{types:["entity","url","symbol","number","boolean","variable","constant","property","regex","inserted"],style:{color:"#36acaa"}},{types:["atrule","keyword","attr-name","selector"],style:{color:"#00a4db"}},{types:["function","deleted","tag"],style:{color:"#d73a49"}},{types:["function-variable"],style:{color:"#6f42c1"}},{types:["tag","selector","keyword"],style:{color:"#00009f"}}]}}}]); \ No newline at end of file +(self.webpackChunkbrush_stroke_tutorial=self.webpackChunkbrush_stroke_tutorial||[]).push([[613],{1410:(e,t,n)=>{const a=n(7694),o=n(3618),r={title:"Brush Rendering Tutorial",tagline:"Learn brush stroke rendering.",url:"https://shenciao.github.io",baseUrl:"/brush-rendering-tutorial/",organizationName:"ShenCiao",projectName:"brush-rendering-tutorial",onBrokenLinks:"throw",onBrokenMarkdownLinks:"warn",i18n:{defaultLocale:"en",locales:["en"]},presets:[["classic",{docs:{routeBasePath:"/",sidebarPath:6679,editUrl:"https://github.com/ShenCiao/brush-rendering-tutorial/tree/main"},blog:!1,theme:{customCss:2295}}]],themeConfig:{colorMode:{disableSwitch:!0},image:"img/vanilla-stroke.png",navbar:{title:"Brush Rendering Tutorial",logo:{alt:"logo",src:"img/vanilla-stroke.png"},items:[{type:"docSidebar",sidebarId:"tutorialSidebar",position:"right",label:"Tutorial"},{href:"https://github.com/ShenCiao/brush-stroke-tutorial",label:"GitHub",position:"right"}]},footer:{style:"light",copyright:`Copyright \xa9 ${(new Date).getFullYear()} Brush Rendering Tutorial, under CC BY-SA 4.0 License`},prism:{theme:a,darkTheme:o},docs:{sidebar:{hideable:!0}}},plugins:["raw-loaders"],trailingSlash:!0};e.exports=r},6679:e=>{e.exports={tutorialSidebar:[{type:"autogenerated",dirName:"."}]}},5632:(e,t,n)=>{"use strict";n.d(t,{ij:()=>v,en:()=>k,Sw:()=>T,rL:()=>L,PQ:()=>y});var a=n(7294),o=n(9477),r=n(5452),i=n(4866),l=n(5162),s=n(3764),c=n(5034),u=n(9279);const d="precision mediump float;\nprecision mediump int;\n\nuniform mat4 modelViewMatrix;\nuniform mat4 projectionMatrix;\n\nin vec2 position0;\nin float radius0;\nin float summedLength0;\nin vec2 position1;\nin float radius1;\nin float summedLength1;\n\nout vec2 p; // position of the current pixel\nflat out vec2 p0;\nflat out float r0;\nflat out float l0;\nflat out vec2 p1;\nflat out float r1;\nflat out float l1;\n\nvoid main()\t{\n r0 = radius0;\n r1 = radius1;\n p0 = position0;\n p1 = position1;\n l0 = summedLength0;\n l1 = summedLength1;\n\n vec2 tangent = normalize(position1 - position0);\n vec2 normal = vec2(-tangent.y, tangent.x);\n float cosTheta = (r0 - r1)/distance(p0, p1); // theta is the angle stroke tilt, there is a diagram in README to explain this.\n // the vertex1 with radius is fully inside the vertex0.\n if(abs(cosTheta) >= 1.0) return;\n\n // Each instance is a trapzoid, whose vertices' positions are determined here.\n // Use gl_VertexID {0, 1, 2, 3} to index and get the desired parameters.\n // Be careful with the backface culling! We are ignoring it here.\n vec2 offsetSign = vec2[](\n vec2(-1.0,-1.0),\n vec2(-1.0, 1.0),\n vec2( 1.0, 1.0),\n vec2( 1.0,-1.0)\n )[gl_VertexID];\n vec2 position = vec2[](position0, position0, position1, position1)[gl_VertexID];\n float radius = vec4(radius0, radius0, radius1, radius1)[gl_VertexID];\n\n float tanHalfTheta = sqrt((1.0+cosTheta) / (1.0-cosTheta));\n float cotHalfTheta = 1.0 / tanHalfTheta;\n float normalTanValue = vec4(tanHalfTheta, tanHalfTheta, cotHalfTheta, cotHalfTheta)[gl_VertexID];\n if(normalTanValue > 10.0 || normalTanValue < 0.1) return;\n\n vec2 trapzoidVertexPosition = position +\n offsetSign.x * radius * tangent +\n offsetSign.y * radius * normal * normalTanValue;\n p = trapzoidVertexPosition;\n\n gl_Position = projectionMatrix * modelViewMatrix * vec4(trapzoidVertexPosition, 0.0, 1.0);\n}\n",p="precision mediump float;\nprecision mediump int;\n\nin vec2 p;\nflat in vec2 p0;\nflat in float r0;\nflat in float l0;\nflat in vec2 p1;\nflat in float r1;\nflat in float l1;\n\n// Common\nuniform int type;\nconst int Vanilla = 0, Stamp = 1, Airbrush = 2;\nuniform vec4 color;\n// Stamp\nuniform mediump sampler2D footprint;\nuniform float stampIntervalRatio;\nuniform float noiseFactor;\nuniform float rotationFactor;\nfloat x2n(float x); // from distance to stamp index.\nfloat n2x(float n); // from stamp index to distance.\nmat2 rotate(float angle);\n// Airbrush\nuniform mediump sampler2D gradient;\nfloat sampleGraident(float distance){ return texture(gradient, vec2(distance, 0.0)).r; }\n\n// Noise helper functions from _The Book of Shader_.\nfloat random (in vec2 st);\nfloat noise (in vec2 st);\nfloat fbm (in vec2 st);\n\nout vec4 outColor;\n\nvoid main() {\n vec2 tangent = normalize(p1 - p0);\n vec2 normal = vec2(-tangent.y, tangent.x);\n\n // The local coordinate orgin at p0, x axis along the tangent direct.\n float len = distance(p1, p0);\n vec2 pLocal = vec2(dot(p-p0, tangent), dot(p-p0, normal));\n vec2 p0Local = vec2(0, 0);\n vec2 p1Local = vec2(len, 0);\n\n float cosTheta = (r0 - r1)/len;\n float d0 = distance(p, p0);\n float d0cos = pLocal.x / d0;\n float d1 = distance(p, p1);\n float d1cos = (pLocal.x - len) / d1;\n\n // Remove corners\n if(d0cos < cosTheta && d0 > r0) discard;\n if(d1cos > cosTheta && d1 > r1) discard;\n\n if(type == Vanilla){\n if(d0 < r0 && d1 < r1) discard;\n float A = (d0 < r0 || d1 < r1) ? 1.0 - sqrt(1.0 - color.a) : color.a;\n outColor = vec4(color.rgb, A);\n return;\n }\n\n if(type == Stamp){\n // The method here is not published yet, it should be explained in a 10min video.\n // The footprint is a disk instead of a square.\n // We set a quadratic polynomial to calculate the effect range, the range on polyline edge footprint can touch the current pixel.\n // Two roots of the quadratic polynomial are the effectRangeFront and effectRangeBack.\n // Formulas from SIGGRAPH 2022 Talk - A Fast & Robust Solution for Cubic & Higher-Order Polynomials\n float a, b, c, delta;\n a = 1.0 - pow(cosTheta, 2.0);\n b = 2.0 * (r0 * cosTheta - pLocal.x);\n c = pow(pLocal.x, 2.0) + pow(pLocal.y, 2.0) - pow(r0, 2.0);\n delta = pow(b, 2.0) - 4.0*a*c;\n if(delta <= 0.0) discard; // This should never happen.\n\n float tempMathBlock = b + sign(b) * sqrt(delta);\n float x1 = -2.0 * c / tempMathBlock;\n float x2 = -tempMathBlock / (2.0*a);\n float effectRangeFront = x1 <= x2 ? x1 : x2;\n float effectRangeBack = x1 > x2 ? x1 : x2;\n\n // We stamp on polyline every time the stamp index comes to an integer.\n float index0 = l0/stampIntervalRatio; // The stamp index of vertex0.\n float startIndex, endIndex;\n if (effectRangeFront <= 0.0){\n startIndex = ceil(index0);\n }\n else{\n startIndex = ceil(index0 + x2n(effectRangeFront));\n }\n float index1 = l1/stampIntervalRatio;\n float backIndex = x2n(effectRangeBack) + index0;\n endIndex = index1 < backIndex ? index1 : backIndex;\n if(startIndex > endIndex) discard;\n\n // The main loop to sample and blend color from the footprint.\n int MAX_i = 128; float currIndex = startIndex;\n float A = 0.0;\n for(int i = 0; i < MAX_i; i++){\n float currStampLocalX = n2x(currIndex - index0);\n // Apply roation and sample the footprint.\n vec2 pToCurrStamp = pLocal - vec2(currStampLocalX, 0.0);\n float currStampRadius = r0 - cosTheta * currStampLocalX;\n float angle = rotationFactor*radians(360.0*fract(sin(currIndex)*1.0));\n pToCurrStamp *= rotate(angle);\n vec2 textureCoordinate = (pToCurrStamp/currStampRadius + 1.0)/2.0;\n float opacity = texture(footprint, textureCoordinate).a;\n // Blend opacity.\n float opacityNoise = noiseFactor*fbm(textureCoordinate*50.0);\n opacity = clamp(opacity - opacityNoise, 0.0, 1.0) * color.a;\n A = A * (1.0-opacity) + opacity;\n\n currIndex += 1.0;\n if(currIndex > endIndex) break;\n }\n if(A < 1e-4) discard;\n outColor = vec4(color.rgb, A);\n return;\n }\n\n if(type == Airbrush){\n // The method here is not published yet. Shen is not fully satisfied with the current solution.\n float tanTheta = sqrt(1.0 - cosTheta*cosTheta)/cosTheta;\n float mid = pLocal.x - abs(pLocal.y)/tanTheta;\n float A = color.a;\n float transparency0 = d0 > r0 ? 1.0:sqrt(1.0 - A*sampleGraident(d0/r0));\n float transparency1 = d1 > r1 ? 1.0:sqrt(1.0 - A*sampleGraident(d1/r1));\n float transparency;\n\n // A bunch of math derived with the continuous form of airbrush here.\n if(mid <= 0.0){\n transparency = transparency0/transparency1;\n }\n if(mid > 0.0 && mid < len){\n float r = (mid * r1 + (len - mid) * r0)/len;\n float dr = distance(pLocal, vec2(mid, 0))/r;\n transparency = (1.0 - A*sampleGraident(dr))/transparency0/transparency1;\n }\n if(mid >= len){\n transparency = transparency1/transparency0;\n }\n\n outColor = vec4(color.rgb, 1.0 - transparency);\n }\n}\n\nfloat x2n(float x){\n float L = distance(p0, p1);\n if(r0 == r1) return x/(stampIntervalRatio*r0);\n else return -L / stampIntervalRatio / (r0 - r1) * log(1.0 - (1.0 - r1/r0)/L * x);\n}\n\nfloat n2x(float n){\n float L = distance(p0, p1);\n if(r0 == r1) return n * stampIntervalRatio * r0;\n else return L * (1.0-exp(-(r0-r1)*n*stampIntervalRatio/L)) / (1.0-r1/r0);\n}\n\n// Helper functions----------------------------------------------------------------------------------\nmat2 rotate(float angle){\n return mat2(cos(angle), -sin(angle), sin(angle), cos(angle));\n}\n\nfloat random (in vec2 st) {\n return fract(sin(dot(st.xy,\n vec2(12.9898,78.233)))*\n 43758.5453123);\n}\n\nfloat noise (in vec2 st) {\n vec2 i = floor(st);\n vec2 f = fract(st);\n\n // Four corners in 2D of a tile\n float a = random(i);\n float b = random(i + vec2(1.0, 0.0));\n float c = random(i + vec2(0.0, 1.0));\n float d = random(i + vec2(1.0, 1.0));\n\n vec2 u = f * f * (3.0 - 2.0 * f);\n\n return mix(a, b, u.x) +\n (c - a)* u.y * (1.0 - u.x) +\n (d - b) * u.x * u.y;\n}\n\n#define OCTAVES 6\nfloat fbm (in vec2 st) {\n // Initial values\n float value = 0.0;\n float amplitude = .5;\n float frequency = 0.;\n //\n // Loop of octaves\n for (int i = 0; i < OCTAVES; i++) {\n value += amplitude * noise(st);\n st *= 2.;\n amplitude *= .5;\n }\n return value;\n}\n";var f=n(1410),m=n.n(f),h=n(412);let g=function(e){return e[e.Vanilla=0]="Vanilla",e[e.Stamp=1]="Stamp",e[e.Airbrush=2]="Airbrush",e}({});function v(e){let{uniforms:t=null,showEditor:n=null}=e;const f=(0,a.useRef)(),m=(0,a.useRef)(),h=(0,a.useRef)();function v(e,t,n){const a=[...t],r=[...t.slice(2)],i=[...n],l=[...n.slice(1)],s=[];let c=0;for(let p=0;p{const e=(1+Math.sqrt(5))/2,n=f.current.clientWidth,a=n*(.5/e),i=4*e,l=i*(.5/e),s=new o.iKG(i/-2,i/2,l/2,l/-2,-1e3,1e3);s.position.z=5;const c=new o.CP7({antialias:!0,alpha:!0,premultipliedAlpha:!1,powerPreference:"high-performance"});function x(){const t=f.current.clientWidth,n=.5*t/e;c.setSize(t,n)}c.setClearColor(new o.Ilk(1,1,1),0),c.setSize(n,a),window.addEventListener("resize",x),f.current.appendChild(c.domElement);const b=new o.xsS,y=new r.o(s,c.domElement);y.enableRotate=!1,y.enableDamping=!1,y.screenSpacePanning=!0,y.addEventListener("change",(()=>{c.render(b,s)})),m.current=()=>c.render(b,s),window.addEventListener("TextureLoaded",m.current);const w=new o.u9r;w.setIndex([0,1,2,2,3,0]);const k=new Function(u.Z),[T,L]=k();v(w,T,L);const A={type:{value:g.Vanilla},color:{value:[0,0,0,1]},footprint:{value:new o.xEZ},stampIntervalRatio:{value:1},noiseFactor:{value:0},rotationFactor:{value:0},gradient:{value:new o.IEO}},S=new o.FIo({uniforms:t||A,vertexShader:d,fragmentShader:p,side:o.ehD,transparent:!0,glslVersion:o.LSk});return h.current=new o.SPe(w,S,T.length-1),h.current.frustumCulled=!1,b.add(h.current),m.current(),()=>{c.dispose(),window.removeEventListener("resize",x),window.removeEventListener("TextureLoaded",m.current)}}),[]);const b=(0,a.useCallback)(((e,t)=>{let n=[],a=[];try{const t=new Function(e);[n,a]=t()}catch(r){return void console.log(r.toString())}function o(e){if(Array.isArray(e)){for(let t=0;t{x(e,"")}})),T&&a.createElement(l.Z,{value:"fragment.glsl"},a.createElement(c.r,{height:y,defaultValue:p,onChange:e=>{x("",e)}})))),a.createElement("div",{ref:f,style:{width:"100%"},onMouseDown:e=>e.preventDefault()}))}let x=new o.xEZ;h.Z.canUseDOM&&(x=(new o.dpR).load(`/${m().projectName}/img/stamp2.png`,(e=>{window.dispatchEvent(new CustomEvent("TextureLoaded"))}),void 0,void 0));let b=new o.xEZ;h.Z.canUseDOM&&(b=(new o.dpR).load(`/${m().projectName}/img/dot.png`,(e=>{window.dispatchEvent(new CustomEvent("TextureLoaded"))}),void 0,void 0));const y={type:{value:g.Stamp},color:{value:[0,0,0,1]},footprint:{value:x},stampIntervalRatio:{value:.4},noiseFactor:{value:1.2},rotationFactor:{value:.75}},w=((e,t)=>{let n=new o.AXT(new o.FM8(0,1),e,t,new o.FM8(1,0));const a=256,r=new Uint8Array(1024),i=n.getPoints(512);for(let o=0;o=n.x&&e<=a.x){let t=(n.y*(a.x-e)+a.y*(e-n.x))/(a.x-n.x);r[4*o]=Math.floor(255*t)}}}const l=new o.IEO(r,a,1);return l.needsUpdate=!0,l})(new o.FM8(.33,1),new o.FM8(.66,0)),k={type:{value:g.Airbrush},color:{value:[0,0,0,1]},gradient:{value:w}},T={type:{value:g.Stamp},color:{value:[0,0,0,.5]},footprint:{value:b},stampIntervalRatio:{value:2},noiseFactor:{value:0},rotationFactor:{value:0}},L={type:{value:g.Stamp},color:{value:[0,0,0,.5]},footprint:{value:b},stampIntervalRatio:{value:1},noiseFactor:{value:0},rotationFactor:{value:0}}},5034:(e,t,n)=>{"use strict";n.d(t,{r:()=>s});var a=n(7462),o=n(7294);const r={comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"[",close:"]"},{open:"{",close:"}"},{open:"(",close:")"},{open:"'",close:"'",notIn:["string","comment"]},{open:'"',close:'"',notIn:["string"]}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}]},i={tokenPostfix:".glsl",defaultToken:"invalid",keywords:["const","uniform","break","continue","do","for","while","if","else","switch","case","in","out","inout","true","false","invariant","discard","return","sampler2D","samplerCube","sampler3D","struct","radians","degrees","sin","cos","tan","asin","acos","atan","pow","sinh","cosh","tanh","asinh","acosh","atanh","exp","log","exp2","log2","sqrt","inversesqrt","abs","sign","floor","ceil","round","roundEven","trunc","fract","mod","modf","min","max","clamp","mix","step","smoothstep","length","distance","dot","cross ","determinant","inverse","normalize","faceforward","reflect","refract","matrixCompMult","outerProduct","transpose","lessThan ","lessThanEqual","greaterThan","greaterThanEqual","equal","notEqual","any","all","not","packUnorm2x16","unpackUnorm2x16","packSnorm2x16","unpackSnorm2x16","packHalf2x16","unpackHalf2x16","dFdx","dFdy","fwidth","textureSize","texture","textureProj","textureLod","textureGrad","texelFetch","texelFetchOffset","textureProjLod","textureLodOffset","textureGradOffset","textureProjLodOffset","textureProjGrad","intBitsToFloat","uintBitsToFloat","floatBitsToInt","floatBitsToUint","isnan","isinf","vec2","vec3","vec4","ivec2","ivec3","ivec4","uvec2","uvec3","uvec4","bvec2","bvec3","bvec4","mat2","mat3","mat2x2","mat2x3","mat2x4","mat3x2","mat3x3","mat3x4","mat4x2","mat4x3","mat4x4","mat4","float","int","uint","void","bool"],operators:["=",">","<","!","~","?",":","==","<=",">=","!=","&&","||","++","--","+","-","*","/","&","|","^","%","<<",">>",">>>","+=","-=","*=","/=","&=","|=","^=","%=","<<=",">>=",">>>="],symbols:/[=>{n.languages.register({id:"glsl"}),n.languages.setMonarchTokensProvider("glsl",i),n.languages.setLanguageConfiguration("glsl",r),"function"==typeof e.onMount&&e.onMount(t,n)}}))}},9279:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});const a="// Generate sinewave geometry \nconst maxRadius = 1/3;\nconst segmentCount = 32;\n\nconst position = [];\nconst radius = [];\n\nconst gr = (1 + Math.sqrt(5)) / 2; // golden ratio\nconst pi = Math.PI;\n\nfor(let i = 0; i <= segmentCount; ++i){\n let a = i / segmentCount\n let x = -pi + (2 * pi * a);\n let y = Math.sin(x) / gr;\n let r = Math.cos(x / 2.0) * maxRadius;\n\n position.push(x, y);\n radius.push(r);\n}\n\nreturn [position, radius];\n"},2919:(e,t,n)=>{"use strict";n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>l,default:()=>f,frontMatter:()=>i,metadata:()=>s,toc:()=>u});var a=n(7462),o=(n(7294),n(3905)),r=n(5632);const i={title:"Table of Contents",hide_title:!0,sidebar_position:1,slug:"/"},l=void 0,s={unversionedId:"toc",id:"toc",title:"Table of Contents",description:"This tutorial series will teach you how to render brush strokes with the modern GPU graphics pipeline.",source:"@site/docs/toc.mdx",sourceDirName:".",slug:"/",permalink:"/brush-rendering-tutorial/",draft:!1,editUrl:"https://github.com/ShenCiao/brush-rendering-tutorial/tree/main/docs/toc.mdx",tags:[],version:"current",sidebarPosition:1,frontMatter:{title:"Table of Contents",hide_title:!0,sidebar_position:1,slug:"/"},sidebar:"tutorialSidebar",next:{title:"Introduction",permalink:"/brush-rendering-tutorial/Introduction/"}},c={},u=[{value:"Table of Contents",id:"table-of-contents",level:2},{value:"Future Contents",id:"future-contents",level:2},{value:"Airbrush",id:"airbrush",level:3},{value:"Stamp density and "ratio-distance"",id:"stamp-density-and-ratio-distance",level:3},{value:"3D stroke",id:"3d-stroke",level:3}],d={toc:u},p="wrapper";function f(e){let{components:t,...n}=e;return(0,o.kt)(p,(0,a.Z)({},d,n,{components:t,mdxType:"MDXLayout"}),(0,o.kt)("p",null,"This tutorial series will teach you how to render brush strokes with the modern GPU graphics pipeline."),(0,o.kt)("p",null,"If you like this series, please star the ",(0,o.kt)("a",{parentName:"p",href:"https://github.com/ShenCiao/brush-rendering-tutorial"},"code repository")," instead of bookmark this website since the domain might be changed."),(0,o.kt)("h2",{id:"table-of-contents"},"Table of Contents"),(0,o.kt)("ul",null,(0,o.kt)("li",{parentName:"ul"},(0,o.kt)("a",{parentName:"li",href:"./introduction"},"Introduction")),(0,o.kt)("li",{parentName:"ul"},"Basics",(0,o.kt)("ul",{parentName:"li"},(0,o.kt)("li",{parentName:"ul"},(0,o.kt)("a",{parentName:"li",href:"./Basics/Vanilla"},"Vanilla")),(0,o.kt)("li",{parentName:"ul"},"Vanilla with variable radius"),(0,o.kt)("li",{parentName:"ul"},"Stamp"),(0,o.kt)("li",{parentName:"ul"},"Stamp with variable radius 1"),(0,o.kt)("li",{parentName:"ul"},"Stamp with variable radius 2"))),(0,o.kt)("li",{parentName:"ul"},"An interleave")),(0,o.kt)("h2",{id:"future-contents"},"Future Contents"),(0,o.kt)("h3",{id:"airbrush"},"Airbrush"),(0,o.kt)(r.ij,{uniforms:r.en,mdxType:"ArticulatedLine2D"}),(0,o.kt)("p",null,'Airbrush is a special type of stamp brush.\nHere I\'m demonstrating a "continuous airbrush", which is mathematically continuous and needs a little bit of calculus to develop.\nYou will learn how to generalize a stamp brush into a continuous form.'),(0,o.kt)("h3",{id:"stamp-density-and-ratio-distance"},'Stamp density and "ratio-distance"'),(0,o.kt)("div",{className:"row row--no-gutters margin-left--xs"},(0,o.kt)("div",{className:"col col--6"},(0,o.kt)(r.ij,{uniforms:r.Sw,mdxType:"ArticulatedLine2D"}),(0,o.kt)("center",null,(0,o.kt)("em",null," Adjacent Dots "))),(0,o.kt)("div",{className:"col col--6"},(0,o.kt)(r.ij,{uniforms:r.rL,mdxType:"ArticulatedLine2D"}),(0,o.kt)("center",null,(0,o.kt)("em",null," Adjacent with one dot interleaved ")))),(0,o.kt)("br",null),(0,o.kt)("p",null,"You can see dots are adjacent to each other instead of equidistantly distributed.\nThe pattern is achieved by setting the intervals between dots proportional to their radii.\nYou will learn how to freely control stamp density along a stamp\nstroke. Very important for a serious project."),(0,o.kt)("h3",{id:"3d-stroke"},"3D stroke"),(0,o.kt)("p",null,"Learn how to extend the algorithms to 3D space."),(0,o.kt)("p",null,"I'm integrating it into the Blender Grease Pencil:"),(0,o.kt)("iframe",{width:"100%",height:"500",src:"https://www.youtube.com/embed/Q7_3IhgHOZM?start=30",title:"Blender Grease Pencil Stamp Brush Demo",allow:"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",allowFullScreen:!0}))}f.isMDXComponent=!0},3618:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>a});const a={plain:{color:"#F8F8F2",backgroundColor:"#282A36"},styles:[{types:["prolog","constant","builtin"],style:{color:"rgb(189, 147, 249)"}},{types:["inserted","function"],style:{color:"rgb(80, 250, 123)"}},{types:["deleted"],style:{color:"rgb(255, 85, 85)"}},{types:["changed"],style:{color:"rgb(255, 184, 108)"}},{types:["punctuation","symbol"],style:{color:"rgb(248, 248, 242)"}},{types:["string","char","tag","selector"],style:{color:"rgb(255, 121, 198)"}},{types:["keyword","variable"],style:{color:"rgb(189, 147, 249)",fontStyle:"italic"}},{types:["comment"],style:{color:"rgb(98, 114, 164)"}},{types:["attr-name"],style:{color:"rgb(241, 250, 140)"}}]}},7694:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>a});const a={plain:{color:"#393A34",backgroundColor:"#f6f8fa"},styles:[{types:["comment","prolog","doctype","cdata"],style:{color:"#999988",fontStyle:"italic"}},{types:["namespace"],style:{opacity:.7}},{types:["string","attr-value"],style:{color:"#e3116c"}},{types:["punctuation","operator"],style:{color:"#393A34"}},{types:["entity","url","symbol","number","boolean","variable","constant","property","regex","inserted"],style:{color:"#36acaa"}},{types:["atrule","keyword","attr-name","selector"],style:{color:"#00a4db"}},{types:["function","deleted","tag"],style:{color:"#d73a49"}},{types:["function-variable"],style:{color:"#6f42c1"}},{types:["tag","selector","keyword"],style:{color:"#00009f"}}]}}}]); \ No newline at end of file diff --git a/assets/js/935f2afb.7ca1a7a0.js b/assets/js/935f2afb.6c3369f6.js similarity index 82% rename from assets/js/935f2afb.7ca1a7a0.js rename to assets/js/935f2afb.6c3369f6.js index 934d91c..dfbeb67 100644 --- a/assets/js/935f2afb.7ca1a7a0.js +++ b/assets/js/935f2afb.6c3369f6.js @@ -1 +1 @@ -"use strict";(self.webpackChunkbrush_stroke_tutorial=self.webpackChunkbrush_stroke_tutorial||[]).push([[53],{1109:e=>{e.exports=JSON.parse('{"pluginId":"default","version":"current","label":"Next","banner":null,"badge":false,"noIndex":false,"className":"docs-version-current","isLast":true,"docsSidebars":{"tutorialSidebar":[{"type":"link","label":"Table of Contents","href":"/brush-rendering-tutorial/","docId":"toc"},{"type":"link","label":"Introduction","href":"/brush-rendering-tutorial/Introduction/","docId":"Introduction/Introduction"},{"type":"category","label":"Basics","collapsible":false,"collapsed":false,"customProps":{"description":"This description can be used in the swizzled DocCard"},"className":"red","items":[{"type":"link","label":"Vanilla","href":"/brush-rendering-tutorial/Basics/Vanilla/","docId":"Basics/Vanilla/Vanilla"}],"href":"/brush-rendering-tutorial/category/basics"},{"type":"link","label":"Tessellation","href":"/brush-rendering-tutorial/Tessellation/","docId":"Tessellation/Tessellation"},{"type":"category","label":"Appendix","collapsible":true,"collapsed":true,"customProps":{"description":"This description can be used in the swizzled DocCard"},"items":[{"type":"link","label":"Vector Fill","href":"/brush-rendering-tutorial/Appendix/Vector-fill/","docId":"Appendix/Vector-fill/Vector-fill"}],"href":"/brush-rendering-tutorial/category/appendix"},{"type":"link","label":"\u2192 I\'m applying for a PhD","href":"/brush-rendering-tutorial/About/","docId":"About/About"}]},"docs":{"About/About":{"id":"About/About","title":"About","description":"Applying for Ph.D.","sidebar":"tutorialSidebar"},"Appendix/Vector-fill/Vector-fill":{"id":"Appendix/Vector-fill/Vector-fill","title":"Pre-introduction to Vector Fill","description":"You may have learned how to render brush strokes on polyline curves.","sidebar":"tutorialSidebar"},"Basics/Vanilla/Vanilla":{"id":"Basics/Vanilla/Vanilla","title":"Vanilla","description":"Shader code will be introduced in this article. Feel free to play with.","sidebar":"tutorialSidebar"},"Introduction/Introduction":{"id":"Introduction/Introduction","title":"Introduction","description":"This tutorial series will teach you how to use the modern GPU graphics pipeline to render brush strokes on vector curves.","sidebar":"tutorialSidebar"},"Tessellation/Tessellation":{"id":"Tessellation/Tessellation","title":"Tessellation-based Rendering","description":"There were works trying to tessellate a stroke and render it with GPU.","sidebar":"tutorialSidebar"},"toc":{"id":"toc","title":"Table of Contents","description":"Vanilla","sidebar":"tutorialSidebar"}}}')}}]); \ No newline at end of file +"use strict";(self.webpackChunkbrush_stroke_tutorial=self.webpackChunkbrush_stroke_tutorial||[]).push([[53],{1109:e=>{e.exports=JSON.parse('{"pluginId":"default","version":"current","label":"Next","banner":null,"badge":false,"noIndex":false,"className":"docs-version-current","isLast":true,"docsSidebars":{"tutorialSidebar":[{"type":"link","label":"Table of Contents","href":"/brush-rendering-tutorial/","docId":"toc"},{"type":"link","label":"Introduction","href":"/brush-rendering-tutorial/Introduction/","docId":"Introduction/Introduction"},{"type":"category","label":"Basics","collapsible":false,"collapsed":false,"customProps":{"description":"This description can be used in the swizzled DocCard"},"className":"red","items":[{"type":"link","label":"Vanilla","href":"/brush-rendering-tutorial/Basics/Vanilla/","docId":"Basics/Vanilla/Vanilla"}],"href":"/brush-rendering-tutorial/category/basics"},{"type":"link","label":"Tessellation","href":"/brush-rendering-tutorial/Tessellation/","docId":"Tessellation/Tessellation"},{"type":"category","label":"Appendix","collapsible":true,"collapsed":true,"customProps":{"description":"This description can be used in the swizzled DocCard"},"items":[{"type":"link","label":"Vector Fill","href":"/brush-rendering-tutorial/Appendix/Vector-fill/","docId":"Appendix/Vector-fill/Vector-fill"}],"href":"/brush-rendering-tutorial/category/appendix"},{"type":"link","label":"\u2192 I\'m applying for a PhD","href":"/brush-rendering-tutorial/About/","docId":"About/About"}]},"docs":{"About/About":{"id":"About/About","title":"About","description":"Applying for Ph.D.","sidebar":"tutorialSidebar"},"Appendix/Vector-fill/Vector-fill":{"id":"Appendix/Vector-fill/Vector-fill","title":"Pre-introduction to Vector Fill","description":"You may have learned how to render brush strokes on polyline curves.","sidebar":"tutorialSidebar"},"Basics/Vanilla/Vanilla":{"id":"Basics/Vanilla/Vanilla","title":"Vanilla","description":"Shader code will be introduced in this article. Feel free to play with.","sidebar":"tutorialSidebar"},"Introduction/Introduction":{"id":"Introduction/Introduction","title":"Introduction","description":"This tutorial series will teach you how to use the modern GPU graphics pipeline to render brush strokes,","sidebar":"tutorialSidebar"},"Tessellation/Tessellation":{"id":"Tessellation/Tessellation","title":"Tessellation-based Rendering","description":"There were works trying to tessellate a stroke and render it with GPU.","sidebar":"tutorialSidebar"},"toc":{"id":"toc","title":"Table of Contents","description":"This tutorial series will teach you how to render brush strokes with the modern GPU graphics pipeline.","sidebar":"tutorialSidebar"}}}')}}]); \ No newline at end of file diff --git a/assets/js/ac092286.ef95a417.js b/assets/js/ac092286.ef95a417.js deleted file mode 100644 index 0ab3a2f..0000000 --- a/assets/js/ac092286.ef95a417.js +++ /dev/null @@ -1 +0,0 @@ -(self.webpackChunkbrush_stroke_tutorial=self.webpackChunkbrush_stroke_tutorial||[]).push([[364],{1410:(e,t,n)=>{const a=n(7694),r=n(3618),o={title:"Brush Rendering Tutorial",tagline:"Learn brush stroke rendering.",url:"https://shenciao.github.io",baseUrl:"/brush-rendering-tutorial/",organizationName:"ShenCiao",projectName:"brush-rendering-tutorial",onBrokenLinks:"throw",onBrokenMarkdownLinks:"warn",i18n:{defaultLocale:"en",locales:["en"]},presets:[["classic",{docs:{routeBasePath:"/",sidebarPath:6679,editUrl:"https://github.com/ShenCiao/brush-rendering-tutorial/tree/main"},blog:!1,theme:{customCss:2295}}]],themeConfig:{colorMode:{disableSwitch:!0},image:"img/vanilla-stroke.png",navbar:{title:"Brush Rendering Tutorial",logo:{alt:"logo",src:"img/vanilla-stroke.png"},items:[{type:"docSidebar",sidebarId:"tutorialSidebar",position:"right",label:"Tutorial"},{href:"https://github.com/ShenCiao/brush-stroke-tutorial",label:"GitHub",position:"right"}]},footer:{style:"light",copyright:`Copyright \xa9 ${(new Date).getFullYear()} Brush Rendering Tutorial, under CC BY-SA 4.0 License`},prism:{theme:a,darkTheme:r},docs:{sidebar:{hideable:!0}}},plugins:["raw-loaders"],trailingSlash:!0};e.exports=o},6679:e=>{e.exports={tutorialSidebar:[{type:"autogenerated",dirName:"."}]}},5632:(e,t,n)=>{"use strict";n.d(t,{ij:()=>v,en:()=>x,Sw:()=>T,rL:()=>I,PQ:()=>k});var a=n(7294),r=n(9477),o=n(5452),i=n(4866),s=n(5162),l=n(3764),c=n(5034),u=n(9279);const d="precision mediump float;\nprecision mediump int;\n\nuniform mat4 modelViewMatrix;\nuniform mat4 projectionMatrix;\n\nin vec2 position0;\nin float radius0;\nin float summedLength0;\nin vec2 position1;\nin float radius1;\nin float summedLength1;\n\nout vec2 p; // position of the current pixel\nflat out vec2 p0;\nflat out float r0;\nflat out float l0;\nflat out vec2 p1;\nflat out float r1;\nflat out float l1;\n\nvoid main()\t{\n r0 = radius0;\n r1 = radius1;\n p0 = position0;\n p1 = position1;\n l0 = summedLength0;\n l1 = summedLength1;\n\n vec2 tangent = normalize(position1 - position0);\n vec2 normal = vec2(-tangent.y, tangent.x);\n float cosTheta = (r0 - r1)/distance(p0, p1); // theta is the angle stroke tilt, there is a diagram in README to explain this.\n // the vertex1 with radius is fully inside the vertex0.\n if(abs(cosTheta) >= 1.0) return;\n\n // Each instance is a trapzoid, whose vertices' positions are determined here.\n // Use gl_VertexID {0, 1, 2, 3} to index and get the desired parameters.\n // Be careful with the backface culling! We are ignoring it here.\n vec2 offsetSign = vec2[](\n vec2(-1.0,-1.0),\n vec2(-1.0, 1.0),\n vec2( 1.0, 1.0),\n vec2( 1.0,-1.0)\n )[gl_VertexID];\n vec2 position = vec2[](position0, position0, position1, position1)[gl_VertexID];\n float radius = vec4(radius0, radius0, radius1, radius1)[gl_VertexID];\n\n float tanHalfTheta = sqrt((1.0+cosTheta) / (1.0-cosTheta));\n float cotHalfTheta = 1.0 / tanHalfTheta;\n float normalTanValue = vec4(tanHalfTheta, tanHalfTheta, cotHalfTheta, cotHalfTheta)[gl_VertexID];\n if(normalTanValue > 10.0 || normalTanValue < 0.1) return;\n\n vec2 trapzoidVertexPosition = position +\n offsetSign.x * radius * tangent +\n offsetSign.y * radius * normal * normalTanValue;\n p = trapzoidVertexPosition;\n\n gl_Position = projectionMatrix * modelViewMatrix * vec4(trapzoidVertexPosition, 0.0, 1.0);\n}\n",p="precision mediump float;\nprecision mediump int;\n\nin vec2 p;\nflat in vec2 p0;\nflat in float r0;\nflat in float l0;\nflat in vec2 p1;\nflat in float r1;\nflat in float l1;\n\n// Common\nuniform int type;\nconst int Vanilla = 0, Stamp = 1, Airbrush = 2;\nuniform vec4 color;\n// Stamp\nuniform mediump sampler2D footprint;\nuniform float stampIntervalRatio;\nuniform float noiseFactor;\nuniform float rotationFactor;\nfloat x2n(float x); // from distance to stamp index.\nfloat n2x(float n); // from stamp index to distance.\nmat2 rotate(float angle);\n// Airbrush\nuniform mediump sampler2D gradient;\nfloat sampleGraident(float distance){ return texture(gradient, vec2(distance, 0.0)).r; }\n\n// Noise helper functions from _The Book of Shader_.\nfloat random (in vec2 st);\nfloat noise (in vec2 st);\nfloat fbm (in vec2 st);\n\nout vec4 outColor;\n\nvoid main() {\n vec2 tangent = normalize(p1 - p0);\n vec2 normal = vec2(-tangent.y, tangent.x);\n\n // The local coordinate orgin at p0, x axis along the tangent direct.\n float len = distance(p1, p0);\n vec2 pLocal = vec2(dot(p-p0, tangent), dot(p-p0, normal));\n vec2 p0Local = vec2(0, 0);\n vec2 p1Local = vec2(len, 0);\n\n float cosTheta = (r0 - r1)/len;\n float d0 = distance(p, p0);\n float d0cos = pLocal.x / d0;\n float d1 = distance(p, p1);\n float d1cos = (pLocal.x - len) / d1;\n\n // Remove corners\n if(d0cos < cosTheta && d0 > r0) discard;\n if(d1cos > cosTheta && d1 > r1) discard;\n\n if(type == Vanilla){\n if(d0 < r0 && d1 < r1) discard;\n float A = (d0 < r0 || d1 < r1) ? 1.0 - sqrt(1.0 - color.a) : color.a;\n outColor = vec4(color.rgb, A);\n return;\n }\n\n if(type == Stamp){\n // The method here is not published yet, it should be explained in a 10min video.\n // The footprint is a disk instead of a square.\n // We set a quadratic polynomial to calculate the effect range, the range on polyline edge footprint can touch the current pixel.\n // Two roots of the quadratic polynomial are the effectRangeFront and effectRangeBack.\n // Formulas from SIGGRAPH 2022 Talk - A Fast & Robust Solution for Cubic & Higher-Order Polynomials\n float a, b, c, delta;\n a = 1.0 - pow(cosTheta, 2.0);\n b = 2.0 * (r0 * cosTheta - pLocal.x);\n c = pow(pLocal.x, 2.0) + pow(pLocal.y, 2.0) - pow(r0, 2.0);\n delta = pow(b, 2.0) - 4.0*a*c;\n if(delta <= 0.0) discard; // This should never happen.\n\n float tempMathBlock = b + sign(b) * sqrt(delta);\n float x1 = -2.0 * c / tempMathBlock;\n float x2 = -tempMathBlock / (2.0*a);\n float effectRangeFront = x1 <= x2 ? x1 : x2;\n float effectRangeBack = x1 > x2 ? x1 : x2;\n\n // We stamp on polyline every time the stamp index comes to an integer.\n float index0 = l0/stampIntervalRatio; // The stamp index of vertex0.\n float startIndex, endIndex;\n if (effectRangeFront <= 0.0){\n startIndex = ceil(index0);\n }\n else{\n startIndex = ceil(index0 + x2n(effectRangeFront));\n }\n float index1 = l1/stampIntervalRatio;\n float backIndex = x2n(effectRangeBack) + index0;\n endIndex = index1 < backIndex ? index1 : backIndex;\n if(startIndex > endIndex) discard;\n\n // The main loop to sample and blend color from the footprint.\n int MAX_i = 128; float currIndex = startIndex;\n float A = 0.0;\n for(int i = 0; i < MAX_i; i++){\n float currStampLocalX = n2x(currIndex - index0);\n // Apply roation and sample the footprint.\n vec2 pToCurrStamp = pLocal - vec2(currStampLocalX, 0.0);\n float currStampRadius = r0 - cosTheta * currStampLocalX;\n float angle = rotationFactor*radians(360.0*fract(sin(currIndex)*1.0));\n pToCurrStamp *= rotate(angle);\n vec2 textureCoordinate = (pToCurrStamp/currStampRadius + 1.0)/2.0;\n float opacity = texture(footprint, textureCoordinate).a;\n // Blend opacity.\n float opacityNoise = noiseFactor*fbm(textureCoordinate*50.0);\n opacity = clamp(opacity - opacityNoise, 0.0, 1.0) * color.a;\n A = A * (1.0-opacity) + opacity;\n\n currIndex += 1.0;\n if(currIndex > endIndex) break;\n }\n if(A < 1e-4) discard;\n outColor = vec4(color.rgb, A);\n return;\n }\n\n if(type == Airbrush){\n // The method here is not published yet. Shen is not fully satisfied with the current solution.\n float tanTheta = sqrt(1.0 - cosTheta*cosTheta)/cosTheta;\n float mid = pLocal.x - abs(pLocal.y)/tanTheta;\n float A = color.a;\n float transparency0 = d0 > r0 ? 1.0:sqrt(1.0 - A*sampleGraident(d0/r0));\n float transparency1 = d1 > r1 ? 1.0:sqrt(1.0 - A*sampleGraident(d1/r1));\n float transparency;\n\n // A bunch of math derived with the continuous form of airbrush here.\n if(mid <= 0.0){\n transparency = transparency0/transparency1;\n }\n if(mid > 0.0 && mid < len){\n float r = (mid * r1 + (len - mid) * r0)/len;\n float dr = distance(pLocal, vec2(mid, 0))/r;\n transparency = (1.0 - A*sampleGraident(dr))/transparency0/transparency1;\n }\n if(mid >= len){\n transparency = transparency1/transparency0;\n }\n\n outColor = vec4(color.rgb, 1.0 - transparency);\n }\n}\n\nfloat x2n(float x){\n float L = distance(p0, p1);\n if(r0 == r1) return x/(stampIntervalRatio*r0);\n else return -L / stampIntervalRatio / (r0 - r1) * log(1.0 - (1.0 - r1/r0)/L * x);\n}\n\nfloat n2x(float n){\n float L = distance(p0, p1);\n if(r0 == r1) return n * stampIntervalRatio * r0;\n else return L * (1.0-exp(-(r0-r1)*n*stampIntervalRatio/L)) / (1.0-r1/r0);\n}\n\n// Helper functions----------------------------------------------------------------------------------\nmat2 rotate(float angle){\n return mat2(cos(angle), -sin(angle), sin(angle), cos(angle));\n}\n\nfloat random (in vec2 st) {\n return fract(sin(dot(st.xy,\n vec2(12.9898,78.233)))*\n 43758.5453123);\n}\n\nfloat noise (in vec2 st) {\n vec2 i = floor(st);\n vec2 f = fract(st);\n\n // Four corners in 2D of a tile\n float a = random(i);\n float b = random(i + vec2(1.0, 0.0));\n float c = random(i + vec2(0.0, 1.0));\n float d = random(i + vec2(1.0, 1.0));\n\n vec2 u = f * f * (3.0 - 2.0 * f);\n\n return mix(a, b, u.x) +\n (c - a)* u.y * (1.0 - u.x) +\n (d - b) * u.x * u.y;\n}\n\n#define OCTAVES 6\nfloat fbm (in vec2 st) {\n // Initial values\n float value = 0.0;\n float amplitude = .5;\n float frequency = 0.;\n //\n // Loop of octaves\n for (int i = 0; i < OCTAVES; i++) {\n value += amplitude * noise(st);\n st *= 2.;\n amplitude *= .5;\n }\n return value;\n}\n";var m=n(1410),h=n.n(m),f=n(412);let g=function(e){return e[e.Vanilla=0]="Vanilla",e[e.Stamp=1]="Stamp",e[e.Airbrush=2]="Airbrush",e}({});function v(e){let{uniforms:t=null,showEditor:n=null}=e;const m=(0,a.useRef)(),h=(0,a.useRef)(),f=(0,a.useRef)();function v(e,t,n){const a=[...t],o=[...t.slice(2)],i=[...n],s=[...n.slice(1)],l=[];let c=0;for(let p=0;p{const e=(1+Math.sqrt(5))/2,n=m.current.clientWidth,a=n*(.5/e),i=4*e,s=i*(.5/e),l=new r.iKG(i/-2,i/2,s/2,s/-2,-1e3,1e3);l.position.z=5;const c=new r.CP7({antialias:!0,alpha:!0,premultipliedAlpha:!1,powerPreference:"high-performance"});function y(){const t=m.current.clientWidth,n=.5*t/e;c.setSize(t,n)}c.setClearColor(new r.Ilk(1,1,1),0),c.setSize(n,a),window.addEventListener("resize",y),m.current.appendChild(c.domElement);const b=new r.xsS,k=new o.o(l,c.domElement);k.enableRotate=!1,k.enableDamping=!1,k.screenSpacePanning=!0,k.addEventListener("change",(()=>{c.render(b,l)})),h.current=()=>c.render(b,l),window.addEventListener("TextureLoaded",h.current);const w=new r.u9r;w.setIndex([0,1,2,2,3,0]);const x=new Function(u.Z),[T,I]=x();v(w,T,I);const A={type:{value:g.Vanilla},color:{value:[0,0,0,1]},footprint:{value:new r.xEZ},stampIntervalRatio:{value:1},noiseFactor:{value:0},rotationFactor:{value:0},gradient:{value:new r.IEO}},S=new r.FIo({uniforms:t||A,vertexShader:d,fragmentShader:p,side:r.ehD,transparent:!0,glslVersion:r.LSk});return f.current=new r.SPe(w,S,T.length-1),f.current.frustumCulled=!1,b.add(f.current),h.current(),()=>{c.dispose(),window.removeEventListener("resize",y),window.removeEventListener("TextureLoaded",h.current)}}),[]);const b=(0,a.useCallback)(((e,t)=>{let n=[],a=[];try{const t=new Function(e);[n,a]=t()}catch(o){return void console.log(o.toString())}function r(e){if(Array.isArray(e)){for(let t=0;t{y(e,"")}})),T&&a.createElement(s.Z,{value:"fragment.glsl"},a.createElement(c.r,{height:k,defaultValue:p,onChange:e=>{y("",e)}})))),a.createElement("div",{ref:m,style:{width:"100%"},onMouseDown:e=>e.preventDefault()}))}let y=new r.xEZ;f.Z.canUseDOM&&(y=(new r.dpR).load(`/${h().projectName}/img/stamp2.png`,(e=>{window.dispatchEvent(new CustomEvent("TextureLoaded"))}),void 0,void 0));let b=new r.xEZ;f.Z.canUseDOM&&(b=(new r.dpR).load(`/${h().projectName}/img/dot.png`,(e=>{window.dispatchEvent(new CustomEvent("TextureLoaded"))}),void 0,void 0));const k={type:{value:g.Stamp},color:{value:[0,0,0,1]},footprint:{value:y},stampIntervalRatio:{value:.4},noiseFactor:{value:1.2},rotationFactor:{value:.75}},w=((e,t)=>{let n=new r.AXT(new r.FM8(0,1),e,t,new r.FM8(1,0));const a=256,o=new Uint8Array(1024),i=n.getPoints(512);for(let r=0;r=n.x&&e<=a.x){let t=(n.y*(a.x-e)+a.y*(e-n.x))/(a.x-n.x);o[4*r]=Math.floor(255*t)}}}const s=new r.IEO(o,a,1);return s.needsUpdate=!0,s})(new r.FM8(.33,1),new r.FM8(.66,0)),x={type:{value:g.Airbrush},color:{value:[0,0,0,1]},gradient:{value:w}},T={type:{value:g.Stamp},color:{value:[0,0,0,.5]},footprint:{value:b},stampIntervalRatio:{value:2},noiseFactor:{value:0},rotationFactor:{value:0}},I={type:{value:g.Stamp},color:{value:[0,0,0,.5]},footprint:{value:b},stampIntervalRatio:{value:1},noiseFactor:{value:0},rotationFactor:{value:0}}},5034:(e,t,n)=>{"use strict";n.d(t,{r:()=>l});var a=n(7462),r=n(7294);const o={comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"[",close:"]"},{open:"{",close:"}"},{open:"(",close:")"},{open:"'",close:"'",notIn:["string","comment"]},{open:'"',close:'"',notIn:["string"]}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}]},i={tokenPostfix:".glsl",defaultToken:"invalid",keywords:["const","uniform","break","continue","do","for","while","if","else","switch","case","in","out","inout","true","false","invariant","discard","return","sampler2D","samplerCube","sampler3D","struct","radians","degrees","sin","cos","tan","asin","acos","atan","pow","sinh","cosh","tanh","asinh","acosh","atanh","exp","log","exp2","log2","sqrt","inversesqrt","abs","sign","floor","ceil","round","roundEven","trunc","fract","mod","modf","min","max","clamp","mix","step","smoothstep","length","distance","dot","cross ","determinant","inverse","normalize","faceforward","reflect","refract","matrixCompMult","outerProduct","transpose","lessThan ","lessThanEqual","greaterThan","greaterThanEqual","equal","notEqual","any","all","not","packUnorm2x16","unpackUnorm2x16","packSnorm2x16","unpackSnorm2x16","packHalf2x16","unpackHalf2x16","dFdx","dFdy","fwidth","textureSize","texture","textureProj","textureLod","textureGrad","texelFetch","texelFetchOffset","textureProjLod","textureLodOffset","textureGradOffset","textureProjLodOffset","textureProjGrad","intBitsToFloat","uintBitsToFloat","floatBitsToInt","floatBitsToUint","isnan","isinf","vec2","vec3","vec4","ivec2","ivec3","ivec4","uvec2","uvec3","uvec4","bvec2","bvec3","bvec4","mat2","mat3","mat2x2","mat2x3","mat2x4","mat3x2","mat3x3","mat3x4","mat4x2","mat4x3","mat4x4","mat4","float","int","uint","void","bool"],operators:["=",">","<","!","~","?",":","==","<=",">=","!=","&&","||","++","--","+","-","*","/","&","|","^","%","<<",">>",">>>","+=","-=","*=","/=","&=","|=","^=","%=","<<=",">>=",">>>="],symbols:/[=>{n.languages.register({id:"glsl"}),n.languages.setMonarchTokensProvider("glsl",i),n.languages.setLanguageConfiguration("glsl",o),"function"==typeof e.onMount&&e.onMount(t,n)}}))}},9279:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});const a="// Generate sinewave geometry \nconst maxRadius = 1/3;\nconst segmentCount = 32;\n\nconst position = [];\nconst radius = [];\n\nconst gr = (1 + Math.sqrt(5)) / 2; // golden ratio\nconst pi = Math.PI;\n\nfor(let i = 0; i <= segmentCount; ++i){\n let a = i / segmentCount\n let x = -pi + (2 * pi * a);\n let y = Math.sin(x) / gr;\n let r = Math.cos(x / 2.0) * maxRadius;\n\n position.push(x, y);\n radius.push(r);\n}\n\nreturn [position, radius];\n"},154:(e,t,n)=>{"use strict";n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>s,default:()=>m,frontMatter:()=>i,metadata:()=>l,toc:()=>u});var a=n(7462),r=(n(7294),n(3905)),o=n(5632);const i={title:"Introduction",sidebar_position:2},s=void 0,l={unversionedId:"Introduction/Introduction",id:"Introduction/Introduction",title:"Introduction",description:"This tutorial series will teach you how to use the modern GPU graphics pipeline to render brush strokes on vector curves.",source:"@site/docs/Introduction/Introduction.mdx",sourceDirName:"Introduction",slug:"/Introduction/",permalink:"/brush-rendering-tutorial/Introduction/",draft:!1,editUrl:"https://github.com/ShenCiao/brush-rendering-tutorial/tree/main/docs/Introduction/Introduction.mdx",tags:[],version:"current",sidebarPosition:2,frontMatter:{title:"Introduction",sidebar_position:2},sidebar:"tutorialSidebar",previous:{title:"Table of Contents",permalink:"/brush-rendering-tutorial/"},next:{title:"Basics",permalink:"/brush-rendering-tutorial/category/basics"}},c={},u=[{value:"Modern GPU",id:"modern-gpu",level:2},{value:"Brush strokes",id:"brush-strokes",level:2},{value:"Vector curves",id:"vector-curves",level:2},{value:"Structure",id:"structure",level:2},{value:"Citation",id:"citation",level:2}],d={toc:u},p="wrapper";function m(e){let{components:t,...i}=e;return(0,r.kt)(p,(0,a.Z)({},d,i,{components:t,mdxType:"MDXLayout"}),(0,r.kt)("p",null,"This tutorial series will teach you how to use the ",(0,r.kt)("strong",{parentName:"p"},"modern GPU")," graphics pipeline to render ",(0,r.kt)("strong",{parentName:"p"},"brush strokes")," on ",(0,r.kt)("strong",{parentName:"p"},"vector curves"),".\nThe contents mainly come from my research work ",(0,r.kt)("a",{parentName:"p",href:"https://github.com/ShenCiao/Ciallo"},"Ciallo: The next generation vector paint program"),".\nI will introduce this tutorial from the three aspects above."),(0,r.kt)("h2",{id:"modern-gpu"},"Modern GPU"),(0,r.kt)("p",null,(0,r.kt)("img",{alt:"sketchpad",src:n(9614).Z,width:"480",height:"360"})),(0,r.kt)("p",null,(0,r.kt)("em",{parentName:"p"},"Draw lines in Ivan Sutherland's Sketchpad.")),(0,r.kt)("p",null,"Drawing lines or rendering strokes is one of the oldest topics in Computer Graphics.\nYou can easily find a lot of pioneering works, for example, ",(0,r.kt)("a",{parentName:"p",href:"https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm"},"Bresenham's line algorithm"),".\nThey emerged from an era with certain conditions:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},"Programs ran without the benefit of parallelization."),(0,r.kt)("li",{parentName:"ul"},"Programs could access framebuffer directly without significant performance penalty.")),(0,r.kt)("p",null,"But time has changed, now we have modern GPU hardware crafted for graphics and parallel computing,\nand directly accessing a GPU framebuffer from a CPU can significantly hurt the performance.\nSo old algorithms may not satisfy your needs for real-time rendering."),(0,r.kt)("p",null,"In this tutorial, you will learn about the brush stroke rendering algorithms designed for the GPU graphics pipeline.\nWe (I and my mentor ",(0,r.kt)("a",{parentName:"p",href:"https://www.liyiwei.org/"},"Liyi-Wei"),") call these algorithms ",(0,r.kt)("em",{parentName:"p"},"Articulated")," in our paper, because they look like drawing an articulated arm.\nI assume our readers are already familiar with a graphics API like OpenGL or D3D.\nThis tutorial will concentrate more on the high-level algorithms than the implementation details."),(0,r.kt)("p",null,"Although graphics APIs provide us line primitives, including ",(0,r.kt)("inlineCode",{parentName:"p"},"LINES"),", ",(0,r.kt)("inlineCode",{parentName:"p"},"LINE_STRIP"),", and ",(0,r.kt)("inlineCode",{parentName:"p"},"LINE_LOOP"),",\nthere are several well-known issues when using these primitives directly.\nCheck out Matt Deslauries' article ",(0,r.kt)("a",{parentName:"p",href:"https://mattdesl.svbtle.com/drawing-lines-is-hard#line-primitives_1"},(0,r.kt)("em",{parentName:"a"},"Drawing Lines is Hard"))," if you know nothing about them.\nAs for our brush rendering, the most significant issue is the limitation on the maximum line width or stroke radius (half width).\nWe must be able to fully control the radius values when rendering brush strokes."),(0,r.kt)("h2",{id:"brush-strokes"},"Brush strokes"),(0,r.kt)("p",null,"Brush strokes refer to strokes drawn with the paint tool in graphics software such as Photoshop or Krita.\nArtists configure their digital brushes to control stroke properties like radius or stylization,\nthen stroke on the canvas with dedicated input devices: Tablet and Stylus.\nIf you're unfamiliar with tablets and styluses, you can watch the video below for more information:"),(0,r.kt)("p",null,(0,r.kt)("a",{parentName:"p",href:"https://www.youtube.com/watch?app=desktop&v=83BRMfjJXIk"},(0,r.kt)("img",{parentName:"a",src:"https://img.youtube.com/vi/83BRMfjJXIk/maxresdefault.jpg",alt:"Tablet"}))),(0,r.kt)("p",null,"While you may recognize a brush stroke by its stylization, another crucial property could be ignored: the variable radius along the stroke.\n(I ignored it in my paper too.)\nThe radii are typically generated from the pressure values as a stylus presses and moves on a tablet.\nFor experienced artists after installing a painting program, one of the highest priorities is to configure the mapping function from pen pressure to brush radius."),(0,r.kt)("p",null,'In this tutorial, you will learn to render a stroke with variable radius, and the most popular way to stylize it called "Stamp."\nMore than 90 percent of brushes in popular paint software are the stamp brushes.\nAdditionally, GPU brush stroke rendering a newly emerged topic.\nResearchers will develop more novel methods in the future.\nSo I will continuously update this tutorial series to teach them.\nMake sure to star our ',(0,r.kt)("a",{parentName:"p",href:"https://github.com/ShenCiao/brush-rendering-tutorial"},"code repository")," for easy access to the latest updates."),(0,r.kt)("h2",{id:"vector-curves"},"Vector curves"),(0,r.kt)("p",null,"Variable radius is imperative for the most artists working on digital painting,\nbut it's not included in public vector standards like ",(0,r.kt)("a",{parentName:"p",href:"https://www.w3.org/Graphics/SVG/WG/wiki/Proposals/Variable_width_stroke"},"SVG"),".\nAnd since that, configuring the variable width value of vector lines is commonly underdeveloped in popular graphics design software.\nThis limitation is one of the primary reasons that lots of digital artists don't use vector workflow.\n(Another one is filling color.)"),(0,r.kt)("p",null,"To support the variable radius, we will render a unique type of vector curve:\nAn ordered list of points (polyline) with radius values assigned to each point.\nAs a stylus is pressed and moved on a tablet, the program generate a sequence of points to record the trace of movement.\nAdditionally, the pen pressure is transformed into the radius value assigned to each point."),(0,r.kt)("p",null,"We can approximate any type of curve by increasing the number of points in a polyline, whether freehand-drawn or mathematically defined.\nTry to change the ",(0,r.kt)("inlineCode",{parentName:"p"},"maxRadius")," and ",(0,r.kt)("inlineCode",{parentName:"p"},"segmentCount")," value in the code editor below to see how the stroke changes.\nFeel free to change any other parts of the code as long as the function return the ",(0,r.kt)("inlineCode",{parentName:"p"},"position")," and ",(0,r.kt)("inlineCode",{parentName:"p"},"radius")," array correctly."),(0,r.kt)(o.ij,{showEditor:[!0,!1,!1],mdxType:"ArticulatedLine2D"}),(0,r.kt)("admonition",{title:"code editor & canvas",type:"info"},(0,r.kt)("p",{parentName:"admonition"},"The development environment is inspired by ",(0,r.kt)("a",{parentName:"p",href:"https://thebookofshaders.com/"},(0,r.kt)("em",{parentName:"a"},"The Book of Shader")),".\nYou can watch the rendering result in real time after modifying the code."),(0,r.kt)("p",{parentName:"admonition"},"When hovering your mouse on the canvas you can:"),(0,r.kt)("ul",{parentName:"admonition"},(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("strong",{parentName:"li"},"Pan"),": Left-click and drag the mouse."),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("strong",{parentName:"li"},"Zoom"),": Scroll or drag the mouse wheel.")),(0,r.kt)("p",{parentName:"admonition"},"If there are bugs for common usages, tell me at the ",(0,r.kt)("a",{parentName:"p",href:"https://github.com/ShenCiao/brush-rendering-tutorial/issues"},"issue")," page.")),(0,r.kt)("h2",{id:"structure"},"Structure"),(0,r.kt)("p",null,"Although the algorithms are very straightforward, I know it's hard to learn and reproduce a research work.\nThat's why I created this tutorial, designed with a smooth learning curve and providing the seamless development environment."),(0,r.kt)("p",null,"You should start with the Basic part, which covers the basics of the rendering methods.\nRemember to read the articles in the Basic part in its original order, or you may miss something important.\nNext, select your favorite topics to learn.\nI will list extra prerequisites at the very beginning of each article."),(0,r.kt)("p",null,"Wish you happy learning!"),(0,r.kt)("h2",{id:"citation"},"Citation"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre"},"@inproceedings{Ciallo2023,\n author = {Ciao, Shen and Wei, Li-Yi},\n title = {Ciallo: The next-Generation Vector Paint Program},\n year = {2023},\n isbn = {9798400701436},\n publisher = {Association for Computing Machinery},\n address = {New York, NY, USA},\n url = {https://doi.org/10.1145/3587421.3595418},\n doi = {10.1145/3587421.3595418},\n booktitle = {ACM SIGGRAPH 2023 Talks},\n articleno = {67},\n numpages = {2},\n keywords = {Digital painting, stylized stroke, arrangement, vector graphics. coloring, graphics processing unit (GPU)},\n location = {Los Angeles, CA, USA},\n series = {SIGGRAPH '23}\n}\n")),(0,r.kt)("admonition",{title:"Research Tip",type:"note"},(0,r.kt)("p",{parentName:"admonition"},"To demonstrate your research work about brush rendering, select vector drawings have variable radius or pen pressure data.\nRegular vector drawing datasets don't contain them."),(0,r.kt)("ul",{parentName:"admonition"},(0,r.kt)("li",{parentName:"ul"},"Zeyu Wang's work: ",(0,r.kt)("a",{parentName:"li",href:"https://dl.acm.org/doi/10.1145/3450626.3459819"},"Paper")," | ",(0,r.kt)("a",{parentName:"li",href:"https://github.com/zachzeyuwang/tracing-vs-freehand"},"Dataset")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"https://cloud.blender.org/p/gallery/5b642e25bf419c1042056fc6"},"Blender Grease Pencil")),(0,r.kt)("li",{parentName:"ul"},"... Tell me more in the ",(0,r.kt)("a",{parentName:"li",href:"https://github.com/ShenCiao/brush-rendering-tutorial/discussions/1"},"discussion"),"."))))}m.isMDXComponent=!0},3618:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>a});const a={plain:{color:"#F8F8F2",backgroundColor:"#282A36"},styles:[{types:["prolog","constant","builtin"],style:{color:"rgb(189, 147, 249)"}},{types:["inserted","function"],style:{color:"rgb(80, 250, 123)"}},{types:["deleted"],style:{color:"rgb(255, 85, 85)"}},{types:["changed"],style:{color:"rgb(255, 184, 108)"}},{types:["punctuation","symbol"],style:{color:"rgb(248, 248, 242)"}},{types:["string","char","tag","selector"],style:{color:"rgb(255, 121, 198)"}},{types:["keyword","variable"],style:{color:"rgb(189, 147, 249)",fontStyle:"italic"}},{types:["comment"],style:{color:"rgb(98, 114, 164)"}},{types:["attr-name"],style:{color:"rgb(241, 250, 140)"}}]}},7694:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>a});const a={plain:{color:"#393A34",backgroundColor:"#f6f8fa"},styles:[{types:["comment","prolog","doctype","cdata"],style:{color:"#999988",fontStyle:"italic"}},{types:["namespace"],style:{opacity:.7}},{types:["string","attr-value"],style:{color:"#e3116c"}},{types:["punctuation","operator"],style:{color:"#393A34"}},{types:["entity","url","symbol","number","boolean","variable","constant","property","regex","inserted"],style:{color:"#36acaa"}},{types:["atrule","keyword","attr-name","selector"],style:{color:"#00a4db"}},{types:["function","deleted","tag"],style:{color:"#d73a49"}},{types:["function-variable"],style:{color:"#6f42c1"}},{types:["tag","selector","keyword"],style:{color:"#00009f"}}]}},9614:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});const a=n.p+"assets/images/sketchpad-be46fe81f29f99371fdce79d1452ae85.gif"}}]); \ No newline at end of file diff --git a/assets/js/ac092286.fea992f7.js b/assets/js/ac092286.fea992f7.js new file mode 100644 index 0000000..ae2f5e5 --- /dev/null +++ b/assets/js/ac092286.fea992f7.js @@ -0,0 +1 @@ +(self.webpackChunkbrush_stroke_tutorial=self.webpackChunkbrush_stroke_tutorial||[]).push([[364],{1410:(e,t,n)=>{const a=n(7694),o=n(3618),r={title:"Brush Rendering Tutorial",tagline:"Learn brush stroke rendering.",url:"https://shenciao.github.io",baseUrl:"/brush-rendering-tutorial/",organizationName:"ShenCiao",projectName:"brush-rendering-tutorial",onBrokenLinks:"throw",onBrokenMarkdownLinks:"warn",i18n:{defaultLocale:"en",locales:["en"]},presets:[["classic",{docs:{routeBasePath:"/",sidebarPath:6679,editUrl:"https://github.com/ShenCiao/brush-rendering-tutorial/tree/main"},blog:!1,theme:{customCss:2295}}]],themeConfig:{colorMode:{disableSwitch:!0},image:"img/vanilla-stroke.png",navbar:{title:"Brush Rendering Tutorial",logo:{alt:"logo",src:"img/vanilla-stroke.png"},items:[{type:"docSidebar",sidebarId:"tutorialSidebar",position:"right",label:"Tutorial"},{href:"https://github.com/ShenCiao/brush-stroke-tutorial",label:"GitHub",position:"right"}]},footer:{style:"light",copyright:`Copyright \xa9 ${(new Date).getFullYear()} Brush Rendering Tutorial, under CC BY-SA 4.0 License`},prism:{theme:a,darkTheme:o},docs:{sidebar:{hideable:!0}}},plugins:["raw-loaders"],trailingSlash:!0};e.exports=r},6679:e=>{e.exports={tutorialSidebar:[{type:"autogenerated",dirName:"."}]}},5632:(e,t,n)=>{"use strict";n.d(t,{ij:()=>v,en:()=>w,Sw:()=>T,rL:()=>L,PQ:()=>b});var a=n(7294),o=n(9477),r=n(5452),i=n(4866),l=n(5162),s=n(3764),c=n(5034),u=n(9279);const d="precision mediump float;\nprecision mediump int;\n\nuniform mat4 modelViewMatrix;\nuniform mat4 projectionMatrix;\n\nin vec2 position0;\nin float radius0;\nin float summedLength0;\nin vec2 position1;\nin float radius1;\nin float summedLength1;\n\nout vec2 p; // position of the current pixel\nflat out vec2 p0;\nflat out float r0;\nflat out float l0;\nflat out vec2 p1;\nflat out float r1;\nflat out float l1;\n\nvoid main()\t{\n r0 = radius0;\n r1 = radius1;\n p0 = position0;\n p1 = position1;\n l0 = summedLength0;\n l1 = summedLength1;\n\n vec2 tangent = normalize(position1 - position0);\n vec2 normal = vec2(-tangent.y, tangent.x);\n float cosTheta = (r0 - r1)/distance(p0, p1); // theta is the angle stroke tilt, there is a diagram in README to explain this.\n // the vertex1 with radius is fully inside the vertex0.\n if(abs(cosTheta) >= 1.0) return;\n\n // Each instance is a trapzoid, whose vertices' positions are determined here.\n // Use gl_VertexID {0, 1, 2, 3} to index and get the desired parameters.\n // Be careful with the backface culling! We are ignoring it here.\n vec2 offsetSign = vec2[](\n vec2(-1.0,-1.0),\n vec2(-1.0, 1.0),\n vec2( 1.0, 1.0),\n vec2( 1.0,-1.0)\n )[gl_VertexID];\n vec2 position = vec2[](position0, position0, position1, position1)[gl_VertexID];\n float radius = vec4(radius0, radius0, radius1, radius1)[gl_VertexID];\n\n float tanHalfTheta = sqrt((1.0+cosTheta) / (1.0-cosTheta));\n float cotHalfTheta = 1.0 / tanHalfTheta;\n float normalTanValue = vec4(tanHalfTheta, tanHalfTheta, cotHalfTheta, cotHalfTheta)[gl_VertexID];\n if(normalTanValue > 10.0 || normalTanValue < 0.1) return;\n\n vec2 trapzoidVertexPosition = position +\n offsetSign.x * radius * tangent +\n offsetSign.y * radius * normal * normalTanValue;\n p = trapzoidVertexPosition;\n\n gl_Position = projectionMatrix * modelViewMatrix * vec4(trapzoidVertexPosition, 0.0, 1.0);\n}\n",p="precision mediump float;\nprecision mediump int;\n\nin vec2 p;\nflat in vec2 p0;\nflat in float r0;\nflat in float l0;\nflat in vec2 p1;\nflat in float r1;\nflat in float l1;\n\n// Common\nuniform int type;\nconst int Vanilla = 0, Stamp = 1, Airbrush = 2;\nuniform vec4 color;\n// Stamp\nuniform mediump sampler2D footprint;\nuniform float stampIntervalRatio;\nuniform float noiseFactor;\nuniform float rotationFactor;\nfloat x2n(float x); // from distance to stamp index.\nfloat n2x(float n); // from stamp index to distance.\nmat2 rotate(float angle);\n// Airbrush\nuniform mediump sampler2D gradient;\nfloat sampleGraident(float distance){ return texture(gradient, vec2(distance, 0.0)).r; }\n\n// Noise helper functions from _The Book of Shader_.\nfloat random (in vec2 st);\nfloat noise (in vec2 st);\nfloat fbm (in vec2 st);\n\nout vec4 outColor;\n\nvoid main() {\n vec2 tangent = normalize(p1 - p0);\n vec2 normal = vec2(-tangent.y, tangent.x);\n\n // The local coordinate orgin at p0, x axis along the tangent direct.\n float len = distance(p1, p0);\n vec2 pLocal = vec2(dot(p-p0, tangent), dot(p-p0, normal));\n vec2 p0Local = vec2(0, 0);\n vec2 p1Local = vec2(len, 0);\n\n float cosTheta = (r0 - r1)/len;\n float d0 = distance(p, p0);\n float d0cos = pLocal.x / d0;\n float d1 = distance(p, p1);\n float d1cos = (pLocal.x - len) / d1;\n\n // Remove corners\n if(d0cos < cosTheta && d0 > r0) discard;\n if(d1cos > cosTheta && d1 > r1) discard;\n\n if(type == Vanilla){\n if(d0 < r0 && d1 < r1) discard;\n float A = (d0 < r0 || d1 < r1) ? 1.0 - sqrt(1.0 - color.a) : color.a;\n outColor = vec4(color.rgb, A);\n return;\n }\n\n if(type == Stamp){\n // The method here is not published yet, it should be explained in a 10min video.\n // The footprint is a disk instead of a square.\n // We set a quadratic polynomial to calculate the effect range, the range on polyline edge footprint can touch the current pixel.\n // Two roots of the quadratic polynomial are the effectRangeFront and effectRangeBack.\n // Formulas from SIGGRAPH 2022 Talk - A Fast & Robust Solution for Cubic & Higher-Order Polynomials\n float a, b, c, delta;\n a = 1.0 - pow(cosTheta, 2.0);\n b = 2.0 * (r0 * cosTheta - pLocal.x);\n c = pow(pLocal.x, 2.0) + pow(pLocal.y, 2.0) - pow(r0, 2.0);\n delta = pow(b, 2.0) - 4.0*a*c;\n if(delta <= 0.0) discard; // This should never happen.\n\n float tempMathBlock = b + sign(b) * sqrt(delta);\n float x1 = -2.0 * c / tempMathBlock;\n float x2 = -tempMathBlock / (2.0*a);\n float effectRangeFront = x1 <= x2 ? x1 : x2;\n float effectRangeBack = x1 > x2 ? x1 : x2;\n\n // We stamp on polyline every time the stamp index comes to an integer.\n float index0 = l0/stampIntervalRatio; // The stamp index of vertex0.\n float startIndex, endIndex;\n if (effectRangeFront <= 0.0){\n startIndex = ceil(index0);\n }\n else{\n startIndex = ceil(index0 + x2n(effectRangeFront));\n }\n float index1 = l1/stampIntervalRatio;\n float backIndex = x2n(effectRangeBack) + index0;\n endIndex = index1 < backIndex ? index1 : backIndex;\n if(startIndex > endIndex) discard;\n\n // The main loop to sample and blend color from the footprint.\n int MAX_i = 128; float currIndex = startIndex;\n float A = 0.0;\n for(int i = 0; i < MAX_i; i++){\n float currStampLocalX = n2x(currIndex - index0);\n // Apply roation and sample the footprint.\n vec2 pToCurrStamp = pLocal - vec2(currStampLocalX, 0.0);\n float currStampRadius = r0 - cosTheta * currStampLocalX;\n float angle = rotationFactor*radians(360.0*fract(sin(currIndex)*1.0));\n pToCurrStamp *= rotate(angle);\n vec2 textureCoordinate = (pToCurrStamp/currStampRadius + 1.0)/2.0;\n float opacity = texture(footprint, textureCoordinate).a;\n // Blend opacity.\n float opacityNoise = noiseFactor*fbm(textureCoordinate*50.0);\n opacity = clamp(opacity - opacityNoise, 0.0, 1.0) * color.a;\n A = A * (1.0-opacity) + opacity;\n\n currIndex += 1.0;\n if(currIndex > endIndex) break;\n }\n if(A < 1e-4) discard;\n outColor = vec4(color.rgb, A);\n return;\n }\n\n if(type == Airbrush){\n // The method here is not published yet. Shen is not fully satisfied with the current solution.\n float tanTheta = sqrt(1.0 - cosTheta*cosTheta)/cosTheta;\n float mid = pLocal.x - abs(pLocal.y)/tanTheta;\n float A = color.a;\n float transparency0 = d0 > r0 ? 1.0:sqrt(1.0 - A*sampleGraident(d0/r0));\n float transparency1 = d1 > r1 ? 1.0:sqrt(1.0 - A*sampleGraident(d1/r1));\n float transparency;\n\n // A bunch of math derived with the continuous form of airbrush here.\n if(mid <= 0.0){\n transparency = transparency0/transparency1;\n }\n if(mid > 0.0 && mid < len){\n float r = (mid * r1 + (len - mid) * r0)/len;\n float dr = distance(pLocal, vec2(mid, 0))/r;\n transparency = (1.0 - A*sampleGraident(dr))/transparency0/transparency1;\n }\n if(mid >= len){\n transparency = transparency1/transparency0;\n }\n\n outColor = vec4(color.rgb, 1.0 - transparency);\n }\n}\n\nfloat x2n(float x){\n float L = distance(p0, p1);\n if(r0 == r1) return x/(stampIntervalRatio*r0);\n else return -L / stampIntervalRatio / (r0 - r1) * log(1.0 - (1.0 - r1/r0)/L * x);\n}\n\nfloat n2x(float n){\n float L = distance(p0, p1);\n if(r0 == r1) return n * stampIntervalRatio * r0;\n else return L * (1.0-exp(-(r0-r1)*n*stampIntervalRatio/L)) / (1.0-r1/r0);\n}\n\n// Helper functions----------------------------------------------------------------------------------\nmat2 rotate(float angle){\n return mat2(cos(angle), -sin(angle), sin(angle), cos(angle));\n}\n\nfloat random (in vec2 st) {\n return fract(sin(dot(st.xy,\n vec2(12.9898,78.233)))*\n 43758.5453123);\n}\n\nfloat noise (in vec2 st) {\n vec2 i = floor(st);\n vec2 f = fract(st);\n\n // Four corners in 2D of a tile\n float a = random(i);\n float b = random(i + vec2(1.0, 0.0));\n float c = random(i + vec2(0.0, 1.0));\n float d = random(i + vec2(1.0, 1.0));\n\n vec2 u = f * f * (3.0 - 2.0 * f);\n\n return mix(a, b, u.x) +\n (c - a)* u.y * (1.0 - u.x) +\n (d - b) * u.x * u.y;\n}\n\n#define OCTAVES 6\nfloat fbm (in vec2 st) {\n // Initial values\n float value = 0.0;\n float amplitude = .5;\n float frequency = 0.;\n //\n // Loop of octaves\n for (int i = 0; i < OCTAVES; i++) {\n value += amplitude * noise(st);\n st *= 2.;\n amplitude *= .5;\n }\n return value;\n}\n";var m=n(1410),f=n.n(m),h=n(412);let g=function(e){return e[e.Vanilla=0]="Vanilla",e[e.Stamp=1]="Stamp",e[e.Airbrush=2]="Airbrush",e}({});function v(e){let{uniforms:t=null,showEditor:n=null}=e;const m=(0,a.useRef)(),f=(0,a.useRef)(),h=(0,a.useRef)();function v(e,t,n){const a=[...t],r=[...t.slice(2)],i=[...n],l=[...n.slice(1)],s=[];let c=0;for(let p=0;p{const e=(1+Math.sqrt(5))/2,n=m.current.clientWidth,a=n*(.5/e),i=4*e,l=i*(.5/e),s=new o.iKG(i/-2,i/2,l/2,l/-2,-1e3,1e3);s.position.z=5;const c=new o.CP7({antialias:!0,alpha:!0,premultipliedAlpha:!1,powerPreference:"high-performance"});function y(){const t=m.current.clientWidth,n=.5*t/e;c.setSize(t,n)}c.setClearColor(new o.Ilk(1,1,1),0),c.setSize(n,a),window.addEventListener("resize",y),m.current.appendChild(c.domElement);const x=new o.xsS,b=new r.o(s,c.domElement);b.enableRotate=!1,b.enableDamping=!1,b.screenSpacePanning=!0,b.addEventListener("change",(()=>{c.render(x,s)})),f.current=()=>c.render(x,s),window.addEventListener("TextureLoaded",f.current);const k=new o.u9r;k.setIndex([0,1,2,2,3,0]);const w=new Function(u.Z),[T,L]=w();v(k,T,L);const A={type:{value:g.Vanilla},color:{value:[0,0,0,1]},footprint:{value:new o.xEZ},stampIntervalRatio:{value:1},noiseFactor:{value:0},rotationFactor:{value:0},gradient:{value:new o.IEO}},C=new o.FIo({uniforms:t||A,vertexShader:d,fragmentShader:p,side:o.ehD,transparent:!0,glslVersion:o.LSk});return h.current=new o.SPe(k,C,T.length-1),h.current.frustumCulled=!1,x.add(h.current),f.current(),()=>{c.dispose(),window.removeEventListener("resize",y),window.removeEventListener("TextureLoaded",f.current)}}),[]);const x=(0,a.useCallback)(((e,t)=>{let n=[],a=[];try{const t=new Function(e);[n,a]=t()}catch(r){return void console.log(r.toString())}function o(e){if(Array.isArray(e)){for(let t=0;t{y(e,"")}})),T&&a.createElement(l.Z,{value:"fragment.glsl"},a.createElement(c.r,{height:b,defaultValue:p,onChange:e=>{y("",e)}})))),a.createElement("div",{ref:m,style:{width:"100%"},onMouseDown:e=>e.preventDefault()}))}let y=new o.xEZ;h.Z.canUseDOM&&(y=(new o.dpR).load(`/${f().projectName}/img/stamp2.png`,(e=>{window.dispatchEvent(new CustomEvent("TextureLoaded"))}),void 0,void 0));let x=new o.xEZ;h.Z.canUseDOM&&(x=(new o.dpR).load(`/${f().projectName}/img/dot.png`,(e=>{window.dispatchEvent(new CustomEvent("TextureLoaded"))}),void 0,void 0));const b={type:{value:g.Stamp},color:{value:[0,0,0,1]},footprint:{value:y},stampIntervalRatio:{value:.4},noiseFactor:{value:1.2},rotationFactor:{value:.75}},k=((e,t)=>{let n=new o.AXT(new o.FM8(0,1),e,t,new o.FM8(1,0));const a=256,r=new Uint8Array(1024),i=n.getPoints(512);for(let o=0;o=n.x&&e<=a.x){let t=(n.y*(a.x-e)+a.y*(e-n.x))/(a.x-n.x);r[4*o]=Math.floor(255*t)}}}const l=new o.IEO(r,a,1);return l.needsUpdate=!0,l})(new o.FM8(.33,1),new o.FM8(.66,0)),w={type:{value:g.Airbrush},color:{value:[0,0,0,1]},gradient:{value:k}},T={type:{value:g.Stamp},color:{value:[0,0,0,.5]},footprint:{value:x},stampIntervalRatio:{value:2},noiseFactor:{value:0},rotationFactor:{value:0}},L={type:{value:g.Stamp},color:{value:[0,0,0,.5]},footprint:{value:x},stampIntervalRatio:{value:1},noiseFactor:{value:0},rotationFactor:{value:0}}},5034:(e,t,n)=>{"use strict";n.d(t,{r:()=>s});var a=n(7462),o=n(7294);const r={comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"[",close:"]"},{open:"{",close:"}"},{open:"(",close:")"},{open:"'",close:"'",notIn:["string","comment"]},{open:'"',close:'"',notIn:["string"]}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}]},i={tokenPostfix:".glsl",defaultToken:"invalid",keywords:["const","uniform","break","continue","do","for","while","if","else","switch","case","in","out","inout","true","false","invariant","discard","return","sampler2D","samplerCube","sampler3D","struct","radians","degrees","sin","cos","tan","asin","acos","atan","pow","sinh","cosh","tanh","asinh","acosh","atanh","exp","log","exp2","log2","sqrt","inversesqrt","abs","sign","floor","ceil","round","roundEven","trunc","fract","mod","modf","min","max","clamp","mix","step","smoothstep","length","distance","dot","cross ","determinant","inverse","normalize","faceforward","reflect","refract","matrixCompMult","outerProduct","transpose","lessThan ","lessThanEqual","greaterThan","greaterThanEqual","equal","notEqual","any","all","not","packUnorm2x16","unpackUnorm2x16","packSnorm2x16","unpackSnorm2x16","packHalf2x16","unpackHalf2x16","dFdx","dFdy","fwidth","textureSize","texture","textureProj","textureLod","textureGrad","texelFetch","texelFetchOffset","textureProjLod","textureLodOffset","textureGradOffset","textureProjLodOffset","textureProjGrad","intBitsToFloat","uintBitsToFloat","floatBitsToInt","floatBitsToUint","isnan","isinf","vec2","vec3","vec4","ivec2","ivec3","ivec4","uvec2","uvec3","uvec4","bvec2","bvec3","bvec4","mat2","mat3","mat2x2","mat2x3","mat2x4","mat3x2","mat3x3","mat3x4","mat4x2","mat4x3","mat4x4","mat4","float","int","uint","void","bool"],operators:["=",">","<","!","~","?",":","==","<=",">=","!=","&&","||","++","--","+","-","*","/","&","|","^","%","<<",">>",">>>","+=","-=","*=","/=","&=","|=","^=","%=","<<=",">>=",">>>="],symbols:/[=>{n.languages.register({id:"glsl"}),n.languages.setMonarchTokensProvider("glsl",i),n.languages.setLanguageConfiguration("glsl",r),"function"==typeof e.onMount&&e.onMount(t,n)}}))}},9279:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});const a="// Generate sinewave geometry \nconst maxRadius = 1/3;\nconst segmentCount = 32;\n\nconst position = [];\nconst radius = [];\n\nconst gr = (1 + Math.sqrt(5)) / 2; // golden ratio\nconst pi = Math.PI;\n\nfor(let i = 0; i <= segmentCount; ++i){\n let a = i / segmentCount\n let x = -pi + (2 * pi * a);\n let y = Math.sin(x) / gr;\n let r = Math.cos(x / 2.0) * maxRadius;\n\n position.push(x, y);\n radius.push(r);\n}\n\nreturn [position, radius];\n"},154:(e,t,n)=>{"use strict";n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>l,default:()=>m,frontMatter:()=>i,metadata:()=>s,toc:()=>u});var a=n(7462),o=(n(7294),n(3905)),r=n(5632);const i={title:"Introduction",sidebar_position:2},l=void 0,s={unversionedId:"Introduction/Introduction",id:"Introduction/Introduction",title:"Introduction",description:"This tutorial series will teach you how to use the modern GPU graphics pipeline to render brush strokes,",source:"@site/docs/Introduction/Introduction.mdx",sourceDirName:"Introduction",slug:"/Introduction/",permalink:"/brush-rendering-tutorial/Introduction/",draft:!1,editUrl:"https://github.com/ShenCiao/brush-rendering-tutorial/tree/main/docs/Introduction/Introduction.mdx",tags:[],version:"current",sidebarPosition:2,frontMatter:{title:"Introduction",sidebar_position:2},sidebar:"tutorialSidebar",previous:{title:"Table of Contents",permalink:"/brush-rendering-tutorial/"},next:{title:"Basics",permalink:"/brush-rendering-tutorial/category/basics"}},c={},u=[{value:"Prerequisites",id:"prerequisites",level:2},{value:"Structure",id:"structure",level:2},{value:"Content",id:"content",level:3},{value:"Live coding",id:"live-coding",level:3},{value:"Citation",id:"citation",level:2}],d={toc:u},p="wrapper";function m(e){let{components:t,...n}=e;return(0,o.kt)(p,(0,a.Z)({},d,n,{components:t,mdxType:"MDXLayout"}),(0,o.kt)("p",null,"This tutorial series will teach you how to use the modern GPU graphics pipeline to render ",(0,o.kt)("strong",{parentName:"p"},"brush strokes"),",\nthose are commonly seen with a paint tool in graphics design software like Photoshop."),(0,o.kt)("div",{className:"row row--no-gutters margin-left--xs"},(0,o.kt)("div",{className:"col col--6"},(0,o.kt)(r.ij,{mdxType:"ArticulatedLine2D"}),(0,o.kt)("center",null,(0,o.kt)("em",null," Vanilla "))),(0,o.kt)("div",{className:"col col--6"},(0,o.kt)(r.ij,{uniforms:r.PQ,mdxType:"ArticulatedLine2D"}),(0,o.kt)("center",null,(0,o.kt)("em",null," Pencil (Stamp) ")))),(0,o.kt)("br",null),(0,o.kt)("admonition",{type:"note"},(0,o.kt)("p",{parentName:"admonition"},"When hovering your mouse on the canvas you can:"),(0,o.kt)("ul",{parentName:"admonition"},(0,o.kt)("li",{parentName:"ul"},(0,o.kt)("strong",{parentName:"li"},"Pan"),": Left-click and drag the mouse."),(0,o.kt)("li",{parentName:"ul"},(0,o.kt)("strong",{parentName:"li"},"Zoom"),": Scroll or drag the mouse wheel."))),(0,o.kt)("p",null,"The contents mainly come from my research work ",(0,o.kt)("a",{parentName:"p",href:"https://github.com/ShenCiao/Ciallo"},"Ciallo: The next generation vector paint program"),".\nSince there will be more research work on GPU brush stroke rendering,\nI will continuously update this tutorial series to teach you related techniques in (potentially) influential research works."),(0,o.kt)("h2",{id:"prerequisites"},"Prerequisites"),(0,o.kt)("p",null,"Decent experience in one of the GPU graphics APIs like OpenGL and D3D is required.\nIf you were relatively new to computer graphics, you should at least have rendered your first 3D scene and practiced instanced rendering.\nIn this tutorial, we will learn techniques to render and stylize curves."),(0,o.kt)("p",null,"Though I create all the demos in the web environment, you don't need to know about WebGL or WebGPU.\nWe will concentrate on high-level techniques rather than the implementation details.\nNo matter which GPU API you are familiar with, utilizing them to render a stroke will be easy after this tutorial series."),(0,o.kt)("h2",{id:"structure"},"Structure"),(0,o.kt)("h3",{id:"content"},"Content"),(0,o.kt)("p",null,"The ",(0,o.kt)("a",{parentName:"p",href:"../category/basics/"},"Basic")," section covers the basics of the rendering and stylization methods.\nArticles in the Basic part are organized in a linear fashion.\nYou may miss something important if skip one of them.\nAfter learning all stuffs in the Basic section, you can select your favorite topics in the TOC to learn.\nI will list extra prerequisites at the very beginning of each article."),(0,o.kt)("h3",{id:"live-coding"},"Live coding"),(0,o.kt)("p",null,"You will find live code editors like the one below. which is inspired by ",(0,o.kt)("a",{parentName:"p",href:"https://thebookofshaders.com/"},(0,o.kt)("em",{parentName:"a"},"The Book of Shader")),".\nThe rendering result is updated in real time after modifying the code.\nTry it out by changing the variable ",(0,o.kt)("inlineCode",{parentName:"p"},"maxRadius")," and ",(0,o.kt)("inlineCode",{parentName:"p"},"segmentCount")," and watching the changes in the canvas below."),(0,o.kt)(r.ij,{showEditor:[!0,!1,!1],mdxType:"ArticulatedLine2D"}),(0,o.kt)("admonition",{title:"code editor",type:"note"},(0,o.kt)("p",{parentName:"admonition"},"If there are bugs for common usages, tell me at the ",(0,o.kt)("a",{parentName:"p",href:"https://github.com/ShenCiao/brush-rendering-tutorial/issues"},"issue")," page.")),(0,o.kt)("p",null,"Feel free to change any other parts of the code as long as it returns the ",(0,o.kt)("inlineCode",{parentName:"p"},"position")," and ",(0,o.kt)("inlineCode",{parentName:"p"},"radius")," array correctly."),(0,o.kt)("p",null,'Only geometry generation code "geometry.js" is demonstrated here.\nYou will find "vertex.glsl" and "fragment.glsl" for vertex and fragment shader code.\nWhether they are hidden or shown will depend on the context.'),(0,o.kt)("h2",{id:"citation"},"Citation"),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre"},"@inproceedings{Ciallo2023,\n author = {Ciao, Shen and Wei, Li-Yi},\n title = {Ciallo: The next-Generation Vector Paint Program},\n year = {2023},\n isbn = {9798400701436},\n publisher = {Association for Computing Machinery},\n address = {New York, NY, USA},\n url = {https://doi.org/10.1145/3587421.3595418},\n doi = {10.1145/3587421.3595418},\n booktitle = {ACM SIGGRAPH 2023 Talks},\n articleno = {67},\n numpages = {2},\n keywords = {Digital painting, stylized stroke, arrangement, vector graphics. coloring, graphics processing unit (GPU)},\n location = {Los Angeles, CA, USA},\n series = {SIGGRAPH '23}\n}\n")),(0,o.kt)("admonition",{title:"Research Tip",type:"note"},(0,o.kt)("p",{parentName:"admonition"},"To demonstrate your research work about brush rendering, select vector drawings have variable radius or pen pressure data.\nRegular vector drawing datasets don't contain them."),(0,o.kt)("ul",{parentName:"admonition"},(0,o.kt)("li",{parentName:"ul"},"Zeyu Wang's work: ",(0,o.kt)("a",{parentName:"li",href:"https://dl.acm.org/doi/10.1145/3450626.3459819"},"Paper")," | ",(0,o.kt)("a",{parentName:"li",href:"https://github.com/zachzeyuwang/tracing-vs-freehand"},"Dataset")),(0,o.kt)("li",{parentName:"ul"},(0,o.kt)("a",{parentName:"li",href:"https://cloud.blender.org/p/gallery/5b642e25bf419c1042056fc6"},"Blender Grease Pencil")),(0,o.kt)("li",{parentName:"ul"},"... Tell me more in the ",(0,o.kt)("a",{parentName:"li",href:"https://github.com/ShenCiao/brush-rendering-tutorial/discussions/1"},"discussion"),"."))))}m.isMDXComponent=!0},3618:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>a});const a={plain:{color:"#F8F8F2",backgroundColor:"#282A36"},styles:[{types:["prolog","constant","builtin"],style:{color:"rgb(189, 147, 249)"}},{types:["inserted","function"],style:{color:"rgb(80, 250, 123)"}},{types:["deleted"],style:{color:"rgb(255, 85, 85)"}},{types:["changed"],style:{color:"rgb(255, 184, 108)"}},{types:["punctuation","symbol"],style:{color:"rgb(248, 248, 242)"}},{types:["string","char","tag","selector"],style:{color:"rgb(255, 121, 198)"}},{types:["keyword","variable"],style:{color:"rgb(189, 147, 249)",fontStyle:"italic"}},{types:["comment"],style:{color:"rgb(98, 114, 164)"}},{types:["attr-name"],style:{color:"rgb(241, 250, 140)"}}]}},7694:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>a});const a={plain:{color:"#393A34",backgroundColor:"#f6f8fa"},styles:[{types:["comment","prolog","doctype","cdata"],style:{color:"#999988",fontStyle:"italic"}},{types:["namespace"],style:{opacity:.7}},{types:["string","attr-value"],style:{color:"#e3116c"}},{types:["punctuation","operator"],style:{color:"#393A34"}},{types:["entity","url","symbol","number","boolean","variable","constant","property","regex","inserted"],style:{color:"#36acaa"}},{types:["atrule","keyword","attr-name","selector"],style:{color:"#00a4db"}},{types:["function","deleted","tag"],style:{color:"#d73a49"}},{types:["function-variable"],style:{color:"#6f42c1"}},{types:["tag","selector","keyword"],style:{color:"#00009f"}}]}}}]); \ No newline at end of file diff --git a/assets/js/runtime~main.4ba509da.js b/assets/js/runtime~main.4ba509da.js new file mode 100644 index 0000000..1a000cb --- /dev/null +++ b/assets/js/runtime~main.4ba509da.js @@ -0,0 +1 @@ +(()=>{"use strict";var e,t,r,o,a,n={},i={};function u(e){var t=i[e];if(void 0!==t)return t.exports;var r=i[e]={id:e,loaded:!1,exports:{}};return n[e].call(r.exports,r,r.exports,u),r.loaded=!0,r.exports}u.m=n,u.c=i,e=[],u.O=(t,r,o,a)=>{if(!r){var n=1/0;for(l=0;l=a)&&Object.keys(u.O).every((e=>u.O[e](r[c])))?r.splice(c--,1):(i=!1,a0&&e[l-1][2]>a;l--)e[l]=e[l-1];e[l]=[r,o,a]},u.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return u.d(t,{a:t}),t},r=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,u.t=function(e,o){if(1&o&&(e=this(e)),8&o)return e;if("object"==typeof e&&e){if(4&o&&e.__esModule)return e;if(16&o&&"function"==typeof e.then)return e}var a=Object.create(null);u.r(a);var n={};t=t||[null,r({}),r([]),r(r)];for(var i=2&o&&e;"object"==typeof i&&!~t.indexOf(i);i=r(i))Object.getOwnPropertyNames(i).forEach((t=>n[t]=()=>e[t]));return n.default=()=>e,u.d(a,n),a},u.d=(e,t)=>{for(var r in t)u.o(t,r)&&!u.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},u.f={},u.e=e=>Promise.all(Object.keys(u.f).reduce(((t,r)=>(u.f[r](e,t),t)),[])),u.u=e=>"assets/js/"+({53:"935f2afb",147:"26251b8b",195:"22948d09",364:"ac092286",370:"ed94b18f",394:"5244698c",514:"1be78505",613:"6ae0415c",736:"d50c89f1",805:"bc662926",817:"14eb3368",918:"17896441",940:"a7b05c2e"}[e]||e)+"."+{5:"32dafc35",53:"6c3369f6",147:"088d1b64",195:"fa9be84a",364:"fea992f7",370:"9dbb1674",394:"83d2a283",514:"27794ed6",613:"07c5054e",736:"e53c93e3",805:"a9adb9e5",817:"1495867d",918:"b90ff644",940:"fd1fd14f",972:"25a3f09a"}[e]+".js",u.miniCssF=e=>{},u.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),u.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),o={},a="brush-stroke-tutorial:",u.l=(e,t,r,n)=>{if(o[e])o[e].push(t);else{var i,c;if(void 0!==r)for(var f=document.getElementsByTagName("script"),l=0;l{i.onerror=i.onload=null,clearTimeout(b);var a=o[e];if(delete o[e],i.parentNode&&i.parentNode.removeChild(i),a&&a.forEach((e=>e(r))),t)return t(r)},b=setTimeout(s.bind(null,void 0,{type:"timeout",target:i}),12e4);i.onerror=s.bind(null,i.onerror),i.onload=s.bind(null,i.onload),c&&document.head.appendChild(i)}},u.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},u.p="/brush-rendering-tutorial/",u.gca=function(e){return e={17896441:"918","935f2afb":"53","26251b8b":"147","22948d09":"195",ac092286:"364",ed94b18f:"370","5244698c":"394","1be78505":"514","6ae0415c":"613",d50c89f1:"736",bc662926:"805","14eb3368":"817",a7b05c2e:"940"}[e]||e,u.p+u.u(e)},(()=>{var e={303:0,532:0};u.f.j=(t,r)=>{var o=u.o(e,t)?e[t]:void 0;if(0!==o)if(o)r.push(o[2]);else if(/^(303|532)$/.test(t))e[t]=0;else{var a=new Promise(((r,a)=>o=e[t]=[r,a]));r.push(o[2]=a);var n=u.p+u.u(t),i=new Error;u.l(n,(r=>{if(u.o(e,t)&&(0!==(o=e[t])&&(e[t]=void 0),o)){var a=r&&("load"===r.type?"missing":r.type),n=r&&r.target&&r.target.src;i.message="Loading chunk "+t+" failed.\n("+a+": "+n+")",i.name="ChunkLoadError",i.type=a,i.request=n,o[1](i)}}),"chunk-"+t,t)}},u.O.j=t=>0===e[t];var t=(t,r)=>{var o,a,n=r[0],i=r[1],c=r[2],f=0;if(n.some((t=>0!==e[t]))){for(o in i)u.o(i,o)&&(u.m[o]=i[o]);if(c)var l=c(u)}for(t&&t(r);f{"use strict";var e,t,r,o,a,n={},i={};function c(e){var t=i[e];if(void 0!==t)return t.exports;var r=i[e]={id:e,loaded:!1,exports:{}};return n[e].call(r.exports,r,r.exports,c),r.loaded=!0,r.exports}c.m=n,c.c=i,e=[],c.O=(t,r,o,a)=>{if(!r){var n=1/0;for(d=0;d=a)&&Object.keys(c.O).every((e=>c.O[e](r[u])))?r.splice(u--,1):(i=!1,a0&&e[d-1][2]>a;d--)e[d]=e[d-1];e[d]=[r,o,a]},c.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return c.d(t,{a:t}),t},r=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,c.t=function(e,o){if(1&o&&(e=this(e)),8&o)return e;if("object"==typeof e&&e){if(4&o&&e.__esModule)return e;if(16&o&&"function"==typeof e.then)return e}var a=Object.create(null);c.r(a);var n={};t=t||[null,r({}),r([]),r(r)];for(var i=2&o&&e;"object"==typeof i&&!~t.indexOf(i);i=r(i))Object.getOwnPropertyNames(i).forEach((t=>n[t]=()=>e[t]));return n.default=()=>e,c.d(a,n),a},c.d=(e,t)=>{for(var r in t)c.o(t,r)&&!c.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},c.f={},c.e=e=>Promise.all(Object.keys(c.f).reduce(((t,r)=>(c.f[r](e,t),t)),[])),c.u=e=>"assets/js/"+({53:"935f2afb",147:"26251b8b",195:"22948d09",364:"ac092286",370:"ed94b18f",394:"5244698c",514:"1be78505",613:"6ae0415c",736:"d50c89f1",805:"bc662926",817:"14eb3368",918:"17896441",940:"a7b05c2e"}[e]||e)+"."+{5:"32dafc35",53:"7ca1a7a0",147:"2343b74d",195:"fa9be84a",364:"ef95a417",370:"9dbb1674",394:"83d2a283",514:"27794ed6",613:"aa8a3689",736:"e53c93e3",805:"a9adb9e5",817:"1495867d",918:"b90ff644",940:"fd1fd14f",972:"25a3f09a"}[e]+".js",c.miniCssF=e=>{},c.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),c.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),o={},a="brush-stroke-tutorial:",c.l=(e,t,r,n)=>{if(o[e])o[e].push(t);else{var i,u;if(void 0!==r)for(var f=document.getElementsByTagName("script"),d=0;d{i.onerror=i.onload=null,clearTimeout(b);var a=o[e];if(delete o[e],i.parentNode&&i.parentNode.removeChild(i),a&&a.forEach((e=>e(r))),t)return t(r)},b=setTimeout(s.bind(null,void 0,{type:"timeout",target:i}),12e4);i.onerror=s.bind(null,i.onerror),i.onload=s.bind(null,i.onload),u&&document.head.appendChild(i)}},c.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},c.p="/brush-rendering-tutorial/",c.gca=function(e){return e={17896441:"918","935f2afb":"53","26251b8b":"147","22948d09":"195",ac092286:"364",ed94b18f:"370","5244698c":"394","1be78505":"514","6ae0415c":"613",d50c89f1:"736",bc662926:"805","14eb3368":"817",a7b05c2e:"940"}[e]||e,c.p+c.u(e)},(()=>{var e={303:0,532:0};c.f.j=(t,r)=>{var o=c.o(e,t)?e[t]:void 0;if(0!==o)if(o)r.push(o[2]);else if(/^(303|532)$/.test(t))e[t]=0;else{var a=new Promise(((r,a)=>o=e[t]=[r,a]));r.push(o[2]=a);var n=c.p+c.u(t),i=new Error;c.l(n,(r=>{if(c.o(e,t)&&(0!==(o=e[t])&&(e[t]=void 0),o)){var a=r&&("load"===r.type?"missing":r.type),n=r&&r.target&&r.target.src;i.message="Loading chunk "+t+" failed.\n("+a+": "+n+")",i.name="ChunkLoadError",i.type=a,i.request=n,o[1](i)}}),"chunk-"+t,t)}},c.O.j=t=>0===e[t];var t=(t,r)=>{var o,a,n=r[0],i=r[1],u=r[2],f=0;if(n.some((t=>0!==e[t]))){for(o in i)c.o(i,o)&&(c.m[o]=i[o]);if(u)var d=u(c)}for(t&&t(r);f Appendix | Brush Rendering Tutorial - + - + \ No newline at end of file diff --git a/category/basics/index.html b/category/basics/index.html index 6990c9a..ba83d92 100644 --- a/category/basics/index.html +++ b/category/basics/index.html @@ -4,13 +4,13 @@ Basics | Brush Rendering Tutorial - + - + \ No newline at end of file diff --git a/index.html b/index.html index 3af6d55..afe114a 100644 --- a/index.html +++ b/index.html @@ -3,19 +3,19 @@ -Table of Contents | Brush Rendering Tutorial - +Table of Contents | Brush Rendering Tutorial +
-
Loading...
Vanilla
Loading...
Pencil

This tutorial series will teach you how to render brush strokes with the modern GPU graphics pipeline.

If you like this series, please star the code repository instead of bookmark this website since the domain might be changed.

Table of Contents

  • Introduction
  • Basics
    • Vanilla
    • Vanilla with variable radius
    • Stamp
    • Stamp with variable radius 1
    • Stamp with variable radius 2
  • An interleave

Future Contents

Airbrush

Loading...

Airbrush is a special type of stamp brush. +

This tutorial series will teach you how to render brush strokes with the modern GPU graphics pipeline.

If you like this series, please star the code repository instead of bookmark this website since the domain might be changed.

Table of Contents

  • Introduction
  • Basics
    • Vanilla
    • Vanilla with variable radius
    • Stamp
    • Stamp with variable radius 1
    • Stamp with variable radius 2
  • An interleave

Future Contents

Airbrush

Loading...

Airbrush is a special type of stamp brush. Here I'm demonstrating a "continuous airbrush", which is mathematically continuous and needs a little bit of calculus to develop. You will learn how to generalize a stamp brush into a continuous form.

Stamp density and "ratio-distance"

Loading...
Adjacent Dots
Loading...
Adjacent with one dot interleaved

You can see dots are adjacent to each other instead of equidistantly distributed. The pattern is achieved by setting the intervals between dots proportional to their radii. You will learn how to freely control stamp density along a stamp stroke. Very important for a serious project.

3D stroke

Learn how to extend the algorithms to 3D space.

I'm integrating it into the Blender Grease Pencil:

- + \ No newline at end of file