<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[obsidian-vault]]></title><description><![CDATA[Obsidian digital garden]]></description><link>http://github.com/dylang/node-rss</link><image><url>site-lib/media/favicon.png</url><title>obsidian-vault</title><link></link></image><generator>Webpage HTML Export plugin for Obsidian</generator><lastBuildDate>Fri, 12 Jun 2026 20:03:54 GMT</lastBuildDate><atom:link href="site-lib/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Fri, 12 Jun 2026 20:03:41 GMT</pubDate><ttl>60</ttl><dc:creator></dc:creator><item><title><![CDATA[R(n) low discrepancy sequence]]></title><description><![CDATA[Great blog post about R(2) sequence <a rel="noopener nofollow" class="external-link is-unresolved" href="http://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/" target="_self">http://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/</a>. It's essentially just a mul and an add.The post suggest to use the generalized golden ratio as the irrational numbers for R(n)import numpy as np def phi(d): x=2.0000 for i in range(10): x = pow(1+x,1/(d+1)) return x d=2 # Number of dimensions. n=50 # number of required points g = phi(d) alpha = np.zeros(d) for j in range(d): alpha[j] = pow(1/g,j+1) %1 z = np.zeros((n, d)) for i in range(n): z[i] = (seed + alpha*(i+1)) %1 print(z)
However I found that for very high dimensionality, square roots of various primes work better. Thanks to @loicvdbruh for this:// From loicvdbruh: https://www.shadertoy.com/view/NlGXzz. Square roots of primes.
const LDS_MAX_DIMENSIONS: usize = 32;
const LDS_PRIMES: [u32; LDS_MAX_DIMENSIONS] = [ 0x6a09e667u32, 0xbb67ae84u32, 0x3c6ef372u32, 0xa54ff539u32, 0x510e527fu32, 0x9b05688au32, 0x1f83d9abu32, 0x5be0cd18u32, 0xcbbb9d5cu32, 0x629a2929u32, 0x91590159u32, 0x452fecd8u32, 0x67332667u32, 0x8eb44a86u32, 0xdb0c2e0bu32, 0x47b5481du32, 0xae5f9155u32, 0xcf6c85d1u32, 0x2f73477du32, 0x6d1826cau32, 0x8b43d455u32, 0xe360b595u32, 0x1c456002u32, 0x6f196330u32, 0xd94ebeafu32, 0x9cc4a611u32, 0x261dc1f2u32, 0x5815a7bdu32, 0x70b7ed67u32, 0xa1513c68u32, 0x44f93634u32, 0x720dcdfcu32
]; pub fn lds(n: u32, dimension: usize, offset: u32) -&gt; f32 { const INV_U32_MAX_FLOAT: f32 = 1.0 / 4294967296.0; (LDS_PRIMES[dimension].wrapping_mul(n.wrapping_add(offset))) as f32 * INV_U32_MAX_FLOAT }
Bonus: PCG hash function. Sometimes useful when doing low discrepancy stuff:pub fn pcg_hash(input: u32) -&gt; u32 { let state = input * 747796405u32 + 2891336453u32; let word = ((state &gt;&gt; ((state &gt;&gt; 28u32) + 4u32)) ^ state) * 277803737u32; (word &gt;&gt; 22u32) ^ word
}
<br><a href=".?query=tag:math" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#math">#math</a> <a href=".?query=tag:probability" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#probability">#probability</a>]]></description><link>math/r(n)-low-discrepancy-sequence.html</link><guid isPermaLink="false">Math/R(n) low discrepancy sequence.md</guid><pubDate>Fri, 12 Jun 2026 20:01:05 GMT</pubDate></item><item><title><![CDATA[Spherical Harmonics]]></title><description><![CDATA[Spherical Harmonics pop up in certain fields of graphics, as they are useful for encoding spherical functions. In this document I describe various notes and tidbits about them.
Note: Skip to <a data-href="#Fourier basis" href="#Fourier basis" class="internal-link" target="_self" rel="noopener nofollow">Fourier basis</a> section if you already know what a basis function is.
A vector space is (roughly) a set whose elements can be added together or multiplied by scalars (ie. scaled). These 2 operations must satisfy some sensible axioms, such as a associativity and commutativity of addition. Here is a full list of axioms:<br>
<img alt="Pasted image 20231005215702.png" src="attachments/pasted-image-20231005215702.png" target="_self">
Some common vector spaces you may be familiar with are and ie. the space of real-valued 2D and 3D vectors, or what you may just know as colloquially as "2D and 3D vectors". Another example of a vector space is , the space of complex-valued 2D vectors.A basis for a given vector space, is a set vectors with the special property that any element of the vector space can be written in a unique way, as a linear combination of elements of the basis. Here are some examples of bases for : aka. the "standard basis" Note that something like is not a valid basis, because the 2 vectors are parallel, meaning that not every element of the vector space can be written as a linear combination of the 2.We call a basis "orthogonal" if all of the vectors in the basis are mutually orthogonal. We additionally call the basis "orthonormal" if all of the vectors are normalized. Looking at the 3 examples listed above, the first is orthonormal, the second is only orthogonal, and the third is neither orthogonal nor orthonormal.We typically think of elements of vector spaces, vectors, as lists of numbers. However, this is limiting a definition. In fact, if we loosen this definition, we can use functions as our vectors, forming a function space. The addition and scaling operators are defined as such:Where is a scalar, and and are functions.The elements of a basis for a function space is then called a basis function. A simple example of a function space is a the space of single-indeterminate polynomials. One possible basis for this vector space is the monomial basis:This is clearly a valid basis as every polynomial can be written as a linear combination of the elements:Where are constant coefficients for each basis function.Spherical Harmonics are an analogue of the Fourier basis on the surface of a sphere. To understand this, we need to go over a few more concepts.The Fourier series is a neat kind of infinite series which can be used to approximate periodic functions (functions that repeat their values at regular intervals, called the period). Assume we have a periodic function with a period of , defined on the interval . We can such a function to a sum of a constant and an infinite series of cosine and sine functions, known as a Fourier series, like so:Where is a constant scalar, and are constant coefficients. The functions . Form a special kind of basis known as a Fourier basis. Written more explicitly, the Fourier basis is:Note that this is just one of infinitely many possible Fourier bases, since scaling a basis yields another basis. Common among all Fourier bases is the constant term and the alternating cosine and sine terms.
Note: You may have seen different definitions of Fourier series than what I have shown. In fact, Fourier series have several equivalent definitions, including the sine-cosine definition, the exponential definition, and amplitude-phase definition. The other definitions are not relevant to the topic at hand.
One particular kind of Fourier basis is formed by the so-called "Circular Harmonics" (CH). This basis is a scaled version of what we have seen before, given by:These functions form a basis for periodic functions with period , or the circumference of a unit circle (hence the name Circular Harmonics - these functions can be "wrapped" around a unit circle). The denominators in the basis functions aren't of particular importance - they are just normalization terms, there to ensure that we can easily project into the basis (ie. calculate the required coefficients for a linear combination of the basis functions, given a function we want to approximate) using the exact same basis functions as we would use in the final reconstruction (linear combination of basis functions). Importantly, this basis is orthonormal.As mentioned just above, 2 operations of particular importance are projection and reconstruction. They are both fairly straight forward to define. If we want to project a function into the CH basis, we do it like so:Where is the i'th basis function, and is the coefficient to associate with the i'th basis function. Naturally, to reconstruct our approximate version of , we take a simple inner product:A pragmatic way to think about Circular Harmonics are as a kind of compression - a way to store a -periodic function numerically using very little space. The more coefficients we store, the better the approximation will be, but we'll quickly face diminishing returns. In other words, the approximation will already be quite good with only a few coefficients for many kinds of functions, and in particular those of low-frequency. Next I'll show a concrete example of the aforementioned projection and reconstruction operations using some Python code. First, let's define a function which we will project into CH basis and then reconstruct. I've chosen a saw wave:def f(x): x = np.mod(x, 2*np.pi) return x/(2*np.pi)
<br><img alt="Pasted image 20231005232357.png" src="attachments/pasted-image-20231005232357.png" target="_self">Next, let's define a function that gives us the n'th basis function:import numpy as np # n is the index of the basis function, x is the input to the function
def fourier_basis(n, x): multiplier = np.ceil(n / 2) if n == 0: return 1 / np.sqrt(2 * np.pi) elif n % 2 == 1: return np.cos(multiplier * x) / np.sqrt(np.pi) else: return np.sin(multiplier * x) / np.sqrt(np.pi)
<br>Next, we need a way to calculate integrals. I've implemented a simple <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Riemann_sum" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Riemann_sum" target="_self">Riemann sum</a>:# function is the function to integrate
# a and b are the bounds of the integral
# n is the number of discrete steps to take
def riemann(function,a,b,n): sumval = 0 h = (b-a)/n for i in range(0,n-1): current_x = a+i*h sumval = sumval + function(current_x) * h return sumval
With those 2 building blocks, we can define a function that calculates coefficient for the n'th basis function:# f is the function to project into the CH basis
# n is the index of the basis function
# calculates the integral of the n'th basis function multiplied by f
def fourier_project(n, f): return riemann(lambda x: f(x) * fourier_basis(n, x), 0, 2*np.pi, 1000)
We can then define a function that performs reconstruction. We'll use 30 basis functions for this example:# f is the function to project into CH basis, then reconstruct
# x is the is the input to the function
def fourier_reconstruct(f, x): sum = 0 for n in range(30): sum += fourier_project(n, f) * fourier_basis(n, x) return sum
Finally, we can compare the actual and reconstructed functions:from matplotlib import pyplot as plt x = np.linspace(-np.pi*2, np.pi*2, 1000)
fig, ax = plt.subplots()
ax.plot(x, f(x), label='f(x)')
ax.plot(x, fourier_reconstruct(f, x), label='f_reconstructed(x)')
ax.legend()
plt.show()
<br><img alt="Pasted image 20231005232635.png" src="attachments/pasted-image-20231005232635.png" target="_self">
Looks pretty good to me!I mentioned earlier that Spherical Harmonics (SH) are an analogue of the Fourier basis on the surface of a sphere. Phrased differently, they are analogous to the Circular Harmonics, but with an extra dimension of space. Just like the CH basis, the SH basis is orthonormal.Unlike with Circular Harmonics, the basis functions for the SH basis come in levels. Each level contains basis functions. You may have seen this represented as a "pyramid" of basis functions before:<br>
<img alt="Pasted image 20231008034016.png" src="attachments/pasted-image-20231008034016.png" target="_self"><br>
By convention, we number each basis function starting at a negative index, and going to a positive index of equal absolute value. We typically write each basis function as , where is the level, is the index, and are spherical coordinates representing a direction (see <a data-href="Spherical integrals" href="math/light-transport/spherical-integrals.html" class="internal-link" target="_self" rel="noopener nofollow">Spherical integrals</a> for more info).While the SH basis functions are typically written in literature using Spherical Coordinates, we are more often working with 3 dimensional unit vectors in graphics. These 2 representations are equivalent with the following transformation:Using this more convenient representation, the first 3 levels of SH basis functions are given by:A few notable observations to be made here:
The basis function is just a constant!
The basis functions are just a constant multiplied by the , , and components of the input vector respectively.
3 of the basis functions are just a constant multiplied by the components of the input vector.
All of these observations are pertinent to efficiently evaluating Spherical Harmonics in certain use cases within the fields of graphics. Note: What I've shown thus far are known as the "real" (as in real numbers) Spherical Harmonics. Spherical Harmonics have a different expansion which uses complex instead of real numbers, and is a bit more complex. Since we are only usually working with real-valued functions in graphics programming, this expansion is less useful to us, so I've omitted it.
Projection and reconstruction with the SH basis is completely analogous to case of circular harmonics. For projection:<br>Where is the coefficient to associate with basis function . If you are confused about this integral, please see <a data-href="Spherical integrals" href="math/light-transport/spherical-integrals.html" class="internal-link" target="_self" rel="noopener nofollow">Spherical integrals</a>.Reconstruction is again just an inner product:Where is the order (amount of levels) of the basis we are using.<br>
Note: The reason why I use Spherical Coordinates in the above description, despite having just explained that we usually prefer to use unit vectors, is primarily to make the integral more tenable. The space of unit vectors doesn't map intuitively to a pleasant domain of integration. Alternatively, we could write the double integral as a single integral in <a data-href="Solid Angle" href="math/solid-angle.html" class="internal-link" target="_self" rel="noopener nofollow">Solid Angle</a> domain, but integrals in solid angle domain are not easy to calculate directly, so we usually translate to spherical coordinates before calculating anyways.
Spherical Harmonics have a nice property that lets us efficiently calculate the integral of the product of 2 spherical functions. Consider the following such integral:<br>Where denotes the 2-sphere aka. regular 3-dimensional sphere, and is differential solid angle (see <a data-href="Spherical integrals" href="math/light-transport/spherical-integrals.html" class="internal-link" target="_self" rel="noopener nofollow">Spherical integrals</a> and <a data-href="Solid angle" href="math/solid-angle.html" class="internal-link" target="_self" rel="noopener nofollow">Solid angle</a>).The integral looks a bit daunting at first, but if we happen to have both and represented as Spherical Harmonics, we can calculate the result with relative ease using the following identity:Where are SH coefficients for and are SH coefficients for .<br>One can also calculate <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Convolution" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Convolution" target="_self">convolutions</a> of spherical harmonics without too much effort, using the following <a data-tooltip-position="top" aria-label="https://www.cs.jhu.edu/~misha/Spring15/17.pdf" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.cs.jhu.edu/~misha/Spring15/17.pdf" target="_self">convolution theorem</a>: Here, denotes the convolution of the spherical functions and , and is thus the coefficients of the convolution projected into the SH basis. It is important to note that this operation is only well defined when is circularly symmetric around the Z axis. Functions with this property, also known as azimuthal independence, will only have non-zero coefficients for the SH basis functions with index 0, ie. those in the middle column of the SH pyramid shown earlier. This column of basis functions are called the zonal harmonics.Two quantities of particular importance when rendering scenes with global illumination (GI) are radiance and irradiance. I'll briefly summarize what these quantities mean below:Radiance is the quantity typically associated with a single ray of light, while irradiance is the quantity associated with a point on a surface. You can think of radiance as roughly being being the energy carried by a ray of light, and irradiance as the total energy incident on a point on a surface from all directions.We can calculate the irradiance at a given point on a surface by integrating radiance over the hemisphere of directions centered around the normal vector at the point, attenuated by the cosine of the angle between incoming light direction and normal vector:Here denotes the hemisphere centered around normal vector , is the point on the surface. denotes incoming light direction. The term is the dot of the normal and incoming light direction, ie. the cosine of the angle between the 2. The entire is sometimes aptly called the "clamped cosine term" and is sometimes written as .<br> Note: The cosine term is necessary due to how projected area varies with the angle between the surface normal and the normal of the area being projected. Roughly speaking, we want to weigh light coming from shallow angles less, as that light is spread over a larger area. For some context, see the images in <a data-href="Cosine weighted sampling#Why" href="math/light-transport/cosine-weighted-sampling.html#Why" class="internal-link" target="_self" rel="noopener nofollow">Cosine weighted sampling &gt; Why</a>.
One technique for lighting scenes with GI is light probes. These are positions in space with which we associate a quantity of light. When it is time to shade a surface point, we can grab the few nearest probes to said point, interpolate between them, and use them to get an estimate of the indirect light contribution. That begs the question - what exactly do we store in each light probe? Spherical Harmonics turn out to be a great a fit, for a few main reasons:
They are trivial to interpolate between.
They can approximate low-frequency signals (such as is the case for irradiance) using very little data. We'll typically L2 Spherical Harmonics, which amounts to 27 coefficients per probe assuming we are using RGB (as opposed to a different method of sampling the visible spectrum).
They are very cheap to evaluate in realtime. Evaluation boils down to basically a few dot products.
Recall that the radiometric quantity associated with light incident on a surface point is irradiance. It should then be no surprise that the quantity we should project into the SH basis and store in each light probe is also irradiance. To compute the coefficients for each probe, we can use path tracing.Path tracing involves shooting a bunch of light rays from a point, bouncing them around the scene, and attenuating their contribution whenever they hit a surface. Note however that the radiometric quantity associated with a ray of light is radiance, not irradiance. I'll ignore this mismatch in desired quantities for now, and first explain how to project radiance into the L2 SH basis. Below is some pseudo-C#-code to do this. Note that I'm not going to bother with color - we'll just be using monochromatic lighting for simplicity.// Given a probe position, returns the SH coefficients for radiance
// incoming from all directions to that position, using monte carlo
// integration.
float[] ProjectRadianceIntoSH(Vector3 probePosition)
{ float[] radianceSH = new float[9]; for (int i = 0; i &lt; TotalSamplesPerProbe; i++) { // Trace a light path going in a random direction to get radiance // associated with the direction Vector3 rayDirection = GetRandomDirection(); float radiance = TracePath(probePosition, rayDirection); for (int n = 0; n &lt; 9; n++) { // SHBasis(n, d) evaluates the n'th SH basis function // given a direction vector d. radianceSH[n] += radiance * SHBasis(n, rayDirection); } } // Monte carlo normalization float reciprocalSampleCount = 1.0f / TotalSamplesPerProbe; float reciprocalUniformSphereDensity = 4.0f * Math.PI; return radianceSH * reciprocalSampleCount * reciprocalUniformSphereDensity;
}
For convenience, I've used a flat index for indexing SH basis functions, rather than an explicit level and index-in-level . Index 0 thus means basis function , index 1 is , index 2 is etc. Using L2 SH means we have to store 9 coefficients for the 9 basis functions. This pseudocode is essentially just implementing the SH projection operator described earlier, with radiance in a given direction, as the function to project.<br>Now we are faced the issue I outlined earlier: If we want to actually shade a surface with this, we need irradiance rather than radiance, which means that we essentially need to calculate the integral described in <a data-href="#A brief primer on radiometry" href="math/light-transport/spherical-harmonics.html#A_brief_primer_on_radiometry_0" class="internal-link" target="_self" rel="noopener nofollow">A brief primer on radiometry</a>:<br>Ramamoorthi and Hanrahan describe how to do just this in their paper <a data-tooltip-position="top" aria-label="https://cseweb.ucsd.edu/~ravir/papers/invlamb/josa.pdf" rel="noopener nofollow" class="external-link is-unresolved" href="https://cseweb.ucsd.edu/~ravir/papers/invlamb/josa.pdf" target="_self">On the relationship between radiance and irradiance: determining the illumination from images of a convex Lambertian object</a>. I'm not going to go over all the details of their derivations, but focus on the conclusions in broad strokes - you can read the paper for more info. Ramamoorthi defines an operator for the clamped cosine term called :
Where: is the angle between the surface normal and the incident light direction. is the coefficient associated with the l'th SH level, of the clamped cosine term projected into the SH basis. is the SH basis function at level , index 0.
It's important to note here that we are only indexing the SH coefficients of projected into the SH basis using the level , and omitting the index in said level. The clamped cosine term is only dependent on the polar angle , and not the azimuthal angle . This should make intuitive sense - the total amount of light reflected at a point on a lambertian surface is only dependent on how shallow the angle of incidence is. Due to this property, we can discard any coefficient that isn't associated with a zonal harmonic (remember, these are basis functions with index 0) - and we end up with only as many coefficients for the projection of as we have levels of SH.Ramamoorthi then goes on to derive an analytical expression for the coefficients :Let's now revisit the irradiance integral with these definitions. We'll start by rewriting it a tiny bit:Note that we have dropped the surface position from irradiance function . This is because we assume the illumination is distant, which means changes in position have negligible effect.<br>Given the nice property I described in the section <a data-href="Spherical Harmonics#Integral of product" href="math/light-transport/spherical-harmonics.html#Integral_of_product_0" class="internal-link" target="_self" rel="noopener nofollow">Spherical Harmonics &gt; Integral of product</a>, one may now be tempted calculate irradiance integral like so:However, this would be incorrect. The reason is a mismatch in coordinate systems. Incoming radiance, is defined with respect to global coordinates, while the clamped cosine term is with a local coordinates ie. the coordinate system where the surface normal at the point we are shading is pointing straight upwards. We need to introduce a rotation term in order to account for this. I'll omit some details here, as this term is quite complicated to calculate, and just name it somewhat opaquely . With this term, we can now write irradiance as:The effect that this term has when multiplied onto the summand is to transform the input to the radiance function from local coordinate space, which is the domain of our irradiance integral, into global coordinate space, which is the domain of the radiance function, i.e.:Ramamoorthi shows that the rotation term can be rewritten to:Thus the expression for irradiance becomes:<br>In the follow up paper <a data-tooltip-position="top" aria-label="https://cseweb.ucsd.edu/~ravir/papers/envmap/envmap.pdf" rel="noopener nofollow" class="external-link is-unresolved" href="https://cseweb.ucsd.edu/~ravir/papers/envmap/envmap.pdf" target="_self">An Efficient Representation for Irradiance Environment Maps</a>, Ramamoorthi further simplifies this expression by defining a new set of coefficients :Which lets us finally express irradiance as:Next, we rewrite irradiance in terms of SH coefficients, using what we know from the sections on projection:It then becomes evident from the last 2 equations that:Phrased in plain English, if we have radiance in SH, given by coefficients , and we want irradiance in SH, given by coefficients , we need only multiply the radiance coefficients by a factor which varies only per SH level.<br>
Note: You may notice that the above expression looks quite similar to the expression in the <a data-href="Spherical Harmonics#Convolution" href="math/light-transport/spherical-harmonics.html#Convolution_0" class="internal-link" target="_self" rel="noopener nofollow">Spherical Harmonics &gt; Convolution</a> section. This is because the transformation from radiance to irradiance just that - a convolution of radiance with the clamped cosine term.
Since we already have an analytical expression for , and differs from only by a constant factor per SH level, we can derive an analytical expression for too, which is exactly what Ramamoorthi does next:The term for the first 3 levels of SH (which is what we typically care about for light probes), is:We can put this all together to write some pseudocode that convolves our SH coefficients for radiance incoming from a given direction, to irradiance given the normal vector of a surface point:// Given 9 SH coefficients (L2 SH) encoding directional radiance,
// return 9 SH coefficients encoding directional irradiance
float[] ConvolveRadianceToIrradianceSH(float[] radianceSH)
{ float[] irradianceSH = new float[9]; // L0 float aHat0 = 3.14159; irradianceSH[0] = aHat0 * radianceSH[0]; // L1 float aHat1 = 2.09439; irradianceSH[1] = aHat1 * radianceSH[1]; irradianceSH[2] = aHat1 * radianceSH[2]; irradianceSH[3] = aHat1 * radianceSH[3]; // L2 float aHat2 = 0.78539; irradianceSH[4] = aHat2 * radianceSH[4]; irradianceSH[5] = aHat2 * radianceSH[5]; irradianceSH[6] = aHat2 * radianceSH[6]; irradianceSH[7] = aHat2 * radianceSH[7]; irradianceSH[8] = aHat2 * radianceSH[8]; return irradianceSH;
}
As well as a function to shade a surface given a surface normal, surface albedo and a set of SH coefficients corresponding to the a light probe:float ShadeSH(float[] irradianceSH, float surfaceAlbedo, Vector3 surfaceNormal)
{ float outputColor = 0; for (int n = 0; n &lt; 9; n++) { outputColor += irradianceSH[n] * SHBasis(n, surfaceNormal); } return outputColor * surfaceAlbedo;
}
Note that I am still using monochromatic lighting for simplicity, so the surface albedo is just a single scalar.With this, we can finally write pseudocode to shade a point using a light probe:// First, we "bake" a probe at a given position:
Vector3 probePosition = ...;
float[] radianceSH = ProjectRadianceIntoSH(probePosition);
float[] irradianceSH = ConvolveRadianceToIrradiance(radianceSH); // Next, we use it to shade a point with a given normal:
Vector3 surfaceNormal = ...;
float surfaceAlbedo = ...;
float colorAtSurface = ShadeSH(irradianceSH, surfaceAlbedo, surfaceNormal);
In reality, instead of using a single probe, we would bake multiple probes, yielding several sets of SH coefficients, and then interpolate the few nearest ones to get a set of interpolated SH coefficients for irradiance. This is just a dumbed down example.fC0 * fLight[0] - fC3 * fLight[6]
- fC1 * fLight[3] * x
- fC1 * fLight[1] * y
+ fC1 * fLight[2] * z
+ fC2 * fLight[4] * xy
- fC2 * fLight[5] * yz
+ 3.0 * fC3 * fLight[6] * z^2
- fC2 * fLight[7] * zx
+ fC4 * fLight[8] * (x^2 - y^2)
(1/(2*sqrt(pi))) * f_0 - (sqrt(3)/(3*sqrt(pi))) * f_3 * x
- (sqrt(3)/(3*sqrt(pi))) * f_1 * y
+ (sqrt(3)/(3*sqrt(pi))) * f_2 * z + (sqrt(15)/(8*sqrt(pi))) * f_4 * xy
- (sqrt(15)/(8*sqrt(pi))) * f_5 * yz
- (sqrt(5)/(16*sqrt(pi))) * f_6
+ 3.0 * (sqrt(5)/(16*sqrt(pi))) * f_6 * z^2
- (sqrt(15)/(8*sqrt(pi))) * f_7 * zx
+ (sqrt(5)/(16*sqrt(pi))*0.5) * f_8 * (x^2 - y^2) <br><a href=".?query=tag:math" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#math">#math</a> <a href=".?query=tag:global-illumination" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#global-illumination">#global-illumination</a> <a href=".?query=tag:light-transport" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#light-transport">#light-transport</a> <a href=".?query=tag:path-tracing" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#path-tracing">#path-tracing</a> ]]></description><link>math/light-transport/spherical-harmonics.html</link><guid isPermaLink="false">Math/Light Transport/Spherical Harmonics.md</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Spherical integrals]]></title><description><![CDATA[For basic info on how to calculate integrals, see <a data-href="Integration primer" href="math/integration-primer.html" class="internal-link" target="_self" rel="noopener nofollow">Integration primer</a>. Just as we can integrate over a number line, we should be able to integrate over the surface of a unit sphere. To do so, we can use spherical coordinates.<br><img alt="SphericalCoordinates.png" src="attachments/sphericalcoordinates.png" target="_self">A naive approach would be a double integral like so:We know that the surface area of a sphere is , so for a unit sphere, . # Let's try integrating the constant over the unit sphere using this naive approach. This should give us the surface area of the sphere.That is definitely not ! It turns out we are missing a correction factor for the curvature of a sphere. When we integrate over a sphere, our differential is an infinitely small quadrilateral surface patch on the sphere surface.<br><img alt="SphericalIntegration.png" src="attachments/sphericalintegration.png" target="_self">Notice how the top and bottom edge of the differential surface patch are different lengths! We need to account for the shape of the differential, otherwise we are just integrating over a perfectly rectangular plane! The height of the quadrilateral is , but the width is . Notice how the sine term makes the width small when theta is close to one of the poles.We can revisit our naive method, and add the correction factor of :And now we get the correct surface area of the unit sphere, , so this method of spherical integration works.We can turn the spherical integral into a hemispherical one by changing range of integration for to :<br><a data-href="The rendering equation" href="math/light-transport/the-rendering-equation.html" class="internal-link" target="_self" rel="noopener nofollow">The rendering equation</a> contains a hemispherical integral:
Where denotes a unit hemisphere of directions. Why does this look different from what we've seen, and what exactly is ?<br>The formulation of the rendering equation shown earlier looks different because it is integrating over <a data-href="Solid Angle" href="math/solid-angle.html" class="internal-link" target="_self" rel="noopener nofollow">Solid Angle</a> domain. The relationship between differential solid angle and differential spherical coordinates is:Thus:A final note on spherical integrals: Why use this alternate integration domain and not just spherical coordinates? <br>Essentially, this is just because it makes the math much nicer. There is nothing stopping us from writing the rendering equation out with a parameterization using spherical coordinates. You can think of as roughly representing a ray direction. See <a data-href="Solid angle" href="math/solid-angle.html" class="internal-link" target="_self" rel="noopener nofollow">Solid angle</a> for more info on solid angles.<br><a href=".?query=tag:light-transport" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#light-transport">#light-transport</a> <a href=".?query=tag:math" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#math">#math</a> ]]></description><link>math/light-transport/spherical-integrals.html</link><guid isPermaLink="false">Math/Light Transport/Spherical integrals.md</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="attachments/sphericalcoordinates.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;attachments/sphericalcoordinates.png&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[The rendering equation]]></title><description><![CDATA[ - position in space - outgoing direction of light (towards the eye) - incoming direction of light (towards the light) - surface normal vector at - hemisphere of unit directions oriented around - light leaving in direction - light emitted at in direction - proportion of light reflected from to (BRDF) - light incoming at from direction - Cosine term (N dot L) - Outgoing light from the given point - Emitted light from the given point - The hemisphere of directions oriented around the normal - Ratio of incoming and outgoing light (BRDF) - Light incoming to the given point - Cosine term (N dot L) - more on this later
Can we calculate the rendering equation analytically? No, because the incoming light from a given direction depends on the scene we are rendering! There exists no general solution to the rendering equation, so what can we do - we use <a data-href="Monte carlo methods" href="math/monte-carlo-methods.html" class="internal-link" target="_self" rel="noopener nofollow">Monte carlo methods</a>!<br><a href=".?query=tag:light-transport" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#light-transport">#light-transport</a> <a href=".?query=tag:global-illumination" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#global-illumination">#global-illumination</a> ]]></description><link>math/light-transport/the-rendering-equation.html</link><guid isPermaLink="false">Math/Light Transport/The rendering equation.md</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate></item><item><title><![CDATA[Why divide albedo by π]]></title><description><![CDATA[Let's try calculating total reflected outgoing energy without it:
We can pull out and as they are both constant:
Now, note that (See <a data-href="Spherical integrals" href="math/light-transport/spherical-integrals.html" class="internal-link" target="_self" rel="noopener nofollow">Spherical integrals</a>):
So we get:Oh no! This does not satisfy energy conservation:If we divide albedo by :
We reflect exactly incoming incoming energy attenuated by albedo, and energy conservation is satisfied.<br>See also <a rel="noopener nofollow" class="external-link is-unresolved" href="https://seblagarde.wordpress.com/2012/01/08/pi-or-not-to-pi-in-game-lighting-equation/" target="_self">https://seblagarde.wordpress.com/2012/01/08/pi-or-not-to-pi-in-game-lighting-equation/</a><br><a href=".?query=tag:math" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#math">#math</a> <a href=".?query=tag:light-transport" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#light-transport">#light-transport</a> <a href=".?query=tag:pbr" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#pbr">#pbr</a>]]></description><link>math/light-transport/why-divide-albedo-by-π.html</link><guid isPermaLink="false">Math/Light Transport/Why divide albedo by π.md</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate></item><item><title><![CDATA[Monte carlo methods]]></title><description><![CDATA[Let's say we want to calculate a simple integral of the form:But we are unable do anything else with than evaluate it, so we can't get the antiderivative. If we can't solve it analytically, maybe we can do it numerically?We could use a riemann sum, but this won't work well for high dimensional integrals, such as the rendering equation, due to the curse of dimensionality - there is just too much space to cover in the integration domain.Instead, we can use monte carlo methods, which are stochastic methods relying on random sampling of the domain.The basic idea of monte carlo methods is to sample the domain randomly many times, and average the results.For our purposes, we are interested in a specific kind of monte carlo method typically called monte carlo integration, which is used for estimating definite integrals like ours.In probability theory, we have a theorem called the Law Of The Unconcious Statistician (LOTUS):Note that we only need the probability density on to calculate the integral, not the probability density on . From this law, we can <a data-tooltip-position="top" aria-label="https://scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/monte-carlo-methods-mathematical-foundations/expected-value-of-the-function-of-a-random-variable.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/monte-carlo-methods-mathematical-foundations/expected-value-of-the-function-of-a-random-variable.html" target="_self">derive the estimator for monte carlo integration</a>.The cornerstone of monte carlo integration is the monte carlo estimator. For our simple placeholder integral, it can be written as so: denotes the amount of random samples we have taken is a random variable is the probability density function of is our integrand from earlier. Intuition for division of pdf
Inverse transform method
Importance sampling
Derive estimator for monte carlo integration
<br><a href=".?query=tag:math" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#math">#math</a> <a href=".?query=tag:monte-carlo" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#monte-carlo">#monte-carlo</a> <a href=".?query=tag:probability" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#probability">#probability</a>]]></description><link>math/monte-carlo-methods.html</link><guid isPermaLink="false">Math/Monte carlo methods.md</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate></item><item><title><![CDATA[Sampling discrete distribution in O(1)]]></title><description><![CDATA[
Make bins for each discrete outcome, with the bin containing the probability of that element
Calculate the average probability of all outcomes
For each bin with a probability over the mean, redistribute the excess probability to bins with probability less than the mean It is always possible to do this such that each bin contains at most 2 outcomes
Can be done via "robin-hood" method, take from the bin with most probability, give to the bin with least Each bin now contains 2 outcomes at most, each with their own probability
<img alt="Pasted image 20230917172903.png" src="attachments/pasted-image-20230917172903.png" target="_self"><br>
<img alt="Pasted image 20230917172908.png" src="attachments/pasted-image-20230917172908.png" target="_self"> Generate 2 uniform random numbers in [0; 1]
Use the first number to select which bin to look into
Use the second number to select which of the 2 (at most) outcomes in the bin to choose
This is also known as the "Alias Method" or "Squaring off the histogram"<br>Based on <a rel="noopener nofollow" class="external-link is-unresolved" href="https://stats.stackexchange.com/questions/67911/how-to-sample-from-a-discrete-distribution/68041#68041" target="_self">https://stats.stackexchange.com/questions/67911/how-to-sample-from-a-discrete-distribution/68041#68041</a><br>Example implementation <a rel="noopener nofollow" class="external-link is-unresolved" href="https://github.com/pema99/rust-path-tracer/blob/master/src/light_pick.rs#L24" target="_self">https://github.com/pema99/rust-path-tracer/blob/master/src/light_pick.rs#L24</a><br><a href=".?query=tag:math" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#math">#math</a> <a href=".?query=tag:probability" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#probability">#probability</a>]]></description><link>math/sampling-discrete-distribution-in-o(1).html</link><guid isPermaLink="false">Math/Sampling discrete distribution in O(1).md</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Solid angle]]></title><description><![CDATA[Solid angles are mathematical quantity that shows up fairly commonly in light transport, for example in the typical formulation of <a data-href="The rendering equation" href="math/light-transport/the-rendering-equation.html" class="internal-link" target="_self" rel="noopener nofollow">The rendering equation</a>. They are a measure of how much field of view is covered by an object from the point of view of a static observer.Solid angles are typically measured in a unit called steradians. One steradian is equal to one unit of area on a unit sphere (sphere with radius=1). Thus, an object that fully covers the observers view from all direction would be represented with a solid angle of steradians, as the surface area of the unit sphere is .The formula for calculating solid angles is:<br> is portion of surface area on a sphere centered at the observer, which is covered by a considered object. is the radius of aforementioned sphere. Note how the definition of solid angle naturally accounts for the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Inverse-square_law" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Inverse-square_law" target="_self">inverse square law</a>. As the distance to the observer () grows linearly, the perceived portion of the observers field of view being covered decreases by a factor of the distance squared (). Note: This isn't very important, but interesting nonetheless: Since both the numerator and denominator in the formula for solid angle have the same unit, length squared, solid angles are what you call a "dimensionless" unit, and steradians are a dimensionless quantity. Concretely, the definition steradian, cancels out simplifies to .
Solid angles are typically illustrated as a cone section of a sphere:<br>
<img alt="Pasted image 20231008194242.png" src="attachments/pasted-image-20231008194242.png" target="_self">
However, it is important to note that solid angles have no inherent "shape", as they are only a measure of a portion of a sphere, which is inherently an amorphous quantity.<br>As mentioned in the notes on <a data-href="Spherical integrals" href="math/light-transport/spherical-integrals.html" class="internal-link" target="_self" rel="noopener nofollow">Spherical integrals</a>, the relationship between differential solid angle and differential spherical coordinates is:Which is useful for converting between spherical integrals of solid angle and spherical coordinate domain. Differential solid angles can be visualized as the section of a unit sphere subtended by a surface patch of the unit sphere with infinitesimal area:<br>
<img alt="SphericalIntegration.png" src="attachments/sphericalintegration.png" target="_self">
This quantity is typically associated with rays of lights. For example, if we integrate over a sphere, written , in solid angle domain:We can imagine dividing the sphere into infinitely many infinitesimal surface patches, each associated with a ray. Therefore, when reading integrals in solid angle domain, such as the one above, it can be helpful to think of the variable being integrated over () as meaning "a ray direction", and the differential solid angle () as meaning "the infinitely small portion of the the total sphere which any given ray accounts for".<br>It should come as no surprise that numerical computation of spherical integrals like the one above are often done by shooting a bunch of rays in random directions from an observers point of view, and "averaging" their contribution using <a data-href="Monte carlo methods" href="math/monte-carlo-methods.html" class="internal-link" target="_self" rel="noopener nofollow">Monte carlo methods</a>. <br>See <a data-href="Spherical integrals" href="math/light-transport/spherical-integrals.html" class="internal-link" target="_self" rel="noopener nofollow">Spherical integrals</a> for more info on integrating over the sphere and hemisphere.<br><a href=".?query=tag:math" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#math">#math</a> <a href=".?query=tag:light-transport" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#light-transport">#light-transport</a> <a href=".?query=tag:physics" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#physics">#physics</a> ]]></description><link>math/solid-angle.html</link><guid isPermaLink="false">Math/Solid angle.md</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Anti-aliasing for 2D sdf]]></title><description><![CDATA[float lerpstep(float a, float b, float x)
{ return saturate((x - a)/(b - a));
} void addElement(inout float3 existing, float3 elementColor, float elementDist)
{ const float pixelDiagonal = sqrt(2.0) / 2.0; float distDerivativeLength = sqrt(pow(ddx(elementDist), 2) + pow(ddy(elementDist), 2)); existing = lerp(elementColor, existing, lerpstep(-pixelDiagonal, pixelDiagonal, elementDist/distDerivativeLength)); #endif } Calculate magnitude of screen-space gradient of distance
Convert distance to screen-space distance by dividing by magnitude
Lerp between existing and new color, using lerpstep or smoothstep with a neighborhood of ie. half the pixel diagonal
<a href=".?query=tag:shader-snippets" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#shader-snippets">#shader-snippets</a> <a href=".?query=tag:shaders" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#shaders">#shaders</a> ]]></description><link>shaders/anti-aliasing-for-2d-sdf.html</link><guid isPermaLink="false">Shaders/Anti-aliasing for 2D sdf.md</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate></item><item><title><![CDATA[Matrix inversion]]></title><description><![CDATA[float4x4 inverse(float4x4 mat)
{ float4x4 M=transpose(mat); float m01xy=M[0].x*M[1].y-M[0].y*M[1].x; float m01xz=M[0].x*M[1].z-M[0].z*M[1].x; float m01xw=M[0].x*M[1].w-M[0].w*M[1].x; float m01yz=M[0].y*M[1].z-M[0].z*M[1].y; float m01yw=M[0].y*M[1].w-M[0].w*M[1].y; float m01zw=M[0].z*M[1].w-M[0].w*M[1].z; float m23xy=M[2].x*M[3].y-M[2].y*M[3].x; float m23xz=M[2].x*M[3].z-M[2].z*M[3].x; float m23xw=M[2].x*M[3].w-M[2].w*M[3].x; float m23yz=M[2].y*M[3].z-M[2].z*M[3].y; float m23yw=M[2].y*M[3].w-M[2].w*M[3].y; float m23zw=M[2].z*M[3].w-M[2].w*M[3].z; float4 adjM0,adjM1,adjM2,adjM3; adjM0.x=+dot(M[1].yzw,float3(m23zw,-m23yw,m23yz)); adjM0.y=-dot(M[0].yzw,float3(m23zw,-m23yw,m23yz)); adjM0.z=+dot(M[3].yzw,float3(m01zw,-m01yw,m01yz)); adjM0.w=-dot(M[2].yzw,float3(m01zw,-m01yw,m01yz)); adjM1.x=-dot(M[1].xzw,float3(m23zw,-m23xw,m23xz)); adjM1.y=+dot(M[0].xzw,float3(m23zw,-m23xw,m23xz)); adjM1.z=-dot(M[3].xzw,float3(m01zw,-m01xw,m01xz)); adjM1.w=+dot(M[2].xzw,float3(m01zw,-m01xw,m01xz)); adjM2.x=+dot(M[1].xyw,float3(m23yw,-m23xw,m23xy)); adjM2.y=-dot(M[0].xyw,float3(m23yw,-m23xw,m23xy)); adjM2.z=+dot(M[3].xyw,float3(m01yw,-m01xw,m01xy)); adjM2.w=-dot(M[2].xyw,float3(m01yw,-m01xw,m01xy)); adjM3.x=-dot(M[1].xyz,float3(m23yz,-m23xz,m23xy)); adjM3.y=+dot(M[0].xyz,float3(m23yz,-m23xz,m23xy)); adjM3.z=-dot(M[3].xyz,float3(m01yz,-m01xz,m01xy)); adjM3.w=+dot(M[2].xyz,float3(m01yz,-m01xz,m01xy)); float invDet=rcp(dot(M[0].xyzw,float4(adjM0.x,adjM1.x,adjM2.x,adjM3.x))); return transpose(float4x4(adjM0*invDet,adjM1*invDet,adjM2*invDet,adjM3*invDet));
}
<a href=".?query=tag:shaders" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#shaders">#shaders</a> <a href=".?query=tag:shader-snippets" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#shader-snippets">#shader-snippets</a> <a href=".?query=tag:math" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#math">#math</a> ]]></description><link>shaders/matrix-inversion.html</link><guid isPermaLink="false">Shaders/Matrix inversion.md</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate></item><item><title><![CDATA[SH to Unity shader coefficients]]></title><description><![CDATA[Here is the correspondence between SphericalHarmonicsL2 and the properties fed to shaders (unity_SHAr...unity_SHC):// outCoeffs must be size 7
private void SHToShaderCoefficients(ref SphericalHarmonicsL2 sh, ref Vector4[] outCoeffs)
{ // outCoeffs will have this order: // [0] = unity_SHAr // [1] = unity_SHAg // [2] = unity_SHAb // [3] = unity_SHBr // [4] = unity_SHBg // [5] = unity_SHBb // [6] = unity_SHC for (int i = 0; i &lt; 3; i++) { outCoeffs[i] = new Vector4( sh[i, 3], sh[i, 1], sh[i, 2], sh[i, 0] - sh[i, 6] ); outCoeffs[i + 3] = new Vector4( sh[i, 4], sh[i, 5], sh[i, 6] * 3.0f, sh[i, 7] ); } outCoeffs[6] = new Vector4( sh[0, 8], sh[1, 8], sh[2, 8], 1.0f );
}
<a href=".?query=tag:shaders" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#shaders">#shaders</a> <a href=".?query=tag:unity" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#unity">#unity</a>]]></description><link>shaders/sh-to-unity-shader-coefficients.html</link><guid isPermaLink="false">Shaders/SH to Unity shader coefficients.md</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate></item><item><title><![CDATA[VRChat proposals]]></title><description><![CDATA[Graphics.SetRandomWriteTarget:
<a rel="noopener nofollow" class="external-link is-unresolved" href="https://docs.google.com/document/d/1jycYstnZapfx7ArOT9xJ8OgW4nxZ8VdCBoTksqsm7wo/edit?usp=drivesdk" target="_self">https://docs.google.com/document/d/1jycYstnZapfx7ArOT9xJ8OgW4nxZ8VdCBoTksqsm7wo/edit?usp=drivesdk</a>Various proposals:<br>
<a rel="noopener nofollow" class="external-link is-unresolved" href="https://docs.google.com/document/d/1jT6q3Qdi22a1YRToLywsQrxeC3zV-EguMCWqyPZayv8/edit?usp=drivesdk" target="_self">https://docs.google.com/document/d/1jT6q3Qdi22a1YRToLywsQrxeC3zV-EguMCWqyPZayv8/edit?usp=drivesdk</a><br><a href=".?query=tag:vrchat" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#vrchat">#vrchat</a> <a href=".?query=tag:shaders" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#shaders">#shaders</a> ]]></description><link>shaders/vrchat-proposals.html</link><guid isPermaLink="false">Shaders/VRChat proposals.md</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate></item><item><title><![CDATA[World position from depth]]></title><description><![CDATA[v2f vert (float4 vertex : POSITION, float2 uv : TEXCOORD0)
{ v2f o; o.vertex = float4(float2(1,-1)*(uv*2-1),1,1); o.clipPos = o.vertex; o.inverseVP = inverse(UNITY_MATRIX_VP); return o;
} float4 frag (v2f i) : SV_Target
{ float4 clipPos = i.clipPos / i.clipPos.w; clipPos.z = tex2Dproj(_CameraDepthTexture, ComputeScreenPos(clipPos)); float4 homWorldPos = mul(i.inverseVP, clipPos); float3 wpos = homWorldPos.xyz / homWorldPos.w; return float4(wpos, 1.0f);
}
Just use inverse VP matrix. Need the perspective divide because clip space is homogenous (scaling doesn't affect).<a rel="noopener nofollow" class="external-link is-unresolved" href="https://gist.github.com/pema99/b13a76508bba3e8b70caaaea920ec1c3" target="_self">https://gist.github.com/pema99/b13a76508bba3e8b70caaaea920ec1c3</a><br><a href=".?query=tag:shaders" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#shaders">#shaders</a> <a href=".?query=tag:shader-snippets" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#shader-snippets">#shader-snippets</a> <a href=".?query=tag:math" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#math">#math</a> <a href=".?query=tag:unity" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#unity">#unity</a> ]]></description><link>shaders/world-position-from-depth.html</link><guid isPermaLink="false">Shaders/World position from depth.md</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate></item><item><title><![CDATA[Cosine weighted sampling]]></title><description><![CDATA[For lambertian surfaces, one way to sample them in path tracing is to simply use a uniform distribution of unit directions oriented around the surface normal at the shading point. However, we can do better than this.The uniform distribution doesn't account for the fact that the projected area of a beam of light grows as the angle of incidence becomes shallower.
<img alt="Pasted image 20230917182004.png" src="attachments/pasted-image-20230917182004.png" target="_self"><br>
<img alt="Pasted image 20230917182012.png" src="attachments/pasted-image-20230917182012.png" target="_self"><br>
This effect is described by the cosine term in <a data-href="The rendering equation" href="math/light-transport/the-rendering-equation.html" class="internal-link" target="_self" rel="noopener nofollow">The rendering equation</a>. Looking at this, we can intuit that shallow angles are in some sense "less important" than narrow ones, because they contribute more to the final value of the integral. Thus, this is a perfect place to apply importance sampling.<br>Said in more a more boring way: Path tracing is an application of monte carlo integration (see <a data-href="Monte carlo methods" href="math/monte-carlo-methods.html" class="internal-link" target="_self" rel="noopener nofollow">Monte carlo methods</a>) to estimate the rendering equation. Since the integrand of the rendering equation contains a cosine term, we should importance sample based on that term - matching the "shape" of the integrand with the "shape" of the sampling distribution is a wise choice in general.To do this, we first need to generate samples from our sampling distribution, the cosine weighted distribution:pub fn cosine_sample_hemisphere(r1: f32, r2: f32) -&gt; Vec3 { let theta = r1.sqrt().acos(); let phi = 2.0 * core::f32::consts::PI * r2; Vec3::new( theta.sin() * phi.cos(), theta.cos(), theta.sin() * phi.sin(), )
}
Remember to then transform the sample into the coordinate space oriented about the shading normal!The distribution is shown below. Clearly more samples towards the center of the hemisphere.<br>
<img alt="Pasted image 20230917182832.png" src="attachments/pasted-image-20230917182832.png" target="_self"><br>Once we have our cosine distributed samples, we need to account for the change in sampling distribution by adjusting our PDF. We know that the PDF should be proportional to the cosine term of the rendering equation - that was the whole point. If the PDF is correct, it should integrate to 1 over the hemisphere. Let's check (see <a data-href="Spherical integrals" href="math/light-transport/spherical-integrals.html" class="internal-link" target="_self" rel="noopener nofollow">Spherical integrals</a> for how to calculate this):That didn't quite work - we need to add a correction term of :So the PDF to use is . Using this distribution causes a noticeable reduction in noise for lambertian surfaces:<br>
<img alt="Pasted image 20230917184315.png" src="attachments/pasted-image-20230917184315.png" target="_self">Something interesting happens to the estimator for the rendering equation when we use cosine weighted sampling. Let's write out the rendering equation for our setup. Recall that the lambertian BRDF is Note: I'm omitting the emission term because it isn't relevant
Next, we write the estimator for monte carlo integration with cosine weighted sampling:Now lets simplify, this a bit:So the whole rendering equation estimator boils down to:Which means we can simply sum over incoming radiance without having to take PDFs into account at all - it happens implicitly, so we don't actually need the PDF for cosine weighted sampling in practice.A secret trick graphics developers don't want you to know: If you want to generate cosine distributed directions around a given normal, you can do something much cheaper than the code shown earlier:sample_direction = normalize(surface_normal + random_unit_direction);
Don't ask me why this works - I got no clue.<br><a rel="noopener nofollow" class="external-link is-unresolved" href="https://ciechanow.ski/lights-and-shadows/" target="_self">https://ciechanow.ski/lights-and-shadows/</a><br><a rel="noopener nofollow" class="external-link is-unresolved" href="https://www.rorydriscoll.com/2009/01/07/better-sampling/" target="_self">https://www.rorydriscoll.com/2009/01/07/better-sampling/</a><br><a rel="noopener nofollow" class="external-link is-unresolved" href="https://ameye.dev/notes/sampling-the-hemisphere/" target="_self">https://ameye.dev/notes/sampling-the-hemisphere/</a><br><a href=".?query=tag:math" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#math">#math</a> <a href=".?query=tag:light-transport" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#light-transport">#light-transport</a> <a href=".?query=tag:path-tracing" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#path-tracing">#path-tracing</a> <a href=".?query=tag:monte-carlo" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#monte-carlo">#monte-carlo</a> <a href=".?query=tag:probability" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#probability">#probability</a> <a href=".?query=tag:global-illumination" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#global-illumination">#global-illumination</a> <a href=".?query=tag:pbr" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#pbr">#pbr</a> ]]></description><link>math/light-transport/cosine-weighted-sampling.html</link><guid isPermaLink="false">Math/Light Transport/Cosine weighted sampling.md</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Direct light sampling PDF]]></title><description><![CDATA[When we estimate the rendering equation by monte carlo integration, we typically integrate over the <a data-href="Solid Angle" href="math/solid-angle.html" class="internal-link" target="_self" rel="noopener nofollow">Solid Angle</a> domain, but with direct light sampling, we use the (surface) area domain of the light sources, and need to convert between the 2.An integral for direct lighting can be written as so:
Where crucially denotes only the part of the hemisphere - the part which the visible lights project onto. This is similar to the rendering equation, except we know that the incoming ray is coming from a light source.We can instead integrate over area domain like this:
Where is the distance to the light source, is the surface normal of the light source, and denotes the domain of light source surface area. is thus projected differential area, centered around the point on the light source which we hit.
This uses the fact that:
Which can be rewritten to:
The intuition behind this is fairly straight forward. As we move the light source further from the shading point, by a distance of , the projected area grows by a factor of . This is the inverse square law, and the reason for the term.
As we tilt the light source away from the shading point, the projected area also grows. The factor with which it grows is determined by the cosine of the angle between the surface normal on the light source and the negated incoming light direction. As the angle grows, the cosine shrinks. Since the area should grow and not shrink when this happens, we have the term. Since both<br>
vectors are normalized, that dot product is exactly the cosine of the angle. See <a rel="noopener nofollow" class="external-link is-unresolved" href="https://arxiv.org/abs/1205.4447" target="_self">https://arxiv.org/abs/1205.4447</a> for more details.We can write out an estimator for our integral over area domain, assuming uniform sampling over the surface of the light source:
Where is the surface area of the light source.
We can further simplify to:
Note now that this last part:
Is precisely the reciprocal of the formula you see written out in code below. The reason I use the reciprocal is because I am dividing rather than multiplying this weighting term. It's an implementation detail.Note what this tell us about direct light sampling - to evaluate direct lighting, for a given bounce, we choose a direction towards a light source, and multiply the incoming radiance (light source emission) by the BRDF, by the regular cosine term (often folded into the BRDF), and by the weighting factor described just above - there are 2 cosine at terms at play!This explanation doesn't include visibility checks or weight for multiple different sources, though, so let me briefly describe. When we don't pass a visibility check (ie. the chosen light point is occluded), we simply don't add the contribution, since the probability of hitting that point is 0. When we have multiple light sources, we simply pick one at random and divide the contribution by the probability of picking the given light source. This is just splitting the estimator into multiple addends.Code:let cos_theta = light_normal.dot(-light_direction);
if cos_theta &lt;= 0.0 { return 0.0;
}
return light_distance.powi(2) / (light_area * cos_theta);
<br><a href=".?query=tag:path-tracing" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#path-tracing">#path-tracing</a> <a href=".?query=tag:monte-carlo" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#monte-carlo">#monte-carlo</a> <a href=".?query=tag:math" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#math">#math</a> <a href=".?query=tag:light-transport" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#light-transport">#light-transport</a> <a href=".?query=tag:global-illumination" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#global-illumination">#global-illumination</a> <a href=".?query=tag:probability" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#probability">#probability</a> ]]></description><link>math/light-transport/direct-light-sampling-pdf.html</link><guid isPermaLink="false">Math/Light Transport/Direct light sampling PDF.md</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate></item><item><title><![CDATA[Integration domains and sampling for NDF]]></title><description><![CDATA[
Note: This page is still quite rough / unfinished.
NDF (normal distribution function) are typically given in terms of microfacet area. To convert to macrosurface area, we need to multiple by . This is because . To get into spherical coordinates, we need to multiple by since .<img alt="Pasted image 20241108005122.png" src="attachments/pasted-image-20241108005122.png" target="_self">So before creating sampling routines for an NDF, first multiply by
, the jacobian determinant of the transformation.Quick reference: To get a sampling routine given the PDF in spherical coordinate domain, first integrate out to get the marginal density . Then get conditional density .Then use inverse transform sampling. For each function calculate the CDF by integration:
Finally set the CDF equal to a random variable and solve for the integration variable:
Links:<br>
<a rel="noopener nofollow" class="external-link is-unresolved" href="https://www.reedbeta.com/blog/hows-the-ndf-really-defined/" target="_self">https://www.reedbeta.com/blog/hows-the-ndf-really-defined/</a><br>
<a rel="noopener nofollow" class="external-link is-unresolved" href="https://agraphicsguynotes.com/posts/sample_microfacet_brdf/" target="_self">https://agraphicsguynotes.com/posts/sample_microfacet_brdf/</a><br>
<a rel="noopener nofollow" class="external-link is-unresolved" href="https://www.tobias-franke.eu/log/2014/03/30/notes_on_importance_sampling.html" target="_self">https://www.tobias-franke.eu/log/2014/03/30/notes_on_importance_sampling.html</a><br>
<a rel="noopener nofollow" class="external-link is-unresolved" href="https://jcgt.org/published/0007/04/01/" target="_self">https://jcgt.org/published/0007/04/01/</a>]]></description><link>math/light-transport/integration-domains-and-sampling-for-ndf.html</link><guid isPermaLink="false">Math/Light Transport/Integration domains and sampling for NDF.md</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[N dot L from rendering equation]]></title><description><![CDATA[<a data-href="The rendering equation" href="math/light-transport/the-rendering-equation.html" class="internal-link" target="_self" rel="noopener nofollow">The rendering equation</a> for lambertian BRDF is (see <a data-href="Spherical integrals" href="math/light-transport/spherical-integrals.html" class="internal-link" target="_self" rel="noopener nofollow">Spherical integrals</a> for info on how to solve):
Assume single directional light with direction (a, b) in spherical coordinates:
<br>Where is a delta distribution with value in the direction of the light and everywhere else. This is written as (see <a data-href="Dirac delta for unit sphere" href="math/dirac-delta-for-unit-sphere.html" class="internal-link" target="_self" rel="noopener nofollow">Dirac delta for unit sphere</a>):
Now we can solve the integral:
Note now that is precisely "N dot L" as you typically know it, so this matches typical realtime shading.Note, I used Mathematica to solve the last integral. Wolfram isn't good at delta distributions.FullSimplify[ Integrate[ Integrate[(DiracDelta[t - a, p - b])/Sin[a] Sin [t], {t, 0, pi/2}], {p, 0, 2 pi}], Assumptions -&gt; {0 &lt; a &lt; pi/2, 0 &lt; b &lt; 2 pi}] = 1
<br><a href=".?query=tag:math" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#math">#math</a> <a href=".?query=tag:light-transport" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#light-transport">#light-transport</a> <a href=".?query=tag:pbr" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#pbr">#pbr</a> ]]></description><link>math/light-transport/n-dot-l-from-rendering-equation.html</link><guid isPermaLink="false">Math/Light Transport/N dot L from rendering equation.md</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate></item><item><title><![CDATA[Perfect reflection and refraction PDF]]></title><description><![CDATA[For the case of a perfectly reflective and refractive glass material, the BSDF is a weighted sum of 2 delta distributions, where the weight is for reflection and for refraction.In a path tracer you typically want to only continue one path when you sample a BSDF consisting of multiple parts. You can sample such a BSDF by randomly choosing between reflection and refraction, effectively splitting the integral into 2 addends, and dividing the samples contribution by the probability of picking it to eliminate the added bias when doing monte carlo.I was confused why I my implementation which did not counteract the bias was giving seemingly unbiased results, but it turns out I was sort of accidentally importance sampling, and some terms cancel out. For both reflection and refraction your estimator essentially looks like where is incoming radiance, is fresnel or (1-fresnel) depending on reflection or refraction, and is the probability of picking the sampled direction. The way I was picking between reflection and refraction was by generating a random number [0;1] and picking reflection if it was lower than the fresnel value. This makes , so the estimator simplifies to which explains why not doing any pdf divison was unbiased.See also <a data-href="Ratio for diffuse and specular bounce" href="math/light-transport/ratio-for-diffuse-and-specular-bounce.html" class="internal-link" target="_self" rel="noopener nofollow">Ratio for diffuse and specular bounce</a><br><a href=".?query=tag:math" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#math">#math</a> <a href=".?query=tag:path-tracing" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#path-tracing">#path-tracing</a> <a href=".?query=tag:global-illumination" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#global-illumination">#global-illumination</a> <a href=".?query=tag:light-transport" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#light-transport">#light-transport</a> <a href=".?query=tag:probability" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#probability">#probability</a> ]]></description><link>math/light-transport/perfect-reflection-and-refraction-pdf.html</link><guid isPermaLink="false">Math/Light Transport/Perfect reflection and refraction PDF.md</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate></item><item><title><![CDATA[Polarized light]]></title><description><![CDATA[
Disclaimer: I'm not a physicist. This is probably all wrong. Don't @ me.
Light is electromagnetic radiation. Electromagnetic waves consist of 2 perpendicular waves in the electric and magnetic fields. By shifting the phase of these 2 waves, we get different polarization states.
<img alt="Pasted image 20230923211452.png" src="attachments/pasted-image-20230923211452.png" target="_self">
When the waves are perfectly out of phase, we get circularly polarized light, and they are perfectly in phase, we get linearly polarized light. TipMost natural light is "unpolarized" light. This essentially means a mixture of incoherent linearly and circularly polarized light. Added together, the light as a whole has no polarization.Polarization is directly related to the quantum mechanical "spin" of photons. Photon can have 1 of 2 spin-states, which we'll call left and right spin. Waves of photons with left or right spin correspond to left- or right-hand circularly polarized light. Linearly polarized light consists of photons in superposition of the 2 spin states. The opposing circular rotations add together resulting in oscillation in a single direction.<br>
<img alt="eTkln5rPR6.gif" src="attachments/etkln5rpr6.gif" target="_self">
The image shows 2 circularly polarized waves with opposing handedness adding together to form linear polarization. Note how we can change the angle of linear polarization by shifting the phase of the 2 circularly polarized waves.<br>
<img alt="gficFwsUQ5.gif" src="attachments/gficfwsuq5.gif" target="_self">Using polarizing filters, we can change the polarization of light waves passing through them. They can be used to convert unpolarized light to linearly polarized light.<br>
<img alt="Pasted image 20230923214156.png" src="attachments/pasted-image-20230923214156.png" target="_self">
When linearly polarized light passes through a polarizing filter, only some of it is let through. How much is let through depends on the difference between the angle of polarization and the angle of the polarizing filter. If the angles are the same, all the light is let through. If they are exactly perpendicular, none of the light is let through.<br>
<img alt="Pasted image 20230923214516.png" src="attachments/pasted-image-20230923214516.png" target="_self"><br>
Polarizing filters can be viewed as quantum mechanical measurement devices, and can thus be used to prove the infamous <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Bell%27s_theorem" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Bell%27s_theorem" target="_self">bell inequality</a>. By inserting a diagonal polarizing filter between 2 perpendicular polarizing filters, we somehow get more light passing through.<br>
<img alt="Pasted image 20230923214729.png" src="attachments/pasted-image-20230923214729.png" target="_self">One mathematical object for describing the state of polarized light is the stokes vector. It is a 4D vector written as:<br><img alt="Pasted image 20230923215710.png" src="attachments/pasted-image-20230923215710.png" target="_self">
The I parameter describes the intensity of the light. It is equivalent to radiance. The Q parameter distinguishes horizontal and linear polarization with a sign. The U parameter distinguishes diagonal polarization at +45 or -45 degrees, again using the sign. The V parameter distinguishes right and left circular polarization using the sign.<br>
<img alt="Pasted image 20230923220012.png" src="attachments/pasted-image-20230923220012.png" target="_self">
Of course, this only makes sense given a reference frame (coordinate system). We typically say +Z is along the direction of the light by convention. That leaves choosing a direction for X and Y, the 2 of which form the reference frame.Mueller matrices are matrices that transform stokes vectors, altering their polarization state. Each column of a Mueller matrix is a stokes vector. Mueller matrices are only valid to apply if the reference frame of the stokes vector and Mueller matrix are identical. This can be achieved by applying a rotation before multiplying the Mueller matrix. The rotation is itself represented by a matrix.<br>
<img alt="Pasted image 20230923220434.png" src="attachments/pasted-image-20230923220434.png" target="_self">When implementing polarized light into a path tracer, the state of each ray can be represented by a combination of stokes vectors, rather than a combination of scalars. In a typical, non-spectral, path tracer, each ray carries a 3D dimensional vector with values corresponding to the intensity of red, green and blue light respectively. When adding polarized light, we may instead have a stokes vector for each channel, so scalars for each ray in total. Each ray must also carry its reference frame, such that we can properly rotate Mueller matrices before multiplying them.Materials that affect polarization state are then naturally represented by Mueller matrices, also with each their own reference frame. For example, a horizontal polarizing filter may have the Mueller matrix:<br>
<img alt="Pasted image 20230923221034.png" src="attachments/pasted-image-20230923221034.png" target="_self">
Fresnel equations
Mueller matrices for fresnel
<br><a rel="noopener nofollow" class="external-link is-unresolved" href="https://mitsuba.readthedocs.io/en/stable/src/key_topics/polarization.html" target="_self">https://mitsuba.readthedocs.io/en/stable/src/key_topics/polarization.html</a><br><a href=".?query=tag:math" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#math">#math</a> <a href=".?query=tag:physics" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#physics">#physics</a> <a href=".?query=tag:light-transport" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#light-transport">#light-transport</a> ]]></description><link>math/light-transport/polarized-light.html</link><guid isPermaLink="false">Math/Light Transport/Polarized light.md</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Ratio for diffuse and specular bounce]]></title><description><![CDATA[In a typical path tracer with a PBR shading model, you'll want to distinguish between diffuse and specular bounces. This not only allows combining different models for diffuse and specular reflection, but also lets you sample one kind of bounce differently from the other. Typically we want to importance sample specular reflection, otherwise it will take forever to converge.A typical path tracer will not branch paths, however. Every ray will follow exactly 1 path from start to finish. This means you need to make the choice of whether to sample diffuse or specular reflection at each bounce. Since we are doing monte carlo integration, the choice must be random.When I was writing my last path tracer, I first used the simple ratio 0.5 + 0.5 * metallic for choosing specular and diffuse. This works ok-ish (any ratio is fine so long as you account for it in the PDF for the sample), but isn't great and takes long time to converge.The ratio between reflected and refracted light is governed by the fresnel equations. In the context of a dielectric PBR material, refracted light is roughly equivalent to diffuse reflection. Explained in pseudocode:fr = fresnel(cos_theta, 1.0, 1.5); // assuming 1.5 IOR
spec_contrib = lerp(fr, 1.0, metallic);
diffuse_contrib = lerp(1.0-fr, 0.0, metallic); // neglect absorbption
The reason we just use 1.0 instead of fresnel for conductive (fully metallic) materials, is because metals do not diffuse light at all.When I implemented this, it caused the image to converge faster, but introduces nasty fireflies:
<img alt="Pasted image 20230917175503.png" src="attachments/pasted-image-20230917175503.png" target="_self">The reason for this is that essentially all dielectrics (anything with IOR greater than 1.0) will have some specular contribution. For example, with an air-dielectric interface, with the dielectric having metallic=0, roughness=1, and an IOR=1.5, around 4% of the photons will be reflected specularly. Since this number is so low, our rays won't sample specular paths very often, which leads to high frequency noise - in other words, fireflies.Jakub Boksansky and Adam Marrs propose a fix for this in RT gems 2 chapter 14, which is stupid simple but works well in practice - simple clamp the specular ratio to [0.1; 0.9] when it isn't 1 or 0.<br>
<img alt="Pasted image 20230917180234.png" src="attachments/pasted-image-20230917180234.png" target="_self">Code to implement this:let approx_fresnel = util::fresnel_schlick_scalar( 1.0, DIELECTRIC_IOR, normal.dot(view_direction).max(0.0)); let mut specular_weight = util::lerp(approx_fresnel, 1.0, self.metallic); if specular_weight != 0.0 &amp;&amp; specular_weight != 1.0 { specular_weight = specular_weight.clamp( self.specular_weight_clamp.x, self.specular_weight_clamp.y);
}
Remember to divide the contribution by specular_weight or 1-specular_weight to remain unbiased!<br><a rel="noopener nofollow" class="external-link is-unresolved" href="https://seblagarde.wordpress.com/2013/04/29/memo-on-fresnel-equations/" target="_self">https://seblagarde.wordpress.com/2013/04/29/memo-on-fresnel-equations/</a><br><a href=".?query=tag:math" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#math">#math</a> <a href=".?query=tag:pbr" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#pbr">#pbr</a> <a href=".?query=tag:light-transport" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#light-transport">#light-transport</a> <a href=".?query=tag:path-tracing" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#path-tracing">#path-tracing</a> <a href=".?query=tag:global-illumination" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#global-illumination">#global-illumination</a> ]]></description><link>math/light-transport/ratio-for-diffuse-and-specular-bounce.html</link><guid isPermaLink="false">Math/Light Transport/Ratio for diffuse and specular bounce.md</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Dirac delta for unit sphere]]></title><description><![CDATA[Relevant <a data-tooltip-position="top" aria-label="https://math.stackexchange.com/questions/1231609/delta-function-in-spherical-coordinates-does-my-professor-have-a-mistake" rel="noopener nofollow" class="external-link is-unresolved" href="https://math.stackexchange.com/questions/1231609/delta-function-in-spherical-coordinates-does-my-professor-have-a-mistake" target="_self">stackoverflow question</a>Dirac delta is a function given by:We typically use it to pick specific values out of an integral:We sometimes use shorthand and write something like this:Where is "the delta function which is 0 everywhere but ".Dirac delta function unit sphere:
Where are the desired where the function should be . The reason for the extra term is because of conversion to spherical coordinates.<br>The Dirac delta must integrate to 1 over the domain. Let's try the naïve approach without the sine (see <a data-href="Spherical integrals" href="math/light-transport/spherical-integrals.html" class="internal-link" target="_self" rel="noopener nofollow">Spherical integrals</a> for why the term is there):Now we add the sine term, which cancels out the other one:<br><a href=".?query=tag:math" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#math">#math</a> ]]></description><link>math/dirac-delta-for-unit-sphere.html</link><guid isPermaLink="false">Math/Dirac delta for unit sphere.md</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate></item><item><title><![CDATA[Hindley-Milner]]></title><description><![CDATA[A Hindley-Milner (HM) type system is essentially an extension of typed lambda calculus with parametric polymorphism. This polymorphism is usually implicit and can importantly only be used at the 'top-level'. It has some extremely nice properties such as the type of any term being inferable from its use, and a fairly simple, almost built-in, method for inferring said types.HM deals with 2 kinds of type: Monotypes (aka. simple types) and polytypes (aka. type schemes).Monotypes are simple, non-polymorphic types such as , , and so on. For simplicity, I'll write concrete monotypes with uppercase first letter, and type variables with lowercase.Polytypes, or type schemes, are universally quantified monotypes, such as , which is concretely the type of a generic list, or , which represents the type of the identity function. Polytypes are, as the name implies, polymorphic - the in those types can be literally any type.We can convert between monotypes and polytypes using instantiation and generalization.Instantiation takes a polytype and returns a monotype by removing the quantifier and replacing all usages of it with some monotype. For example, could be instantiated to either or .Generalization takes a monotype and returns a polytype by quantifying over all free type variables in the monotype. For example, would generalize to .Importantly, the types and are different. The former represents some concrete type (think or ) which we don't yet know, so must refer to with a variable. The latter represents literally any type, since it is polymorphic.HM only allows universal quantification of types at the "topmost" layer. IE, any polytype must have the form , with no nesting of the quantifiers. This, as far as I understand, makes the type system decidable, as opposed to the type system dubbed 'System F' which does not have this restriction.The meat of HM can be specified concisely with a small set of inference rules. These look scary, but are fairly straight forward once you understand how to read them. Every inference rule has a set of premises and a conclusion, which are put on the top and bottom of a horizontal bar, like so:Which reads as "if I know P, and I know Q, then I know P and Q". The rule has 2 premises and a single conclusion.When dealing with actual type-theoretic inference, we often carry around an environment, usually written as . The environment contains all the types of terms we have already inferred. Really, it is just a set of a terms and their corresponding types. The main operation we do on the environment is to make statements like $ \Gamma \vdash x : \tau $ which means "given the information in the environment, we know is of type ". This is called a typing judgment.The '' operator, called the turnstile, can be thought of a statement stating that a proof exists. Concretely, $ \Gamma \vdash a $ means that given , the environment, we can prove , whatever that might be.Putting the previous knowledge to use, let's look at the inference rules for HM, one by one.The first rule describes how to assign types to usages of variables. It is written as:In english: "If the environment actually contains the information that is of type , then the environment proves that is of type .", which frankly is stating the obvious.Interpretation: The environment keeps track of all the variables we have inferred the type of so far.The next rules tells us how to type usages of functions in applications (function calls):In english: "If the environment proves that is a function from type to , and that is a expression of type , then the environment proves that the application is of type ". This should be very reminiscent of modus-ponens for those familiar with propositional logic.Interpretation: Giving the correct type of input to a function should result in the correct type of output.This rule tells us how to type function abstractions (declarations of functions, concretely as lambdas):In english: "If the environment and the assumption that is an expression of type prove that is an expression of type , then the environment proves that the lambda has the type of a function from to , written as .Interpretation: A lambda expression is a function taking an input of some initially unknown type, returning a value of the type we infer body to, and the body may make use of the input.This rule implies something about how we should infer the type of lambda expression. First, we make up a new, fresh type variable for the input parameter. Then we add info to the environment stating that the input parameter has that new type, and then we infer the type of the lambda body in that altered environment.This rule tells us how to type let bindings of the form let x = e1 in e2 where x is an identifier and e1 and e2 are both expressions.In english: "If the environment proves that is an expression of type , and the environment with the assumption that is an expression of type proves that is an expression of type , then the environment proves that the let binding is of type ."Interpretation: Let bindings bind a name to a value in a following expression, and evaluate to a value of the type we infer that following expression to be. The following expression can naturally make use of the bound name.As for lambda expressions, this implies that we need to alter the environment before inferring the type for the following expression.This rule usually isn't included in the minimal presentation of HM, but I include it since it is useful:In english: "If the environment proves that the condition expression of an if-then-else is of type , and the environment proves that the expressions in both branches are of type , then the environment proves that the expression is of type ."Interpretation: The condition expression in an if-then-else must be a boolean, and the expression in both conditional branches must match it in type. The entire construct evaluate to said type. What I have described so far is not the full set of inference rules. We are missing the rule for instantiation and generalization. I find these easier to explain and understand informally, so I have omitted them. Refer to the previous section on the topic for more info.When actually performing inference, unification is a process that often comes up. Unification is a process that takes as input 2 types, and returns a substitution that when applied to either type, will make the 2 types equal. We use unification whenever we want to constrain 2 inferred types to be the same.A substitution is a list of bindings from names (of type variables) to concrete types. It can be applied to any structure containing types, which will replace the type variables in said type with the type in their entry of the substitution, where applicable.As an example, the types and can be unified using the substitution , yielding the unified expression . The typical algorithm for unifying 2 types is fairly simple, written below as F# code:let rec unify (t1: Type) (t2: Type) : Map&lt;string, Type&gt; = match t1, t2 with | a, b when a = b -&gt; Map.empty | TypeVar a, b when not (occurs a b) -&gt; Map.ofList [(a, b)] | a, TypeVar b when not (occurs b a) -&gt; Map.ofList [(b, a)] | TypeArrow (l1, r1), TypeArrow (l2, r2) -&gt; let s1 = unify l1 l2 let s2 = unify (apply s1 r1) (apply s2 r2) compose s1 s2 | _ -&gt; failwith "Could not unify types"
In English, this roughly means:
If the 2 types are the same yields the empty substitution
If either type is a type variable, and does not occur in the other type, return a substitution binding the type variable to the other type.
If we are unifying 2 function (arrow) types, unify the first type in each function type, apply the resulting substitution to the second type in each function type, and then unify those. Combine the 2 resulting substitutions into 1 and return that.
All other cases fail to unify. This indicates a type error in the program.
The so-called 'occurs check' is there to prevent pairs of types such as and from unifying, since that would result in an infinite type.The apply function takes a substitution and a type, and applies that substitution to a type.In HM, generalization happens only in 1 place: When inferring let bindings. This is known as let-polymorphism or let-generalization. This lets the type system handle expression such as:let id = fun(x) -&gt; x in (x true, x 3)
Where the name id has been bound to a polymorphic value of type . If one attempts to generalize in other places during inference, it will certainly cause issues and incorrectly inferred types.This means concretely that, while we are inferring the type of a let expression, when we are about to put a new binding into the environment before inferring the body of the let, we generalize the type of that binding, and put the generalized type into the environment instead. Remember that generalization is just universally quantifying free type variables.Disclaimer: I don't really know what I am talking aboutHM is hard to extend without introducing a bunch of soundness holes. It can be done, though.HM (as with many type systems) is essentially just glorified propositional logic. reads both as "the type of a function from p to q" in type-land and as "p implies q" in logic-land. Both are valid interpretations.Haskell-style typeclasses under this interpretation are predicates. The Haskell type Num a =&gt; a -&gt; a -&gt; a written as predicate logic is .One can extend the type scheme, which was previously the most general way to describe a type at our disposal, to also include a list of predicates. Each predicate could be represented as a tuple of an identifier and a type, indicating that the type 'is a member of' the typeclass described by the identifier. If one accumulates these predicates during inference, just as is done for substitutions, one can solve these constraints in the end to see if the program typechecks. The problem basically becomes a typical predicate-logic problem, and one that languages such as Prolog interestingly seem like they were made to solve.Much more about these ideas in the "Typing Haskell in Haskell" paper linked to below.<a rel="noopener nofollow" class="external-link is-unresolved" href="https://gist.github.com/pema99/dab60ee4248eef2cff5e74e76d672620" target="_self">https://gist.github.com/pema99/dab60ee4248eef2cff5e74e76d672620</a><br><a rel="noopener nofollow" class="external-link is-unresolved" href="https://github.com/pema99/bonk/blob/master/src/Inference.fs" target="_self">https://github.com/pema99/bonk/blob/master/src/Inference.fs</a><br><a rel="noopener nofollow" class="external-link is-unresolved" href="https://course.ccs.neu.edu/cs4410sp19/lec_type-inference_notes.html" target="_self">https://course.ccs.neu.edu/cs4410sp19/lec_type-inference_notes.html</a><br><a rel="noopener nofollow" class="external-link is-unresolved" href="http://dev.stephendiehl.com/fun/006_hindley_milner.html#inference-monad" target="_self">http://dev.stephendiehl.com/fun/006_hindley_milner.html#inference-monad</a><br><a rel="noopener nofollow" class="external-link is-unresolved" href="https://jgbm.github.io/eecs662f17/Notes-on-HM.html" target="_self">https://jgbm.github.io/eecs662f17/Notes-on-HM.html</a><br><a rel="noopener nofollow" class="external-link is-unresolved" href="https://gist.github.com/chrisdone/0075a16b32bfd4f62b7b" target="_self">https://gist.github.com/chrisdone/0075a16b32bfd4f62b7b</a><br><a href=".?query=tag:type-theory" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#type-theory">#type-theory</a> <a href=".?query=tag:math" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#math">#math</a> <a href=".?query=tag:hindley-milner" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#hindley-milner">#hindley-milner</a> <a href=".?query=tag:type-inference" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#type-inference">#type-inference</a>]]></description><link>math/hindley-milner.html</link><guid isPermaLink="false">Math/Hindley-Milner.md</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate></item><item><title><![CDATA[Integration primer]]></title><description><![CDATA[For simple univariate functions, integration is equivalent to "finding the area under the function".A discrete method is a Riemann sum: Split into rectangles with height given by the integrand, sum their area:
The limit of a Riemann sum is a definite integral:
Where ranges from to . We can imagine a definite integral as a Riemann sum of infinitely thin rectangles.This infinitely small quantity, , is called a differential.Definite integrals typically look like this:
Fundamental theorem of calculus tells us that (assuming continuity):
Where Sometimes the domain of integration is written at the bottom, as we saw in the rendering equation:This notation lets us use more complex domains, such as "a hemisphere of directions"<a href=".?query=tag:math" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#math">#math</a> <a href=".?query=tag:light-transport" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#light-transport">#light-transport</a> ]]></description><link>math/integration-primer.html</link><guid isPermaLink="false">Math/Integration primer.md</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate></item><item><title><![CDATA[Constructing an orthonormal basis around a normal]]></title><description><![CDATA[void revisedONB(const Vec3f &amp;n, Vec3f &amp;b1, Vec3f &amp;b2) { if(n.z&lt;0.) { const float a = 1.0f / (1.0f - n.z); const float b = n.x * n.y * a; b1 = Vec3f(1.0f - n.x * n.x * a, -b, n.x); b2 = Vec3f(b, n.y * n.y*a - 1.0f, -n.y); } else { const float a = 1.0f / (1.0f + n.z); const float b = -n.x * n.y * a; b1 = Vec3f(1.0f - n.x * n.x * a, b, -n.x); b2 = Vec3f(b, 1.0f - n.y * n.y * a, -n.y); }
}
Branchless:void branchlessONB(const Vec3f &amp;n, Vec3f &amp;b1, Vec3f &amp;b2)
{ float sign = copysignf(1.0f, n.z); const float a = -1.0f / (sign + n.z); const float b = n.x * n.y * a; b1 = Vec3f(1.0f + sign * n.x * n.x * a, sign * b, -sign * n.x); b2 = Vec3f(b, sign + n.y * n.y * a, -n.y);
}
<a rel="noopener nofollow" class="external-link is-unresolved" href="https://jcgt.org/published/0006/01/01/paper.pdf" target="_self">https://jcgt.org/published/0006/01/01/paper.pdf</a><br><a href=".?query=tag:math" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#math">#math</a>]]></description><link>math/constructing-an-orthonormal-basis-around-a-normal.html</link><guid isPermaLink="false">Math/Constructing an orthonormal basis around a normal.md</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate></item><item><title><![CDATA[output]]></title><description><![CDATA[<img src="attachments/output.gif" target="_self">]]></description><link>attachments/output.html</link><guid isPermaLink="false">Attachments/output.gif</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="attachments/output.gif" length="0" type="image/gif"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;attachments/output.gif&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[output 1]]></title><description><![CDATA[<img src="attachments/output-1.gif" target="_self">]]></description><link>attachments/output-1.html</link><guid isPermaLink="false">Attachments/output 1.gif</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[nZeABptci6]]></title><description><![CDATA[<img src="attachments/nzeabptci6.gif" target="_self">]]></description><link>attachments/nzeabptci6.html</link><guid isPermaLink="false">Attachments/nZeABptci6.gif</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="attachments/nzeabptci6.gif" length="0" type="image/gif"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;attachments/nzeabptci6.gif&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[gficFwsUQ5]]></title><description><![CDATA[<img src="attachments/gficfwsuq5.gif" target="_self">]]></description><link>attachments/gficfwsuq5.html</link><guid isPermaLink="false">Attachments/gficFwsUQ5.gif</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="attachments/gficfwsuq5.gif" length="0" type="image/gif"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;attachments/gficfwsuq5.gif&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[ezgif-31017683bfd76e]]></title><description><![CDATA[<img src="attachments/ezgif-31017683bfd76e.gif" target="_self">]]></description><link>attachments/ezgif-31017683bfd76e.html</link><guid isPermaLink="false">Attachments/ezgif-31017683bfd76e.gif</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="attachments/ezgif-31017683bfd76e.gif" length="0" type="image/gif"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;attachments/ezgif-31017683bfd76e.gif&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[euWcmAvPSi]]></title><description><![CDATA[<img src="attachments/euwcmavpsi.gif" target="_self">]]></description><link>attachments/euwcmavpsi.html</link><guid isPermaLink="false">Attachments/euWcmAvPSi.gif</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="attachments/euwcmavpsi.gif" length="0" type="image/gif"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;attachments/euwcmavpsi.gif&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[eTkln5rPR6]]></title><description><![CDATA[<img src="attachments/etkln5rpr6.gif" target="_self">]]></description><link>attachments/etkln5rpr6.html</link><guid isPermaLink="false">Attachments/eTkln5rPR6.gif</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="attachments/etkln5rpr6.gif" length="0" type="image/gif"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;attachments/etkln5rpr6.gif&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[WeirdMips 8]]></title><description><![CDATA[<img src="attachments/weirdmips-8.png" target="_self">]]></description><link>attachments/weirdmips-8.html</link><guid isPermaLink="false">Attachments/WeirdMips 8.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[WeirdMips 7]]></title><description><![CDATA[<img src="attachments/weirdmips-7.png" target="_self">]]></description><link>attachments/weirdmips-7.html</link><guid isPermaLink="false">Attachments/WeirdMips 7.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[WeirdMips 6]]></title><description><![CDATA[<img src="attachments/weirdmips-6.png" target="_self">]]></description><link>attachments/weirdmips-6.html</link><guid isPermaLink="false">Attachments/WeirdMips 6.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[WeirdMips 5]]></title><description><![CDATA[<img src="attachments/weirdmips-5.png" target="_self">]]></description><link>attachments/weirdmips-5.html</link><guid isPermaLink="false">Attachments/WeirdMips 5.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[WeirdMips 4]]></title><description><![CDATA[<img src="attachments/weirdmips-4.png" target="_self">]]></description><link>attachments/weirdmips-4.html</link><guid isPermaLink="false">Attachments/WeirdMips 4.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[WeirdMips 1]]></title><description><![CDATA[<img src="attachments/weirdmips-1.png" target="_self">]]></description><link>attachments/weirdmips-1.html</link><guid isPermaLink="false">Attachments/WeirdMips 1.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Unity_HTfdhuIUKO]]></title><description><![CDATA[<img src="attachments/unity_htfdhuiuko.png" target="_self">]]></description><link>attachments/unity_htfdhuiuko.html</link><guid isPermaLink="false">Attachments/Unity_HTfdhuIUKO.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="attachments/unity_htfdhuiuko.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;attachments/unity_htfdhuiuko.png&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[SphericalIntegration]]></title><description><![CDATA[<img src="attachments/sphericalintegration.png" target="_self">]]></description><link>attachments/sphericalintegration.html</link><guid isPermaLink="false">Attachments/SphericalIntegration.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="attachments/sphericalintegration.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;attachments/sphericalintegration.png&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20250509181704]]></title><description><![CDATA[<img src="attachments/pasted-image-20250509181704.png" target="_self">]]></description><link>attachments/pasted-image-20250509181704.html</link><guid isPermaLink="false">Attachments/Pasted image 20250509181704.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[SphericalCoordinates]]></title><description><![CDATA[<img src="attachments/sphericalcoordinates.png" target="_self">]]></description><link>attachments/sphericalcoordinates.html</link><guid isPermaLink="false">Attachments/SphericalCoordinates.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="attachments/sphericalcoordinates.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;attachments/sphericalcoordinates.png&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20250509175305]]></title><description><![CDATA[<img src="attachments/pasted-image-20250509175305.png" target="_self">]]></description><link>attachments/pasted-image-20250509175305.html</link><guid isPermaLink="false">Attachments/Pasted image 20250509175305.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20250509174714]]></title><description><![CDATA[<img src="attachments/pasted-image-20250509174714.png" target="_self">]]></description><link>attachments/pasted-image-20250509174714.html</link><guid isPermaLink="false">Attachments/Pasted image 20250509174714.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20250509030612]]></title><description><![CDATA[<img src="attachments/pasted-image-20250509030612.png" target="_self">]]></description><link>attachments/pasted-image-20250509030612.html</link><guid isPermaLink="false">Attachments/Pasted image 20250509030612.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20250509173124]]></title><description><![CDATA[<img src="attachments/pasted-image-20250509173124.png" target="_self">]]></description><link>attachments/pasted-image-20250509173124.html</link><guid isPermaLink="false">Attachments/Pasted image 20250509173124.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20250509015001]]></title><description><![CDATA[<img src="attachments/pasted-image-20250509015001.png" target="_self">]]></description><link>attachments/pasted-image-20250509015001.html</link><guid isPermaLink="false">Attachments/Pasted image 20250509015001.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20250508223718]]></title><description><![CDATA[<img src="attachments/pasted-image-20250508223718.png" target="_self">]]></description><link>attachments/pasted-image-20250508223718.html</link><guid isPermaLink="false">Attachments/Pasted image 20250508223718.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20250509010904]]></title><description><![CDATA[<img src="attachments/pasted-image-20250509010904.png" target="_self">]]></description><link>attachments/pasted-image-20250509010904.html</link><guid isPermaLink="false">Attachments/Pasted image 20250509010904.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20250508020347]]></title><description><![CDATA[<img src="attachments/pasted-image-20250508020347.png" target="_self">]]></description><link>attachments/pasted-image-20250508020347.html</link><guid isPermaLink="false">Attachments/Pasted image 20250508020347.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20250507005834]]></title><description><![CDATA[<img src="attachments/pasted-image-20250507005834.png" target="_self">]]></description><link>attachments/pasted-image-20250507005834.html</link><guid isPermaLink="false">Attachments/Pasted image 20250507005834.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20250507015358]]></title><description><![CDATA[<img src="attachments/pasted-image-20250507015358.png" target="_self">]]></description><link>attachments/pasted-image-20250507015358.html</link><guid isPermaLink="false">Attachments/Pasted image 20250507015358.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20231008034016]]></title><description><![CDATA[<img src="attachments/pasted-image-20231008034016.png" target="_self">]]></description><link>attachments/pasted-image-20231008034016.html</link><guid isPermaLink="false">Attachments/Pasted image 20231008034016.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20231008194242]]></title><description><![CDATA[<img src="attachments/pasted-image-20231008194242.png" target="_self">]]></description><link>attachments/pasted-image-20231008194242.html</link><guid isPermaLink="false">Attachments/Pasted image 20231008194242.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20241108005122]]></title><description><![CDATA[<img src="attachments/pasted-image-20241108005122.png" target="_self">]]></description><link>attachments/pasted-image-20241108005122.html</link><guid isPermaLink="false">Attachments/Pasted image 20241108005122.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20250507005659]]></title><description><![CDATA[<img src="attachments/pasted-image-20250507005659.png" target="_self">]]></description><link>attachments/pasted-image-20250507005659.html</link><guid isPermaLink="false">Attachments/Pasted image 20250507005659.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20250507005811]]></title><description><![CDATA[<img src="attachments/pasted-image-20250507005811.png" target="_self">]]></description><link>attachments/pasted-image-20250507005811.html</link><guid isPermaLink="false">Attachments/Pasted image 20250507005811.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20230923220434]]></title><description><![CDATA[<img src="attachments/pasted-image-20230923220434.png" target="_self">]]></description><link>attachments/pasted-image-20230923220434.html</link><guid isPermaLink="false">Attachments/Pasted image 20230923220434.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20230923221034]]></title><description><![CDATA[<img src="attachments/pasted-image-20230923221034.png" target="_self">]]></description><link>attachments/pasted-image-20230923221034.html</link><guid isPermaLink="false">Attachments/Pasted image 20230923221034.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20231005215702]]></title><description><![CDATA[<img src="attachments/pasted-image-20231005215702.png" target="_self">]]></description><link>attachments/pasted-image-20231005215702.html</link><guid isPermaLink="false">Attachments/Pasted image 20231005215702.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20231005232357]]></title><description><![CDATA[<img src="attachments/pasted-image-20231005232357.png" target="_self">]]></description><link>attachments/pasted-image-20231005232357.html</link><guid isPermaLink="false">Attachments/Pasted image 20231005232357.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20231005232635]]></title><description><![CDATA[<img src="attachments/pasted-image-20231005232635.png" target="_self">]]></description><link>attachments/pasted-image-20231005232635.html</link><guid isPermaLink="false">Attachments/Pasted image 20231005232635.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20230923215710]]></title><description><![CDATA[<img src="attachments/pasted-image-20230923215710.png" target="_self">]]></description><link>attachments/pasted-image-20230923215710.html</link><guid isPermaLink="false">Attachments/Pasted image 20230923215710.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20230923220012]]></title><description><![CDATA[<img src="attachments/pasted-image-20230923220012.png" target="_self">]]></description><link>attachments/pasted-image-20230923220012.html</link><guid isPermaLink="false">Attachments/Pasted image 20230923220012.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20230923214729]]></title><description><![CDATA[<img src="attachments/pasted-image-20230923214729.png" target="_self">]]></description><link>attachments/pasted-image-20230923214729.html</link><guid isPermaLink="false">Attachments/Pasted image 20230923214729.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20230923214156]]></title><description><![CDATA[<img src="attachments/pasted-image-20230923214156.png" target="_self">]]></description><link>attachments/pasted-image-20230923214156.html</link><guid isPermaLink="false">Attachments/Pasted image 20230923214156.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20230923214516]]></title><description><![CDATA[<img src="attachments/pasted-image-20230923214516.png" target="_self">]]></description><link>attachments/pasted-image-20230923214516.html</link><guid isPermaLink="false">Attachments/Pasted image 20230923214516.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20230917184315]]></title><description><![CDATA[<img src="attachments/pasted-image-20230917184315.png" target="_self">]]></description><link>attachments/pasted-image-20230917184315.html</link><guid isPermaLink="false">Attachments/Pasted image 20230917184315.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20230923211452]]></title><description><![CDATA[<img src="attachments/pasted-image-20230923211452.png" target="_self">]]></description><link>attachments/pasted-image-20230923211452.html</link><guid isPermaLink="false">Attachments/Pasted image 20230923211452.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20230917182004]]></title><description><![CDATA[<img src="attachments/pasted-image-20230917182004.png" target="_self">]]></description><link>attachments/pasted-image-20230917182004.html</link><guid isPermaLink="false">Attachments/Pasted image 20230917182004.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20230917182012]]></title><description><![CDATA[<img src="attachments/pasted-image-20230917182012.png" target="_self">]]></description><link>attachments/pasted-image-20230917182012.html</link><guid isPermaLink="false">Attachments/Pasted image 20230917182012.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20230917182832]]></title><description><![CDATA[<img src="attachments/pasted-image-20230917182832.png" target="_self">]]></description><link>attachments/pasted-image-20230917182832.html</link><guid isPermaLink="false">Attachments/Pasted image 20230917182832.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20230917172908]]></title><description><![CDATA[<img src="attachments/pasted-image-20230917172908.png" target="_self">]]></description><link>attachments/pasted-image-20230917172908.html</link><guid isPermaLink="false">Attachments/Pasted image 20230917172908.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20230917175503]]></title><description><![CDATA[<img src="attachments/pasted-image-20230917175503.png" target="_self">]]></description><link>attachments/pasted-image-20230917175503.html</link><guid isPermaLink="false">Attachments/Pasted image 20230917175503.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20230917180234]]></title><description><![CDATA[<img src="attachments/pasted-image-20230917180234.png" target="_self">]]></description><link>attachments/pasted-image-20230917180234.html</link><guid isPermaLink="false">Attachments/Pasted image 20230917180234.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[LfHimbKHp2]]></title><description><![CDATA[<img src="attachments/lfhimbkhp2.gif" target="_self">]]></description><link>attachments/lfhimbkhp2.html</link><guid isPermaLink="false">Attachments/LfHimbKHp2.gif</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="attachments/lfhimbkhp2.gif" length="0" type="image/gif"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;attachments/lfhimbkhp2.gif&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Pasted image 20230917172903]]></title><description><![CDATA[<img src="attachments/pasted-image-20230917172903.png" target="_self">]]></description><link>attachments/pasted-image-20230917172903.html</link><guid isPermaLink="false">Attachments/Pasted image 20230917172903.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[KTD0DUw0Ul]]></title><description><![CDATA[<img src="attachments/ktd0duw0ul.gif" target="_self">]]></description><link>attachments/ktd0duw0ul.html</link><guid isPermaLink="false">Attachments/KTD0DUw0Ul.gif</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="attachments/ktd0duw0ul.gif" length="0" type="image/gif"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;attachments/ktd0duw0ul.gif&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[AVNlMTV1Uq]]></title><description><![CDATA[<img src="attachments/avnlmtv1uq.png" target="_self">]]></description><link>attachments/avnlmtv1uq.html</link><guid isPermaLink="false">Attachments/AVNlMTV1Uq.png</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="attachments/avnlmtv1uq.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;attachments/avnlmtv1uq.png&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[J9MzgxLxZf]]></title><description><![CDATA[<img src="attachments/j9mzgxlxzf.gif" target="_self">]]></description><link>attachments/j9mzgxlxzf.html</link><guid isPermaLink="false">Attachments/J9MzgxLxZf.gif</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="attachments/j9mzgxlxzf.gif" length="0" type="image/gif"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;attachments/j9mzgxlxzf.gif&quot;&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[7v9yUxSrbZ (1)]]></title><description><![CDATA[<img src="attachments/7v9yuxsrbz-(1).gif" target="_self">]]></description><link>attachments/7v9yuxsrbz-(1).html</link><guid isPermaLink="false">Attachments/7v9yUxSrbZ (1).gif</guid><pubDate>Fri, 12 Jun 2026 19:56:41 GMT</pubDate><enclosure url="." length="0" type="false"/><content:encoded>&lt;figure&gt;&lt;img src=&quot;.&quot;&gt;&lt;/figure&gt;</content:encoded></item></channel></rss>