Interview with a Material Artist

General / 24 May 2022

This year I have been almost 10 years in the games industry and wanted to share my ideas and answer questions that I have gotten over the years but didn't get to answer. Hopefully my blogpost will inspire you and help you find your way within the industry or maybe even help you find your niche?

How did you get into such a cool IP as Horizon? What sort of brought you in this direction?
Back in 2012 I was already an intern at the studio, a couple of years before I rejoined in 2014, however during my internship I did see some early concept art for this new IP now called Horizon. When I was about to join the team I didn't know for sure which project I'd be working on but, as you can imagine, I had my suspicions. Back then I was mostly working on assets or environment-art but was also looking into creating shaders and material expressions. This technical interest landed me the shader/texture artist position and started delving deeper into this area of expertise over the last couple of years.


What was your general approach to assets in this production? You’ve had quite a tricky task, building all those amazing materials. How did you decide to tackle this?
During the concept phase there were already a whole lot of reference images available (collected by our talented Concept-Artist and Directors) but also my Art-Director had specifications of what he was looking for. The target was to blend this look from the proposed Concept Art and the requirements of the Environment-team/Art-Director(s) and of course I had my own input. From these reference images I have created a huge reference sheet with everything I found interesting per image and from there we picked and chose which characteristics we liked and added callouts to highlight what we felt was necessary to sell the idea of the materials. This really helps to get everyone on board with the exact look we were going for.For any artist I'd suggest; always try to collect images to build your own material library, this can be Pinterest or snapshots on holiday. I do this and then after one or two years, I delete everything and refresh my entire collection.

Ref images

Ref images


You were using Photoshop and ZBrush to craft all those amazing textures. Could you talk in more detail how it all worked?
During the development of previous projects we worked with high poly sculpts in Zbrush to generate detailed heightmap information from those. But when we started implementing Substance with a few textures to get a feel of the program and its workflow. For example with a gravel texture, we generated tiny pebbles and added multiple stacks with offsets and a variety of scaling to make it look more interesting and finalize it with some photo overlays and color correction in Photoshop. 

No matter which program or tool we used, we always focused on getting the height information correct first, before diving into the Color and Roughness values too much. For some textures it felt more comfortable to generate the content in Zbrush as it gave me complete control per brick (or had to match with pre-existing assets/models), I was able to put each brick at an angle or give it height differences to give it some nice parallaxing effect. The downside was: it’s very time consuming. For texturing the albedo/diffuse we tried several approaches, for example: polypainting the bricks in zbrush but we had to keep such a high polycount that Zbrush became unworkable and too little poly density would result in a lack of detail. Then we used Photoshop but now that Substance expanded their libraries a lot is possible now, that wasn't before. I would've picked up a hybrid approach, generated high poly mesh and generated the diffuse and roughness in Substance.


You’ve mentioned that you choose Photoshop because of more control over the subtleties in color/height variation. Why was this more important to you? I mean you could have gotten very similar results Procedurally.
In hindsight I probably could have pulled off a similar result. As the height information was the most important to me, it really sold the textural details and state of the bricks and ultimately sold the believability of the material. In the reference images that were collected, it showed me the importance of all the states of decay that were having subtle tonal variety and height values.

Timelapse of focusing on the height information first.Before adding diffuse/roughness.


How did you make these materials tile in such a beautiful manner? Did you use some other tools to scatter the rocks here and other little things?

With a bit of planning and proper mesh setup, you can easily offset your subtools and align them so it’s tiling perfectly (especially now with Substance Designer in our arsenal). Getting the scale right versus the right amount of detail and uniqueness is tricky. Each brick was placed as a unique subtool, so it could easily get warped and moved around. We iterated many times on the brick layout to get the right feel before we proceeded with the Diffuse/Albedo/Roughness maps.

The scattering of rocks was a combination with custom Maya scripts where I could scatter kitbashed rocks or in Substance Designer. Scattering rocks with photo scanned data was interesting to familiarize yourself with generating procedural content and also match it with pre-existing photoreal content.


You’ve done some absolutely stunning work with the brick wall. It’s like the most favorite subject of every texture artist, but your material is something else. Can you tell us, how did you manage to build it in such a way that the brick wall actually has information about 3 types of bricks: old, worn down and new. 

