add post about decals

master
Andrew Cassidy 4 years ago committed by Andrew Cassidy
parent a7261dd0cc
commit e6a05f8628

@ -1,2 +1,3 @@
source 'https://rubygems.org'
gem 'github-pages'
gem 'rouge'

@ -1,13 +1,14 @@
GEM
remote: https://rubygems.org/
specs:
activesupport (4.2.11.1)
i18n (~> 0.7)
activesupport (6.0.3.2)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
addressable (2.6.0)
public_suffix (>= 2.0.2, < 4.0)
zeitwerk (~> 2.2, >= 2.2.2)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
coffee-script (2.4.1)
coffee-script-source
execjs
@ -15,8 +16,8 @@ GEM
colorator (1.1.0)
commonmarker (0.17.13)
ruby-enum (~> 0.5)
concurrent-ruby (1.1.5)
dnsruby (1.61.2)
concurrent-ruby (1.1.6)
dnsruby (1.61.3)
addressable (~> 2.5)
em-websocket (0.5.1)
eventmachine (>= 0.12.9)
@ -25,33 +26,32 @@ GEM
ffi (>= 1.3.0)
eventmachine (1.2.7)
execjs (2.7.0)
faraday (0.15.4)
faraday (1.0.1)
multipart-post (>= 1.2, < 3)
ffi (1.10.0)
ffi (1.13.1)
forwardable-extended (2.6.0)
gemoji (3.0.1)
github-pages (198)
activesupport (= 4.2.11.1)
github-pages (206)
github-pages-health-check (= 1.16.1)
jekyll (= 3.8.5)
jekyll-avatar (= 0.6.0)
jekyll (= 3.8.7)
jekyll-avatar (= 0.7.0)
jekyll-coffeescript (= 1.1.1)
jekyll-commonmark-ghpages (= 0.1.5)
jekyll-commonmark-ghpages (= 0.1.6)
jekyll-default-layout (= 0.1.4)
jekyll-feed (= 0.11.0)
jekyll-feed (= 0.13.0)
jekyll-gist (= 1.5.0)
jekyll-github-metadata (= 2.12.1)
jekyll-mentions (= 1.4.1)
jekyll-optional-front-matter (= 0.3.0)
jekyll-github-metadata (= 2.13.0)
jekyll-mentions (= 1.5.1)
jekyll-optional-front-matter (= 0.3.2)
jekyll-paginate (= 1.1.0)
jekyll-readme-index (= 0.2.0)
jekyll-redirect-from (= 0.14.0)
jekyll-relative-links (= 0.6.0)
jekyll-remote-theme (= 0.3.1)
jekyll-readme-index (= 0.3.0)
jekyll-redirect-from (= 0.15.0)
jekyll-relative-links (= 0.6.1)
jekyll-remote-theme (= 0.4.1)
jekyll-sass-converter (= 1.5.2)
jekyll-seo-tag (= 2.5.0)
jekyll-sitemap (= 1.2.0)
jekyll-swiss (= 0.4.0)
jekyll-seo-tag (= 2.6.1)
jekyll-sitemap (= 1.4.0)
jekyll-swiss (= 1.0.0)
jekyll-theme-architect (= 0.1.1)
jekyll-theme-cayman (= 0.1.1)
jekyll-theme-dinky (= 0.1.1)
@ -61,19 +61,18 @@ GEM
jekyll-theme-midnight (= 0.1.1)
jekyll-theme-minimal (= 0.1.1)
jekyll-theme-modernist (= 0.1.1)
jekyll-theme-primer (= 0.5.3)
jekyll-theme-primer (= 0.5.4)
jekyll-theme-slate (= 0.1.1)
jekyll-theme-tactile (= 0.1.1)
jekyll-theme-time-machine (= 0.1.1)
jekyll-titles-from-headings (= 0.5.1)
jemoji (= 0.10.2)
jekyll-titles-from-headings (= 0.5.3)
jemoji (= 0.11.1)
kramdown (= 1.17.0)
liquid (= 4.0.0)
listen (= 3.1.5)
liquid (= 4.0.3)
mercenary (~> 0.3)
minima (= 2.5.0)
nokogiri (>= 1.8.5, < 2.0)
rouge (= 2.2.1)
minima (= 2.5.1)
nokogiri (>= 1.10.4, < 2.0)
rouge (= 3.19.0)
terminal-table (~> 1.4)
github-pages-health-check (1.16.1)
addressable (~> 2.3)
@ -81,13 +80,13 @@ GEM
octokit (~> 4.0)
public_suffix (~> 3.0)
typhoeus (~> 1.3)
html-pipeline (2.11.0)
html-pipeline (2.13.0)
activesupport (>= 2)
nokogiri (>= 1.4)
http_parser.rb (0.6.0)
i18n (0.9.5)
concurrent-ruby (~> 1.0)
jekyll (3.8.5)
jekyll (3.8.7)
addressable (~> 2.4)
colorator (~> 1.0)
em-websocket (~> 0.5)
@ -100,49 +99,50 @@ GEM
pathutil (~> 0.9)
rouge (>= 1.7, < 4)
safe_yaml (~> 1.0)
jekyll-avatar (0.6.0)
jekyll (~> 3.0)
jekyll-avatar (0.7.0)
jekyll (>= 3.0, < 5.0)
jekyll-coffeescript (1.1.1)
coffee-script (~> 2.2)
coffee-script-source (~> 1.11.1)
jekyll-commonmark (1.3.1)
commonmarker (~> 0.14)
jekyll (>= 3.7, < 5.0)
jekyll-commonmark-ghpages (0.1.5)
jekyll-commonmark-ghpages (0.1.6)
commonmarker (~> 0.17.6)
jekyll-commonmark (~> 1)
rouge (~> 2)
jekyll-commonmark (~> 1.2)
rouge (>= 2.0, < 4.0)
jekyll-default-layout (0.1.4)
jekyll (~> 3.0)
jekyll-feed (0.11.0)
jekyll (~> 3.3)
jekyll-feed (0.13.0)
jekyll (>= 3.7, < 5.0)
jekyll-gist (1.5.0)
octokit (~> 4.2)
jekyll-github-metadata (2.12.1)
jekyll (~> 3.4)
jekyll-github-metadata (2.13.0)
jekyll (>= 3.4, < 5.0)
octokit (~> 4.0, != 4.4.0)
jekyll-mentions (1.4.1)
jekyll-mentions (1.5.1)
html-pipeline (~> 2.3)
jekyll (~> 3.0)
jekyll-optional-front-matter (0.3.0)
jekyll (~> 3.0)
jekyll (>= 3.7, < 5.0)
jekyll-optional-front-matter (0.3.2)
jekyll (>= 3.0, < 5.0)
jekyll-paginate (1.1.0)
jekyll-readme-index (0.2.0)
jekyll (~> 3.0)
jekyll-redirect-from (0.14.0)
jekyll (~> 3.3)
jekyll-relative-links (0.6.0)
jekyll (~> 3.3)
jekyll-remote-theme (0.3.1)
jekyll (~> 3.5)
rubyzip (>= 1.2.1, < 3.0)
jekyll-readme-index (0.3.0)
jekyll (>= 3.0, < 5.0)
jekyll-redirect-from (0.15.0)
jekyll (>= 3.3, < 5.0)
jekyll-relative-links (0.6.1)
jekyll (>= 3.3, < 5.0)
jekyll-remote-theme (0.4.1)
addressable (~> 2.0)
jekyll (>= 3.5, < 5.0)
rubyzip (>= 1.3.0)
jekyll-sass-converter (1.5.2)
sass (~> 3.4)
jekyll-seo-tag (2.5.0)
jekyll (~> 3.3)
jekyll-sitemap (1.2.0)
jekyll (~> 3.3)
jekyll-swiss (0.4.0)
jekyll-seo-tag (2.6.1)
jekyll (>= 3.3, < 5.0)
jekyll-sitemap (1.4.0)
jekyll (>= 3.7, < 5.0)
jekyll-swiss (1.0.0)
jekyll-theme-architect (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
@ -170,8 +170,8 @@ GEM
jekyll-theme-modernist (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-primer (0.5.3)
jekyll (~> 3.5)
jekyll-theme-primer (0.5.4)
jekyll (> 3.5, < 5.0)
jekyll-github-metadata (~> 2.9)
jekyll-seo-tag (~> 2.0)
jekyll-theme-slate (0.1.1)
@ -183,43 +183,42 @@ GEM
jekyll-theme-time-machine (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-titles-from-headings (0.5.1)
jekyll (~> 3.3)
jekyll-titles-from-headings (0.5.3)
jekyll (>= 3.3, < 5.0)
jekyll-watch (2.2.1)
listen (~> 3.0)
jemoji (0.10.2)
jemoji (0.11.1)
gemoji (~> 3.0)
html-pipeline (~> 2.2)
jekyll (~> 3.0)
jekyll (>= 3.0, < 5.0)
kramdown (1.17.0)
liquid (4.0.0)
listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
ruby_dep (~> 1.2)
liquid (4.0.3)
listen (3.2.1)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
mercenary (0.3.6)
mini_portile2 (2.4.0)
minima (2.5.0)
jekyll (~> 3.5)
minima (2.5.1)
jekyll (>= 3.5, < 5.0)
jekyll-feed (~> 0.9)
jekyll-seo-tag (~> 2.1)
minitest (5.11.3)
multipart-post (2.0.0)
nokogiri (1.10.3)
minitest (5.14.1)
multipart-post (2.1.1)
nokogiri (1.10.9)
mini_portile2 (~> 2.4.0)
octokit (4.14.0)
octokit (4.18.0)
faraday (>= 0.9)
sawyer (~> 0.8.0, >= 0.5.3)
pathutil (0.16.2)
forwardable-extended (~> 2.6)
public_suffix (3.0.3)
rb-fsevent (0.10.3)
rb-inotify (0.10.0)
public_suffix (3.1.1)
rb-fsevent (0.10.4)
rb-inotify (0.10.1)
ffi (~> 1.0)
rouge (2.2.1)
ruby-enum (0.7.2)
rouge (3.19.0)
ruby-enum (0.8.0)
i18n
ruby_dep (1.5.0)
rubyzip (1.2.2)
rubyzip (2.3.0)
safe_yaml (1.0.5)
sass (3.7.4)
sass-listen (~> 4.0.0)
@ -232,17 +231,19 @@ GEM
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
thread_safe (0.3.6)
typhoeus (1.3.1)
typhoeus (1.4.0)
ethon (>= 0.9.0)
tzinfo (1.2.5)
tzinfo (1.2.7)
thread_safe (~> 0.1)
unicode-display_width (1.5.0)
unicode-display_width (1.7.0)
zeitwerk (2.3.0)
PLATFORMS
ruby
DEPENDENCIES
github-pages
rouge
BUNDLED WITH
2.0.1
2.1.4

@ -41,4 +41,5 @@
<link rel="stylesheet" href="/css/fnoots.css"/>
<link rel="stylesheet" href="/css/site.css"/>
<link rel="stylesheet" href="/css/code.css"/>
<link rel="stylesheet" href="/css/sidebar.css"/>

@ -0,0 +1,167 @@
---
title: Forwards-rendered Decals in Unity for KSP
description: Rendering decals with forward rendering
image: capsule.png
tags: KSP conformal-decals gamedev
---
{% include figure-image.html
src="capsule-small.png"
float="left"
caption="NASA logos projected onto a capsule" %}
My most recent project has been [Conformal Decals](https://forum.kerbalspaceprogram.com/index.php?/topic/194802-191-*), a decal mod for Kerbal Space Program where the decals are projected onto the vessel, instead of being just a flat mesh on top of it.
This seemed to be an easy task, since Unity has a projector component that appeared at first glance to do exactly what I wanted. After some experimentation, however, I realized that the projector only works effectively for unlit effects, like a video projector or a shadow. Even after hacking the necessary projection math into a surface shader, information like vertex normals and tangents were missing.
Instead of giving up, I decided to write my own decal projection code. How hard could it be?
## Decal Projection Math
Decal projection math is fairly straightforwards, and works exactly the same as how lighting works in forwards rendering paths. Lighting in a forwards rendering path, at least in Unity, is done by re-rendering the mesh once for every pixel light (after the initial directional light).
Decal projection works in exactly the same way. Each mesh gets rendered an additional time with a new shader and a matrix converting from vertex-space to the local space of the projector. The x and y coordinates in this space become the uv coordinates of the texture, and the z coordinate is the depth. The generation of this matrix is fairly straightforwards with a little matrix multiplication:
{% highlight csharp linenos %}
var projectionMatrix = transform.worldToLocalMatrix * targetRenderer.localToWorldMatrix;
{% endhighlight %}
though since we want the projector to be in the middle of the decal's space, we need to add an additional offset of 0.5. We can also pass in the normal vector (-Z) of the projector so our decal doesnt appear on geometry facing the wrong way (This could also be done in the shader by calculating the vertex normal in projector-space, but that means more work for the shader).
{% highlight csharp linenos %}
var orthoMatrix = Matrix4x4.identity; // orthographic projection matrix
orthoMatrix[0,3] = 0.5f; // offset x coordinate by 0.5
orthoMatrix[1,3] = 0.5f; // offset y coordinate by 0.5
var targetToProjector = transform.worldToLocalMatrix * targetRenderer.localToWorldMatrix;
var projectorToTarget = targetRenderer.worldToLocalMatrix * transform.localToWorldMatrix;
var projectionMatrix = orthoMatrix * targetToProjector;
var decalNormal = projectorToTarget.MultiplyVector(Vector3.back).normalized;
targetMaterial.SetMatrix("_ProjectionMatrix", _projectionMatrix);
targetMaterial.SetVector("_DecalNormal", decalNormal);
{% endhighlight %}
now in the shader you can use this matrix with the vertex position to find its location in projector-space.
{% highlight glsl linenos %}
// in the vertex shader
o.uv_decal = mul(_ProjectionMatrix, v.vertex);
{% endhighlight %}
{% include figure-gallery.html
src="decal-bounds.png"
caption="The decal exists within the local space of the projector" %}
## Writing the Shader
Implementing this in a Unity surface shader works fine. You're able to use the UVs of the mesh and the normal map of the base material to make the decal conform to the normals, or do some math to make the decal fade out with sharp normal changes, to simulate paint wearing at panel seams. The decal receives lighting automatically, and can have its opacity adjusted to fade in and out. A surface shader version of the decal shader is shown below.
{% highlight glsl linenos %}
Shader "ConformalDecals/Paint/Diffuse"
{
Properties
{
_Decal ("Decal", 2D) = "gray" {}
_BumpMap("Base BumpMap", 2D) = "bump" {}
_Opacity("_Opacity", Range(0,1) ) = 1
}
SubShader
{
Tags { "Queue" = "Geometry" }
ZWrite Off
ZTest LEqual
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma surface surf Lambert alpha vertex:vert
#pragma target 4.0
float4x4 _ProjectionMatrix;
float4 _DecalNormal;
sampler2D _Decal;
sampler2D _BumpMap;
float _Opacity;
struct Input
{
float4 uv_decal : TEXCOORD0;
float2 uv_BumpMap : TEXCOORD1;
float4 position : SV_POSITION;
float3 normal : NORMAL;
};
void vert (inout appdata_full v, out Input o) {
o.uv_decal = mul (_ProjectionMatrix, v.vertex);
o.uv_BumpMap = v.texcoord.xy;
o.position = UnityObjectToClipPos(v.vertex);
o.normal = v.normal;
}
void surf (Input IN, inout SurfaceOutput o)
{
fixed4 projUV = UNITY_PROJ_COORD(IN.decal);
// clip fragments outside the projection area
clip(projUV.xyz);
clip(1-projUV.xyz);
// clip backsides
clip(dot(_DecalNormal, IN.normal));
float4 color = tex2D(_Decal, projUV);
o.Albedo = color.rgb;
o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
o.Alpha = _Opacity * color.a;
}
ENDCG
}
}
{% endhighlight %}
## Adding Normal Maps
Things immediately get more complicated when you want to add normal maps to your decals.
Normally when you use normal maps (pun intended), the mesh provides vertex tangent data, which tells the shader which way the normal map gets oriented, and is a vector pointing where the "u" axis of the uv map should be in 3d space. We dont have the benefit of that data for our decal projection, but since our decals are 2 dimensional, this vector is the same everywhere on the mash, and calculating it is fairly easy, just like the normal was.
{% highlight csharp linenos %}
var orthoMatrix = Matrix4x4.identity; // orthographic projection matrix
orthoMatrix[0,3] = 0.5f; // offset x coordinate by 0.5
orthoMatrix[1,3] = 0.5f; // offset y coordinate by 0.5
var targetToProjector = transform.worldToLocalMatrix * targetRenderer.localToWorldMatrix;
var projectorToTarget = targetRenderer.worldToLocalMatrix * transform.localToWorldMatrix;
var projectionMatrix = orthoMatrix * targetToProjector;
var decalNormal = projectorToTarget.MultiplyVector(Vector3.back).normalized;
var decalTangent = projectorToTarget.MultiplyVector(Vector3.right).normalized;
targetMaterial.SetMatrix("_ProjectionMatrix", _projectionMatrix);
targetMaterial.SetVector("_DecalNormal", decalNormal);
targetMaterial.SetVector("_DecalTangent", decalNormal);
{% endhighlight %}
But unfortunately, like a lot of Unity features that are intended to make life easier, the Surface shaders get in the way once you try to do anything interesting. The shader passes tangent data, as well as global vertex normal, binormal, and position, in a 3x4 matrix between vertex and fragment shaders. These values get completely shadowed from surface shaders, and modifying them is impossible. So we need to throw out the surface shader and write our shader from scratch.
I'm not going to go over all the things I did to rewrite the shader as a vertex/fragment shader or this article would be far too long, but I do want to make some notes that would have helped me when I was doing this, and trying to answer the question "how do I make lighting work in a vertex/fragment shader in unity". If you want to see everything I did, the cginc file I wrote is available [here](https://github.com/drewcassidy/KSP-Conformal-Decals/blob/master/Assets/Shaders/DecalsCommon.cginc).
* Surface shaders in Unity generate a vertex/fragment shader when you compile them, which can be seen by selecting the shader and clicking "show generated code". The resulting shader has some common vertex/fragment code that calls the `vert` and `surf` functions values defined in the surface shader, which does things like applying the UV scale/offsets beforehand, and the lighting BRDF afterwards. I decided to recreate this so that as much of the calculation as possible is written in the cginc file, then the shader itself just needs to define `vert` and `surf` functions just like a surface shader.
* Shaders for forwards rendering in Unity have 2 passes: the base pass shades using ambient lights, emissives, and the first directional light in the scene. Then the additive pass gets called for every additional light in the scene.
* Lighting is applied in three steps. First the `UNITY_LIGHT_ATTENUATION` macro calculates the light attenuation value, which also includes shadows and light cookies. Then, the BRDF gets called with the surface output from the `surf` function to calculate the actual color of the pixel. Lastly, in the forward base pass only, vertex and ambient lighting gets added to the color.
## How it All Could Be Improved
The biggest issue with this technique is the number of draw calls. Each mesh that a decal projects on to needs its own draw call, plus draw calls for any pixel lights. If a lot of decals are used, especially large ones which cover multiple meshes, the draw call count could get very big, very quickly. One solution would be to render all the decals on a particular mesh at once, by passing an array of projection matrices and textures into the shader, and looping over them. This would require some tweaks, like moving the projection code into the fragment shader (shaders cant interpolate an array) as well as a change to the code works where each decal is in charge of its own rendering.
Another would be to abandon forward rendering entirely, and use deferred rendering. This was not a solution for this project, since its a mod for a video game and changing the rendering path is not exactly feasable, but would work for any standalone projects. Deferred rendering would also allow for effects like modifying the normal of the underlying geometry.
Adapting the above code to other games should be straightforward, but keep in mind a lot of assumptions were made to simplify things that were only true for KSP, like not needing support for lightmaps.

@ -4,9 +4,10 @@ $subtle: #7A7267;
$margin: darken($background, 8%);
$shadow: rgba(black, 0.4);
$blue: rgb(39, 167, 167);
$orange: rgb(236, 103, 55);
$yellow: rgb(228, 214, 124);
$green: rgb(188, 212, 42);
$blue: rgb(39, 167, 167);
$magenta: rgb(168, 66, 114);
$darken-factor: 15%;

@ -0,0 +1,94 @@
---
---
@import 'util';
@import 'colors';
code {
background-color: darken($background, 2%);
}
// block code
figure.highlight {
width: 100%;
margin-inline-start: 0;
margin-inline-end: 0;
margin-block-start: 0;
margin-block-end: 0;
pre {
margin-top: 0;
margin-bottom: 0;
}
code {
display: block;
overflow-y: scroll;
background-color: darken($background, 2%);
padding: 10px 10px;
margin-left: 10px;
margin-right: 10px;
margin-bottom: 10px;
border-radius: 5px;
}
td.gutter {
color: $subtle;
text-align: right;
padding-right: 0.5ch;
border-right: solid;
border-right-width: 1px;
border-right-color: $subtle
}
td.code {
padding-left: 0.5ch;
}
}
div.highlight {
}
// pre {
// display: inline;
// margin: 0;
// }
.k, .kw, .kd, .kn, .kp, .kr, .kt {
color: $blue
}
.n, .nx{
color: $orange
}
.nb, .np {
color: $blue
}
.nc, .no, .nl, {
color: $orange
}
.nd {
color: $orange;
font-weight: bold;
}
.nf {
color: $green;
}
.l, .ld, .s, .m {
color: $green;
}
.c, .cm, .cp, .c1, .cs {
color: $subtle;
}
.o {
color: $blue;
}

@ -70,7 +70,7 @@ body {
font-size: $body-size;
font-family: $body-font;
color: $body-text;
max-width: 1000px;
max-width: 1312px;
}
@ -167,6 +167,7 @@ footer {
bottom: 20px;
}
flex-grow: 1;
overflow:hidden;
}
hr {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.
Loading…
Cancel
Save