Planning was very essential for this to succeed. First we started blocking out the intact version of the bricks and tested the look and feel of the layout in-game. We checked for scale, height variation, repeating elements - even a flat color in the albedo with some curvature and ambient occlusion information can help a lot visually to give a feel of the surface and readability over distance.

I then reworked the high poly sculpt and baked out maps for the first pass - I grab all the baked maps, e.g. Position to World Space Normals, custom mat caps in Zbrush. This gives me a wide variety of masking methods I can pick and choose from to create the tonal variety. Blending the Curvature map with the Position map and a random (brick) variation mask, created interesting variations. Next step is to apply more colors by adding photos, mask out bricks based on height or manually select them, add tonal gradients with the HSL slider/node for per brick subtle variations.

For the second material we used the exact same layout in Zbrush and started to replace bricks of the same size or used the well known Dam standard brush or Orb Crack brush combined with a custom alpha mask to split up the bricks or use the TrimSmoothBorder brush to soften the edges (as worn brick does over time). On certain bricks we would add some alpha stamps to make the brick look more damaged. Or by moving some bricks even lower and skewed which emphasized the aging process even more.


How did they help you to nail that beautiful hard surface stuff?

Maarten (Art Director) and I were looking for a way to speed up the texturing process but also maintain the quality that was pushed throughout the game. The two of us decided to delve deeper into the Substance packages and set up custom nodes and materials which also extended our internal Substance library. During this iteration process of creating nodes and testing them, we created a smart material that we could apply to almost all the assets. In 90% of the cases it would get us there and in some cases there were some tweaks needed but it sped up the art creation process quite a lot. Between the two of us we managed to export 45-ish component sets within two days with all the latest smart materials updated and correct masking for detail maps.


How did you work on those wonderful rusty elements in the production? How were these set up? What were the challenges in these assets?

The rusty element was an iterative process of creating custom Substance nodes. First, we started making generic materials with some light wear, tear and discoloration. In the second iteration, we started adding things like dust, dirt and rust. To get the realism we were looking for, we worked on custom mask generators, e.g. rust got stored into its own user-channel, which took Ambient Occlusion and Curvature in mind. With an additional custom node, we can generate streaks based on the rust mask user-channel, this gives us the drips and very long streaks.


Over all, to finalize, how did these materials help to tell the story in the environment? Why do you think they are even important for these humongous productions?

Material expressions are supposed to give the player the idea that they are in a believable world, that it becomes almost tangible. If a material looks ‘off’ it will break that illusion and snap the player right out of the immersion. The materials will tell the story the world is being lived in, it shows age and beauty. But also the interaction between materials, how water affects wood or metal for example or what erosion does to rocks or bricks. No matter how large the production environment is, you can do this kind of environmental storytelling in all sorts of ways.



Day 58 - 🔬 Deep dive - The One Concept I Understand Better Now

General / 28 July 2026

Energy conservation. I could follow the rules. I couldn't explain why breaking them breaks everything downstream.


Where I stood on Day 1

Lighting has never been my strongest area. I know how materials respond to it: how roughness, metalness, and F0 determine what gets reflected where. But I hadn't dived into the rendering side properly in a long time. I wrote custom viewport shaders, specific conversion nodes, etc. I'd dabbled with concepts like energy conservation, Fresnel, masking-shadowing. I hadn't gotten my hands into the actual math for over 3-5 years now, and only slightly understood why the rules are the rules.

That gap is part of why the rendering block of this series was the hardest to write. It's also where I learned the most.


What the concept actually connects

The principle: a surface cannot reflect more light than it receives. Diffuse and specular are complementary. As specular rises, diffuse falls. That constraint isn't arbitrary. It's the condition under which every downstream system produces predictable results.

A material that violates energy conservation injects extra energy into every bounce. Indirect light brightens in ways that are hard to trace. The error doesn't stay in the material slot. It propagates through the whole lighting chain.

This is why the albedo range exists. Why metalness is binary. Why F0 for dielectrics sits around 0.04. The rules are load-bearing. They're the conditions under which the lighting math works correctly, from the material outward through global illumination.


Where it clicked

Deeply investigating IBL and Energy Conservation made this concrete. The split-sum approximation precomputes the specular response under a fixed environment, and that derivation is built on the same energy conservation and Fresnel assumptions that PBR materials follow. The two systems are designed together.

A non-compliant material fed into a physically correct IBL produces incorrect output even if the environment capture was perfect. Writing Day 21 - 🔬 Deep dive - PBR Compliant Work and Day 22 - 🔬 Deep dive - How IBL Works in sequence made that connection visible. Trace the energy from the material through the BRDF through the precomputed LUT to the final pixel and you can see how each step depends on the previous one being correct.


Practical takeaway

Knowing how light and materials interact at this level isn't just academic. It explains why certain lighting conditions work better for specific material types, how the BRDF constrains what's artistically possible and where limitations come from. Understanding the rules tells you which ones can be bent and which ones break the whole chain when you push past them.


© 2026 Stefan Groenewoud. All views are my own, not those of my employer.

Day 57 - 🛠️ Behind the process - What I Learned from 60 Days of Writing

General / 27 July 2026

The topics are easy to list. What actually changed is harder to name precisely.


What the habit changed

Putting your thoughts and knowledge out there is daunting, especially as an introvert. In an office environment, sharing ideas with colleagues feels natural. Doing it in public is a different kind of exposure and pressure.

The biggest shift was discovering where my understanding was thinner than I thought. IBL is the clearest example. I'd implemented it, debugged it, used it in production. Then I tried to write a post explaining how it actually works, from cubemap capture through prefiltering to the split-sum approximation, and got stuck. The explanation revealed the gaps.

Some of these systems have also evolved since I first properly read about them. What I understood back then was correct for that moment. Writing forced me to dig deeper and verify, because the last thing I wanted was to put incorrect information out into the world.


What surprised me

Which posts landed and which didn't wasn't what I expected. The technically dense ones (IBL, specular aliasing, tone mapping) got more engagement than the lighter takes. I'd assumed depth would narrow the audience. It seemed to have the opposite effect, at least on ArtStation.

Posts with visual examples consistently held up better than text-heavy ones. That wasn't surprising in hindsight, but I kept underestimating how much one diagram changes the read.


What I'd do differently

Two things.

Visuals first, not last. I kept treating images as something to find or create after the writing was done. That's backwards. If a post needs a diagram, planning it upfront shapes the writing. Several posts that ran long could have been tighter if the diagram had done the explanatory work instead.

A narrower scope. Going from LOD pipelines to BRDF math across eight weeks is a lot of context-switching for a reader. The range made sense to me. It maps to how I actually think about technical art. But I'm not sure it mapped equally well to an audience trying to follow a series. A tighter theme would have built a cleaner thread.


© 2026 Stefan Groenewoud. All views are my own, not those of my employer.

Day 56 - 📖 Learning - Week 8 Reflection

General / 23 July 2026

Writing the BRDF end-to-end showed me how much I'd remembered. Writing the metalness showed me what I was still missing.


What clicked

The BRDF series was eight years in the making, in a sense. I'd implemented something similar back then but never stopped to understand each term properly. Writing it from scratch this time, term by term, was different. The moment the full shader started producing expected output, the individual pieces (diffuse, specular, ambient, roughness) made sense as a system rather than a checklist.

That was the specific shift: not "here is what GGX does" but why each term exists and what breaks when you remove it. Seeing it work end-to-end in code made that real in a way that reading about it never quite did.


What flopped

The metalness experiments. Frustrating and instructive in equal measure. The math was working. The problem was the environment: the shader previewer doesn't support texture sampling, so proper specular reflection was off the table entirely. Knowing that it's an environment constraint and not a math problem is a useful thing to understand. It still wasn't the result I wanted.


Into the final stretch

Block 5 is less about technical depth and more about closing the arc. What the last few posts need to do is say something about what 60 days of this actually felt like, not just what was learned. The technical body of work is there. The final posts are for the other part.


© 2026 Stefan Groenewoud. All views are my own, not those of my employer.

Day 55 - 💬 Take - Why Technical Artists Should Learn Math

General / 22 July 2026

I hated math at school. I just hadn't found a problem worth caring about yet.

Most technical artists I know are self-taught in math. Some never felt the gap. Others hit it mid-career when the work got more complex: shader debugging, LOD budgets, performance tradeoffs that needed real justification. I had a good mentor who reignited the interest in me, because I genuinely hated it in high school.


Where it actually pays off

For me it showed up in three places: writing shaders, optimization, and building custom scripts and nodes for image processing and automated tools. All three involve parameters with mathematical shapes. Knowing whether you're working with a linear, squared, or exponential relationship changes how you approach the problem entirely.

Debugging a shader is different when you can reason about the equation rather than tweak values until it looks right. If the BRDF output looks wrong at grazing angles, knowing which term is responsible tells you exactly where to look. Without the math, you're guessing.

The most consistent payoff: when something breaks and you know why, you fix it faster and explain it clearly to whoever needs to hear it.


Where it doesn't

You don't need a degree. Most of what comes up in technical art is linear algebra and some calculus fundamentals: vectors, dot products, matrices, derivatives.

The value of deeper math also depends on the role. Rigging-heavy work is different from shaders-heavy work. Pipeline work leans more on scripting logic than graphics theory. It compounds fastest if you're working close to rendering.

If math feels like a wall: don't start with a textbook. Start with a problem you're curious about. The math sticks faster when there's something concrete to attach it to.


© 2026 Stefan Groenewoud. All views are my own, not those of my employer.

Day 54 - 🧪 Experiment - Writing a BRDF from Scratch (Part 4)

General / 20 July 2026

The BRDF from Parts 1 through 3 covered opaque, non-transmissive materials. This post adds a translucency term to simulate light passing through the surface. Not full subsurface scattering, but a cheap approximation that reads well at a distance.


The Code

The translucency term bends the light direction slightly toward the surface normal, then raises the result to a power to control spread. A distance-based mask keeps it from affecting geometry too far from the light origin.

float translucencyPower = 175.0;
vec3 LTlight            = l + n * 0.003;
float LTDot             = pow(clamp(dot(e, -LTlight), 0.0, 1.0), translucencyPower) * 1.0;
float t                 = clamp(0.4 * length(fWorldPos - vec3(0.0, 0.3, 0.0)), 0.0, 1.0);
vec3 translucent_light  = light_color * (vec3(LTDot) + ambient) * t * light_strength;


Result

The diffuse contribution is softened by interpolating between the base color and the translucency-adjusted color. Applying a curve to the underlying albedo to lift saturation gives a more convincing read at the rim.

Before


After

© 2026 Stefan Groenewoud. All views are my own, not those of my employer.

Day 53 - 🧪 Experiment - Writing a BRDF from Scratch (Part 3)

General / 20 July 2026

Sometimes the constraint is the environment, not the math.

Parts 1 and 2 built a dielectric BRDF: Burley diffuse, GGX specular, and hemisphere ambient. This part extends it to metallic materials and runs into a hard limit in the shader previewer.


Metallic

Metals behave differently from dielectrics. A metallic material absorbs diffuse entirely, so with metalness at 1.0 the diffuse contribution drops to zero. At low roughness, that leaves a mostly dark surface with a sharp specular highlight as the only visible term.


Albedo and Ambient

Adding the diffuse ambient term back in brightens the material noticeably. Without it, the metal reads too dark outside of the specular contribution. The ambient gives enough indirect light for the surface to read correctly.


Specular Ambient

This is where it breaks down. Proper specular ambient requires sampling a cubemap or prefiltered environment texture to capture reflected light. The shader previewer does not support texture sampling, so there is no way to evaluate the reflected environment term correctly.


Result

The shader ends up incomplete, but the failure mode is worth noting. The missing piece is not a math problem. It is an environment problem. A real specular ambient term needs an IBL pipeline: a prefiltered environment map and a BRDF lookup table at minimum. That is out of scope for a single-file GLSL previewer, and a good reason to move this into a proper engine next.


© 2026 Stefan Groenewoud. All views are my own, not those of my employer.

Day 52 - 🧪 Experiment - Writing a BRDF from Scratch (Part 2)

General / 18 July 2026

Specular is where the BRDF starts to earn its name.

Part 1 covered albedo, Lambertian diffuse, and hemisphere ambient. This part adds specular using GGX, then swaps the Lambertian model for Burley diffuse, which folds roughness into the diffuse response too.

From Lambertian to Burley

Instead of writing the diffuse result directly to out_color, I store it separately. With specular terms arriving, keeping them isolated makes the combining step cleaner to reason about.

Lambertian treats diffuse as constant regardless of viewing angle. Burley adds a Fresnel-based correction: it softens the diffuse response at grazing angles and adds a slight retroreflection on rougher surfaces. The difference is subtle, but since roughness was already going into specular, it made sense to have the diffuse side respond to it too.

vec3 Lambertian(float inNoL, vec3 inAlbedo, vec3 inLightColor, float inLightIntensity)
{
  return (inAlbedo / PI) * inLightColor * inLightIntensity * inNoL;
}


float F_Schlick(float inU, float inf0, float inf90)
{
  float Fc = exp2((-5.55473 * inU - 6.98316) * inU);
  return inf0 + (inf90 - inf0) * Fc;
}


// Disney Burley diffuse
float Burley(float NoV, float NoL, float LoH, float roughness) {
    float f90 = 0.5 + 2.0 * roughness * LoH * LoH;
    float lightScatter = F_Schlick(NoL, 1.0, f90);
    float viewScatter = F_Schlick(NoV, 1.0, f90);
    return lightScatter * viewScatter * (1.0 / PI);
}
// ...
vec3 diffuse = albedo * Burley(NoE, NoL, LoH, roughness) * light_color * light_strength * NoL;


GGX Specular

The specular term is the product of three functions: D, V, and F. D is the Normal Distribution Function, which controls the shape of the specular highlight. V is the geometric visibility term, accounting for microfacets that shadow or occlude each other at grazing angles. F is Fresnel, which drives the grazing-angle brightness rise.

V_SmithGGX is included for reference. I used HCV instead, the height-correlated Smith-GGX formulation and a closer match to the Disney model.

float GGX(float inNoH, float inAlpha)
{
  float temp = inAlpha / max(1e-8, (inNoH * inNoH * (inAlpha * inAlpha - 1.0) + 1.0));
  return temp * temp / PI;
}

float F_Schlick(float inU, float inf0, float inf90)
{
  float Fc = exp2((-5.55473 * inU - 6.98316) * inU);
  return inf0 + (inf90 - inf0) * Fc;
}

float GFGL(float inNoX, float inAlpha)
{
  return -0.5 + 0.5 * sqrt(1.0 + inAlpha * inAlpha * (1.0 / (inNoX * inNoX) - 1.0));
}

float HCV(float inNoL, float inNoE, float inAlpha)
{
  inNoL = max(inNoL, 1e-3);
  inNoE = max(inNoE, 1e-3);
  float geo = 1.0 / (1.0 + GFGL(inNoE, inAlpha) + GFGL(inNoL, inAlpha));
  float normalization = 1.0 / (4.0 * inNoL * inNoE);
  return geo * normalization;
}

float V_SmithGGX(float NoV, float NoL, float a) {
    float a2 = a * a;
    float GGXL = NoV * sqrt((-NoL * a2 + NoL) * NoL + a2);
    float GGXV = NoL * sqrt((-NoV * a2 + NoV) * NoV + a2);
    return 0.5 / (GGXV + GGXL);
}

void main()
{
  vec3 light_color  = vec3(1.0, 1.0, 1.0);
  vec3 albedo       = vec3(0.8, 0.25, 0.0);
  float pRoughness  = 0.5;
  vec3 specColor    = vec3(0.2, 0.2, 0.2);

  float roughness = pRoughness * pRoughness; // perceptual roughness remapping

  float D = GGX(NoH, roughness);
  float V = HCV(NoL, NoE, spec_alpha);
  float F = F_Schlick_Fast(LoH, max(max(specColor.x, specColor.y), specColor.z));

  vec3 specular       = vec3(D * V * F) * specColor;
  vec3 specular_light = specular * light_color;

  vec3 diffuse = albedo * Burley(NoE, NoL, LoH, roughness) * light_color * light_strength * NoL;

  vec3 out_color = diffuse;
  out_color += specular_light;
  out_color += (1.0 - NoL) * albedo * ambient;
}


Result

With roughness driving both the Burley diffuse and the GGX specular, the material responds consistently across the full parameter range. The clip below cycles through: black albedo, specular only, albedo plus specular, then with ambient added. Light position and roughness are both animated to verify behavior across conditions.

© 2026 Stefan Groenewoud. All views are my own, not those of my employer.

Day 51 - 🧪 Experiment - Writing a BRDF from Scratch

General / 17 July 2026

Build it yourself and you stop guessing what it's actually doing.

Instead of relying on the engine's built-in shading model, I wanted to write a physically-based BRDF by hand, term by term, to see where my understanding held up and where it didn't. This is part one: diffuse, Lambertian, and ambient. Specular and roughness come in the next post.

Albedo

I started with a flat color value as the base. For now, this is treated as a Lambertian material: fully diffuse, no specular yet.

vec3 albedo = vec3(0.8, 0.25, 0.0);


Lambertian

Next I added the Lambertian lighting model, the industry standard for diffuse and a clean starting point before adding more complex terms. Added a general light direction to drive it.

vec3 lambertian(float inNoL, vec3 inAlbedo, vec3 inLightColor, float inLightIntensity)
{
  return (inAlbedo / PI) * inLightColor * inLightIntensity * inNoL;
}


Hemisphere Ambient

To fake a skylight and bounce effect, I added a hemisphere ambient term. It uses the global world-space normal direction and interpolates between a sky color and a ground bounce color.

float ambient_intensity = 0.75;
vec3 ambient_light_color = vec3(0.5, 0.6, 1.0);
vec3 ambient_bounce_color = vec3(0.4, 0.3, 0.25);
vec3 ambient = mix(ambient_bounce_color, ambient_light_color, fWorldNormal.y * 0.5 + 0.5) * ambient_intensity;



Combining

Now to merge the Lambertian output with the ambient term. A straight addition would overbrighten the whole surface. Ambient is an indirect effect, so it should only affect areas that aren't already hit by direct light. I take the inverse of the light term to mask out those lit areas, keeping the exposure controlled.

vec3 out_color = lambertian(NoL, albedo, light_color, light_strength); // Lambertian
out_color += (1.0 - NoL) * albedo * ambient;                           // Ambient
gl_FragColor = vec4(pow(out_color, vec3(1.0 / gamma)), 1.0);           // Gamma Correction


Up next

This gets the diffuse side solid. The next post adds specular and roughness, which is where the BRDF gets more interesting and where most of the math lives.


© 2026 Stefan Groenewoud. All views are my own, not those of my employer.

Day 50 - 💬 Take - (My) Best Resources for Graphics Math

General / 15 July 2026

Real-Time Rendering taught me how things actually work. That's worth more than it sounds.

The resource I keep coming back to is Real-Time Rendering by Tomas Akenine-Möller, Eric Haines, and Naty Hoffman. The fourth edition is at realtimerendering.com — the same site that's been an industry reference hub for years. It covers the real-time pipeline with actual math: BRDFs, lighting models, shadow algorithms, PBR theory. Written for practitioners, not theoreticians.

Writing the posts on IBL and PBR compliance for this series forced me back to first principles. This is where the answers were.


Why it clicked

Most resources explain the formula and move on. Real-Time Rendering explains the derivation: where it comes from, what assumptions it makes, and what breaks when those don't hold. Understanding what the geometry term is correcting for changed how I read shader code. When something looks wrong at grazing angles, I now know where to look.

I treat it as a reference rather than something to read cover-to-cover. Go deep on the sections you need, come back when something doesn't make sense in production.


The production papers

Real-Time Rendering covers the foundations. SIGGRAPH production presentations show those applied under real constraints.

Real Shading in Unreal Engine 4 (Brian Karis, Epic Games, SIGGRAPH 2013) is probably the most cited real-time PBR paper. It covers the split-sum approximation for IBL, the Cook-Torrance implementation UE4 ships with, and the specific choices Epic made when adapting theory to a shipped engine.

Technical Art of Uncharted 4 covers how Naughty Dog pushed their materials model beyond textbook. The gap between "physically correct" and "what shipped" is instructive.

The Technical Art of The Last of Us Part II is the companion read: same studio, similar thinking, concrete tradeoffs between physical accuracy and art direction.

The Rendering of The Callisto Protocol (GDC 2023, Striking Distance Studios) is worth adding to the list; it shows the same principles applied under tighter console performance budgets, with a strong focus on skin shading and volumetric lighting.

Reading a paper alongside the RTR chapter on the same topic is worth doing at least once. It makes the abstract concrete quickly.


Additional resources

If you want to understand why the roughness slider does what it does but don't have much shader experience yet: start with blog.demofox.org, then come back to RTR when you want the full picture rather than just the isolated formula.

If you're writing shaders or debugging BRDF behavior: RTR is worth the investment.

Desmos helps with the math — plot functions interactively and see what a parameter change actually does to a curve. I used it constantly when working through the NDF and masking-shadowing sections. Physicallybased.info is a solid companion for real-world material reference values.


© 2026 Stefan Groenewoud. All views are my own, not those of my employer.