Day 21 - 🔬 Deep dive - PBR Compliant Work

General / 29 May 2026

Day 13 told you what broken PBR looks like. This post explains why the rules exist in the first place.


PBR-Compliant vs PBR-Using

When authoring shaders and materials, a PBR ruleset ensures that whatever goes into the shader is read out correctly by the lighting system. But the system, the BRDF shading model, also needs to be physically based. Otherwise whatever energy a material carries could be assigned or read out incorrectly, even with the right shader in place.

This distinction is crucial: having a PBR shader doesn’t guarantee physically accurate results. The artwork it processes must adhere to the same rules. A metalness value of 0.5 applied to a physically based BRDF still produces an impossible result, the shader can’t rectify the texture’s error. This is the framework for everything that follows. The sections below delve into the implications, beginning with the physics that necessitates these rules.

Metallic range from 0 to 1.


Energy Conservation

The unifying principle behind all PBR rules: a surface cannot reflect more light than it receives. Diffuse and specular are complementary, as one goes up, the other comes down. This constraint is what makes PBR self-consistent and is the reason the albedo range, metalness binary, and F0 values are what they are.

Whenever a material receives light and scatters it, energy is lost. A bright material like snow scatters more light than a dark material like charcoal, not further, but more of it. This doesn't just affect the material itself; it propagates through the scene via GI light bounces, which is why a non-conserving material can throw off the entire lighting environment around it.


F0 - Reflectance at Normal Incidence

A surface is the interface between the surrounding medium (typically air, with a refractive index of approximately 1.0) and the object's substance. When light hits that interface, the Fresnel equations describe how it splits: some reflects, the rest refracts and enters the material. The proportion that reflects is the Fresnel reflectance F, and it varies with the angle of incidence.

At normal incidence (light arriving perpendicular to the surface at θ = 0°) reflectance reaches its minimum. This baseline value is F0, and it's a fixed property of the material. What makes it useful is how predictable it is: almost every dielectric lands near 0.04 (4%), with a realistic range of roughly 2–6%. Water, plastic, skin, wood, glass: visually very different materials that all sit in roughly the same place. Their differences come from their diffuse response, not their specular.

As the angle increases toward grazing (θ → 90°), F climbs toward 1.0 for all materials and all frequencies. That's the physics behind the edge brightening visible on almost every surface; at grazing angles, everything becomes a mirror.

Metals are the exception. Their F0 is high (typically 0.5 to 1.0) and tinted across the visible spectrum. A gold surface reflects red and green more than blue, giving its reflections their characteristic color. For metals, F0 is the specular color, which is why a PBR workflow stores that color in the albedo map for metallic materials.

This is the physics behind the metalness binary rule. A value of 0.5 describes no real substance; real materials are either dielectrics (F0 ≈ 0.04, uncolored) or metals (F0 high, tinted). The gap between those two ranges is where physically impossible materials live.

The Cook-Torrance BRDF

The Cook-Torrance model splits the surface response into diffuse and specular. The diffuse term (typically a Lambertian or Burley approximation) handles light that enters the material, scatters internally, and exits in a random direction. The specular term handles light that reflects at the surface without entering, and this is where the physical complexity lives.

The specular lobe is the product of three terms.


The Normal Distribution Function (GGX is the standard implementation) describes the statistical distribution of microfacet orientations: microscopic surface irregularities that each reflect light like tiny mirrors. Roughness is the parameter feeding this distribution. A low roughness concentrates the NDF into a tight peak, producing a sharp highlight. A high roughness spreads it wide. This is why a flat grey roughness map reads as artificial: it collapses the NDF to a single value across the entire surface, something no real material does.

The Geometry/Masking term (Schlick's approximation is common) accounts for microfacets that shadow or obscure each other at grazing angles. Without it, the specular response would be unrealistically bright when a surface is viewed or lit obliquely.

The Fresnel term ties directly to F0. Reflectance isn't constant; it scales from F0 at normal incidence up toward 1.0 at grazing angles. The Fresnel term drives this within the shader, so a surface automatically brightens at glancing angles regardless of material type. At perpendicular incidence you see F0; at grazing you see full reflectance. This is what makes PBR materials behave correctly across changing view angles without manual adjustment.

Together, the three terms produce a specular lobe that is energy-conserving by construction: it distributes incoming light, it doesn't create any.


In-Engine Validation

The albedo range and the light test are covered in Day 13 - ⚡️ Quick - How to Spot Broken Materials, this section focuses on what those checks miss, and where the BRDF debug views pick up the slack.

Having ways to do custom Metal or Roughness override debug view can help isolate the specular response and expose metalness values that aren't binary. What a metalness override at 0.5 actually looks like vs 0 or 1, the wrong specular tint is readable immediately.

Roughness override: how a flat grey roughness map collapses the NDF to a single value and makes that obvious. Why these views catch compliance breaks that the albedo overlay doesn't see, you can have a correct albedo and still have a broken specular response. Also: the difference between checking under a neutral lighting vs a directional light for catching roughness issues specifically.


Practical Takeaway

The F0 values are the most practically useful thing to take from this post. Every dielectric sits near 0.04, you don't need to guess or measure. If you're authoring plastic, stone, or skin, the specular sits around 4% and the albedo carries the diffuse color. For metals, F0 is the specular color, so the characteristic tint goes into the albedo map and metalness goes to 1.0: not 0.9, not 0.5, not somewhere in between.

Violating energy conservation has consequences that extend beyond the material itself. A surface that reflects more light than it receives injects extra energy into every GI bounce in the scene, brightening indirect light in ways that are hard to track down. The error doesn't stay local.

The rules in PBR aren't arbitrary restrictions. They're the conditions under which the lighting math, the BRDF, the IBL precomputation, the tone mapping downstream, all produce predictable results. Stay within the ranges and the system behaves correctly. Step outside them and the shader can't correct for it.

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

Day 20 - 💬 Take - The Lighting Skill Gap in Technical Art

General / 28 May 2026

Technical artists tend to think about lighting as a performance problem. It's also a storytelling tool, and that half gets overlooked.

The title might be a slight overstatement. I've only spent brief stretches of my career fully invested in doing lighting. But those periods taught me a lot about how lighting affects everything downstream, post-processing, bloom, exposure, color grading. I also used photography as a stepping stone: understanding how light controls mood and tone, and how you can use it deliberately to direct the eye or reinforce a narrative.

A well-lit scene leads attention. A poorly lit one lets it wander.

source: Resident Evil 7

The Technical Artist's Default

Most technical artists approach lighting primarily as a performance concern. How many dynamic lights are in range? Can we bake this? Are the shadow maps eating the budget? That instinct isn't wrong, lighting can be extremely expensive, and keeping it under control matters.

But the instinct can tip too far. I've seen lighting stripped back to the point where it sacrifices the intended look entirely. There's always a balance to strike between performance and artistic intent, and defaulting too hard to performance loses the plot.

The Student Blind Spot

I see a different version of the same gap with students. They'll spend weeks on a material or a model, getting every detail right, and then present it flat-lit in a gray viewport. The work can't speak for itself in that context.

Working on an asset means wanting to present it as well as possible. Knowing where to add or pull light to emphasize the storytelling matters. In a game environment, not every asset can be a hero prop, and lighting, just as in a film production, can hide a lack of detail where it doesn't matter, and draw attention to where it does.

A simple example: if you're building a horror environment, you don't fill the darkest corners with dense detail. With no light reaching those areas, the player won't see it. Understanding that is set dressing; it's also lighting.

A Note on Deferred vs Forward

Lighting in deferred rendering is generally cheaper per light than in forward rendering, though both have trade-offs.

In forward rendering, each light evaluates its contribution per-fragment, for every piece of geometry within its range, so the cost scales with the number of lights multiplied by the geometry they touch. In deferred rendering, geometry is rendered once to a G-buffer (storing normals, albedo, roughness, depth, and so on), and then lighting is evaluated in screen-space per pixel against the G-buffer data. Lights only operate on what's visible on screen, not on geometry, which makes adding more lights significantly cheaper.

That said, neither system is free. Not every light should cast dynamic shadows, shadow maps have resolution budgets and runtime cost, and using them indiscriminately is how a scene's frame budget disappears. Knowing which lights earn a dynamic shadow and which don't is part of the skill.

Source: learnopengl.com

The Broader Point

Having a basic understanding of how lighting works, how it interacts with surfaces, how it controls mood, and what it costs, is useful regardless of your specific discipline. Whether you're a material artist trying to present your work convincingly, or a technical artist optimizing a pipeline, lighting is part of the picture.

It's one of those areas where a little knowledge goes a long way.

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

Day 19 - ⚡️ Quick - AO and Bent Normals

General / 27 May 2026

Ambient occlusion tells you how much light is blocked. Bent normals tell you which direction it comes from. One number vs one direction, and that difference matters more than it sounds. Again, this started out as a short format but ended up going longer than expected.

What AO Does

Ambient occlusion is a scalar value, a single number per point on a surface that describes how much of the surrounding hemisphere is blocked by nearby geometry. A value of 1 means fully unoccluded, fully exposed to incoming light from every direction. A value of 0 means fully occluded, buried inside geometry with nothing reaching it.

In practice AO is used to darken areas where light has less access, cavities, corners, the underside of a ledge, the gap between two surfaces pressing together. It approximates the soft shadowing that fills those areas in reality, and it does so cheaply because it is baked and static, which also means it can't update for animated or dynamic geometry.

The limitation is that it's directionless. AO darkens a surface based on how occluded it is, but it has no information about where the unblocked light is actually coming from. When combined with IBL, where the incoming radiance varies across the hemisphere - this becomes a problem.

Source: scahilldesign.co.uk


The Gap AO Leaves

When you sample a prefiltered environment map for diffuse lighting, you're integrating incoming radiance across the hemisphere above the surface. AO scales the result down in occluded areas, but it scales it uniformly, as if every direction contributed equally.

In reality, the sky above a surface is bright and the ground below it is dark. A surface sitting in a corner is mostly occluded from above, so it should receive less skylight, but it might still have a clear view toward a bright part of the environment. Scalar AO can't capture this. It just darkens without knowing what it's blocking.

Examples of different real-time AO implementations in Unity.


What Bent Normals Add

A bent normal stores the average unoccluded direction at a given point, the mean direction the surface can actually receive light from, given its local geometry. Instead of a scalar, it's a vector.

When sampling an environment map for indirect lighting, you use the bent normal to look up the right region of the map rather than the geometric normal. The result is that occluded surfaces sample from the part of the environment actually visible to them, not an incorrect average. A surface in a corner looking up at the sky samples the sky. A surface buried in a crevice samples the dimmer environment around it.

The improvement is most visible in complex indoor environments and on characters, where AO-only solutions tend to over-darken flat areas and under-darken the right spots.

Specular Occlusion

On every project I've worked on, specular occlusion has been worth applying. The problem shows up in deep cavities: the reflected view ray doesn't know whether the surrounding geometry is occluded or not, it just looks up a texel from the environment cubemap and reflects it toward the viewer. In heavily occluded areas, this produces light leaks. The surface looks like it's receiving specular light from directions the geometry would never actually allow.

The fix is to use the AO map to attenuate the specular result, multiplying the deepest range of the occlusion map against the specular values to dim or suppress reflections where they're least plausible. You don't need to apply this uniformly; a remapped range that only affects the heavily occluded end keeps the effect targeted. Ideally this is done as a post-process pass rather than per-material, so you pay the cost once rather than per shader.

Bent Normal

Specular Occlusion

More info regarding Specular Occlusion: https://dev.epicgames.com/documentation/unreal-engine/bent-normal-maps-in-unreal-engine?lang=en-US

Practical Takeaway

Bent normals are a baked asset, generated in the same pass as AO. The cost at runtime is minimal, you're replacing one texture lookup (geometric normal) with another (bent normal). In Unreal Engine, bent normals plug directly into the material and are used automatically for indirect lighting when present.

If you're already baking AO, baking bent normals alongside it costs almost nothing and noticeably improves how surfaces read under Image Based Lighting, particularly in areas with strong directional variation in the environment.

I did not discuss the other implementations that are available regarding realtime ambient occlusion ways of generating this information.

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

Day 18 - 🔬 Deep Dive - Linear Luminance

General / 26 May 2026

Light doesn't behave the way your eyes tell you it does. Understanding the difference is what separates renders that look lit from renders that look real.

What Is Luminance

Luminance is the amount of light emitted or reflected from a surface per unit area, in a given direction. It is a physical quantity, measured in candelas per square-meter (cd/m²), and it describes what is actually happening with the light, not how a person perceives it.

Brightness is different. Brightness is a perceptual response, how the visual system interprets incoming luminance. Two surfaces with the same luminance can look different in brightness depending on context, surrounding values, and adaptation state.

The distinction matters in rendering because the math works with luminance, but the artist is looking at perceived brightness. Confusing the two is one of the most common reasons lighting setups feel physically off even when the numbers seem right.

How the Eye Perceives Light

The human visual system responds to light logarithmically, not linearly. Doubling the physical luminance of a surface does not look like doubling its brightness. It looks like a modest step at the bright end and a much larger one at the dark end. The eye is far more sensitive to changes in shadow than in highlights.

This is why a candle in a dark room stands out, and the same candle in daylight is simply invisible. It's all about relative light perception. The eye continuously adapts its sensitivity based on the surrounding luminance range, a process called adaptation, which means absolute luminance values matter far less than relative ones.

It also explains why HDR capture and tone mapping exist. No display can reproduce the full luminance range a scene contains, so the pipeline has to compress it in a way that approximates how the eye would have adapted to that scene in reality.

Linear Light vs Perceptual Brightness

Working in linear light means the renderer operates on physically correct values. Energy accumulates correctly - two lights of equal intensity produce twice the luminance at a surface. Falloff follows the inverse square law. Materials reflect the proportionally correct amount of incoming light.

Working in perceptual space feels more intuitive because it matches what the eye sees, but it breaks the physics. Add two perceptual brightness values together and the result is wrong. Apply a falloff curve and the math no longer describes reality.

This is the foundation behind the linear workflow and the Gamma Correction discussed in Day 16. The renderer works in linear to keep the physics intact; the gamma encode at the end maps the result back to something the display and the eye can interpret correctly.


Luminance Ranges in Real Scenes

To understand why tone mapping is necessary, it helps to have a sense of the actual luminance range involved:

The full range the adapted eye can handle spans roughly 10 orders of magnitude. No display or file format captures this. A typical SDR monitor tops out around 100-200 cd/m²; a high-end HDR display might reach 1,000–2,000 cd/m². The sun disk is still a billion times brighter.

Tone mapping is therefore a compression problem, not a correction problem. The goal is not to reproduce the scene accurately - that is physically impossible - but to compress the luminance range into something a display can show while preserving the perceptual relationships the eye would have experienced in the original scene.

Conclusion

Knowing real-world luminance ranges gives you a reference point for setting exposure and tone mapping parameters. Instead of eyeballing until it looks right, you can ask: does my sky sit in a plausible range relative to my shadowed surfaces? Does my interior feel correct relative to the window behind it?

The logarithmic perception point is also practically useful: small numerical changes in dark areas will read as larger perceptual shifts than the same change in bright areas. If your shadow detail is disappearing or your darks feel crushed, the issue is often less about the values themselves and more about where they land relative to the eye's adapted range.

With all the things I am learning regarding luminance, I would love to delve further into tone mapping and measuring luminance myself with a lightmeter and experiment with my own library. I still have a lot to learn about Exposure and how it translates into the digital domain.

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

Day 6 - 🛠️ Behind the process - Approaching Material Creation

General / 07 May 2026

How I Approach Material Creation

How to approach material creation depends on whether it’s for personal work or a professional setting.

Personal vs. Professional

The goal shifts completely depending on context. For personal work, you’re trying to demonstrate your range and knowledge — you get to choose the subject matter, push yourself into unfamiliar territory, and show what you’re capable of. For professional work, the asset needs to serve the project first. If I’ve never worked on organic materials and want to explore them, a sci-fi corridor game is not the place to try it for the first time.

How Briefs Come In

I’ve worked from all kinds of starting points. Sometimes it’s a clear-cut piece of concept art with well-defined shapes, values, and a material breakdown already implied. Other times it’s a real-world reference that needs to be pushed toward the project’s visual language. The hardest and slowest is when the brief is open to interpretation. Multiple iterations, trying to translate someone else’s vision onto the canvas, until you land somewhere that feels right. If you can reduce ambiguity early, do it.

Starting Point: Alignment and Reference

On a professional project, my first step is always to align with the art director on their vision for the asset, prop, or environment piece. I don’t start building anything until that conversation has happened.

From there: gather a large pool of reference images. More than you think you need. Bring them to art direction and start culling: decide what stays, what goes, and what elements you’re specifically trying to hit. Reference isn’t decoration; it’s your contract with the team about what the final thing should feel like.

Art Direction

There is some unknown territory depending on whether you are the first artist on a project or joining one that already has a defined look. If you join an arena shooter that has existed for five years, you know that is the style you have to match - your job is to fit into an established system, not reinterpret it in your own way. Look-dev is mostly the time on a new project when a material artist and the art director will try to define the art style together and produce benchmark assets that set the example for the rest of production. In practice that means matching existing roughness ranges, staying within the established palette, and using the same base tiling rates as surrounding assets. The creative challenge shifts from "what should this look like" to "how do I get there within these rules."

For a personal project you are the art director, so you will have to figure out what look you're going for. That can be building toward a specific game company you'd love to work for, or simply setting yourself a new challenge.

I've never had to fully redo a material, but there have been moments where you have to reassess whether the direction you're heading is right for the intended look. A material can look very good in isolation, but in context it becomes too distracting. It might be a great portfolio piece on its own but it's also about striking a balance that best serves the project. That's not always easy.

Splitting It Up

Before diving in, it’s worth asking: is this one material, or a combination of materials and shading effects working together to produce the result? A wall might have a base plaster material, a damage decal layer, and a wetness mask, each is a separate concern. Getting that decomposition right early saves significant rework.

The choice usually comes down to this: a layered material approach gives you flexibility at runtime; think a slick oil layer on top of an asset that needs to shift over time, or a burn effect that sits on top of everything and is controlled by a placement mask. A single material is your typical export directly from Designer or Painter, but you need to account upfront for things like shader-driven tint, detail map blending, or any other parameters that need to be controlled at the engine level. Neither is always the right answer, it depends on how the asset will be used, how often it needs to vary, and what the shader budget allows.

The Actual Process

Set your texel density first. Figure out the tiling rate and set the real-world measurements in Substance Designer or Painter before you start building. This ensures that anything derived from your height map (normals, ambient occlusion, curvature) is physically grounded and PBR-compliant, and that your material sits consistently alongside everything else in the project at the same real-world scale.

Compartmentalize reusable features. If your project has rocks appearing across multiple surfaces — cliff faces, rubble piles, ground scatter; build a rock generator node once and share it across all materials that need it. The same goes for any recurring detail: pine needles, cracks, edge wear. You get visual consistency across the project and a single point to update when something needs to change.

Settle on a naming convention early. Your materials, resources, and shared nodes need to be findable: by you six months from now, and by whoever else is working in the same library. Whatever convention you pick, document it and stick to it.


Closing Thought

What works for one person doesn’t always translate to another’s workflow, so don’t be afraid to experiment and adapt. That said, on a professional project, you often don’t have the bandwidth to rethink your process mid-stream. Build the habits in personal work, so they’re second nature when it counts.

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

Day 4 - 💬 Take - Navigating the Creative Industry

General / 05 May 2026

Navigating the Creative Industry – My Experiences

The games industry is small. People talk. And yet, somehow, the recruitment process remains one of the least transparent parts of a career in this field.

This isn’t a guide on how to write a CV or ace an interview. It’s a collection of real experiences — things that actually happened, shared because I wish someone had told me this earlier. Whether you’re a newcomer or a seasoned professional looking for a change, I hope this saves you some time, frustration, and unnecessary self-doubt.

Studio Experiences

Studio A: Silence After Interest

A recruiter at a major triple-A studio reached out, the conversations went well, and then: nothing. I sent two or three follow-up emails. No response. Not even a “we’ve gone a different direction.”

It wasn’t just frustrating. It was unprofessional. In an industry this small, a recruiter is the face of the studio. Silence communicates more than they probably intend.

Takeaway: If a studio goes quiet after expressing interest, follow up once clearly, then move on. Their silence is information. And if you end up there later, you’ll remember how they treated candidates.

Studio B: The NDA Pressure

After multiple calls with another triple-A studio, they kept pushing for work samples I couldn’t share. The project hadn’t been announced. The work was under NDA; I told them this repeatedly. Their response: ”We could have a quick call so you can just briefly show it.”

Think about that. A company looking to hire trustworthy people, actively trying to get someone to breach a legal agreement. I’m certain they wouldn’t appreciate it if their own staff did the same.

Takeaway: A studio that pressures you to violate an NDA is showing you exactly how they operate. Walk away cleanly and without guilt.

Studio C: The Visa Oversight

Two interviews in. Good conversations. Then I had to chase the recruiter for a status update, only to find out they’d run into complications hiring me because of the visa process. Something they could have identified in the very first call.

Weeks of my time, and theirs, wasted on something that should have been a 5-minute check at the start.

Takeaway: Early in any process, ask directly: ”Are there any relocation, work authorisation, or visa constraints that could affect this role?” It protects everyone’s time. If a studio doesn’t have that answer upfront, that tells you something too.

Studio D: The Low-Ball Offer

A studio in a major European city extended an offer that barely covered basic living costs, let alone healthcare, in one of the most expensive cities in the world. I was more junior at the time, but the number was still disconnected from the reality of actually living there.

Takeaway: Always research cost of living against the offered salary before getting emotionally invested. Tools like Numbeo exist. Use them. Knowing your number going in puts you in a much stronger position and prevents the gut-punch of an offer that insults the city you’d be moving to.

Recruiter Experiences

Recruiter A: The Mentor Mirage

About ten or twelve years ago, I came across someone offering paid “mentor sessions” for newcomers to the industry. The pitch: guaranteed connections, 10+ years of recruitment experience, interview prep, resume polish, introductions to major studios.

When it came time to deliver on those introductions, they vanished.

It cost people money and, more importantly, time and trust during a vulnerable moment in their careers. To this day, I haven't seen this person deliver on the introductions they promised, and I haven't seen them working at any major studio since. In hindsight, there was probably more to the story than I knew at the time.

Takeaway: Vet anyone offering mentorship or career connections with the same rigour you’d apply to a job offer. Ask for specific examples. Ask who they’ve placed and where. Legitimate mentors welcome those questions.

Recruiter B: “It’s a Done Deal”

For a role at one of the largest studios in the US, the recruiter was enthusiastic to the point of being unprofessional. Phrases like ”it’s a done deal” and ”they’re lucky to have you in the process.” I went through the interviews. The team was great. Then I found out the job description didn’t actually match the role that was being filled.

Not only unprofessional; it’s a waste of the hiring team’s time and the candidate’s.

Takeaway: Treat recruiter enthusiasm as a yellow flag, not a green one. Ask direct, specific questions about the role’s responsibilities, team size, and reporting structure before investing in multiple interview rounds.

What I’ve Learned Overall

The recruitment process in games asks a lot of you: your time, your portfolio, your emotional energy. Not every studio or recruiter treats that investment with the respect it deserves.

A few things that have helped me:

  • Communicate your constraints early. Visa status, location, availability, salary expectations: raise these in the first conversation. It’s not impolite, it’s efficient.
  • Set your boundaries. It's okay to say no, or to step away from a process if something feels off.
  • Your NDA is not negotiable. Any studio worth working for will understand this immediately.
  • A recruiter’s enthusiasm is not an offer. Verify everything in writing.
  • Research the studio independently. Glassdoor, LinkedIn, people in your network who’ve worked there. The small industry works both ways; information travels.
  • Silence after engagement is an answer. Don’t chase indefinitely. One clear follow-up, then redirect your energy.

The industry is small. Your reputation matters, and so does theirs. Don’t be afraid to hold studios and recruiters to the same standard they hold you.

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

Refining Performant Levels of Detail (LOD)

General / 25 May 2025

Welcome back to the second part of our discussion on Level of Detail meshes! This blog post will be slightly shorter as it primarily builds upon the concepts introduced in the first post. In the first draft, we focused on calculating the LOD based on the diameter of an object. However, since the player in a game would see the mesh from various angles, it’s crucial to consider all viewpoints, not just one. 

Additionally, as someone pointed out in the comment section, this process assumes absolute values. To ensure your values are positive, simply wrap an Abs() function around them.

For instance, if we encounter assets from different angles, such as full frontal, full side view, or full top view, depending on their positioning and rotation by the artist, we shouldn’t only use one angle (the diagonal) for calculation. By taking into account all angles, we can give thin or flat objects a more realistic chance of being accurately measured. We collect all these values and calculate the mean as the input value. 

In hindsight, it might be better to use the maximum of all angles. If the difference between the smallest and largest number is too large, the LODs will fade out too quickly.

In an ideal setup, if the assets are placed and scaled, whether it’s larger or smaller, or if the field of view is wide or small, the game engine should compensate for these factors accordingly.


pseudo code


diagonal = ∛(0.075² + 0.157² + 0.167²) = 0.387m
front = ∛(0.075² + 0.157²)  = 0.312m
side = ∛(0.157² + 0.167²)  = 0.375m
top = ∛(0.075² + 0.167²)  = 0.322m


mean = ((diagonal + front + side + top) / 4)  = 0.349m


CalculateDistance(inSize=mean, inScreenSpacePercentage=100, inFovDegrees=45.0, inVerticalResolution=1080):
   ratio = inVerticalResolution / 1080 # compensate for 4k or FullHD screens
   # distance to see the asset, top to bottom
   distance_to_object = (inSize * 0.5) / (tan(inFovDegrees * 0.5))
   distance_by_screensize = distance_to_object * 100 / inScreenSpacePercentage
   max_distance = distance_by_screensize * ratio
   return max_distance


Horizon Forbidden West: Automated Asset Conversion And Updating

General / 06 March 2025

This postmortem analysis delves into my approach to swiftly converting a substantial amount of assets. Picture this scenario: when transitioning from Horizon Zero Dawn, the initial game, to its sequel, Horizon Forbidden West, we encountered the task of updating the game’s asset-art (props and environment-art models) to reflect the latest technological advancements and ensure compliance with the technical specifications. Moreover, we had to replace the assigned shaders with novel ones specifically designed for Horizon Forbidden West. The sequel would leverage existing assets while introducing new ones. Consequently, some of these scripts and pipelines were reusable. Given the sheer number of assets we wanted to display on screen, we prioritized optimizing their setup to stay within the memory budgets while maintaining the visual fidelity of the first game, supporting an approximately 7-year-old platform (PS4) and the (at the time) newly released PS5.

Step-by-step

  • Asset Management: Collected, tagged, and tracked over 5000 assets in a local database, highlighting the importance of testing and validating changes.
  • Texture Optimization: Optimized textures by discarding unnecessary maps, converting PSDs to PNGs, and ensuring consistency in materials like rock and stone.
  • Conversion Process: Utilized the Substance Automation Toolkit API and a shared Python library for texture conversion, ensuring PBR compliance and compatibility with the engine.
  • Maya: Executed Maya batch script, providing information and updating shaders, and relinking textures.
  • Export: Exported assets to the engine and linked to a test level for evaluation purposes.
  • Evaluating: Evaluated GPU performance, export/conversion issues, and internal tools/settings in-game.

I initiated the process by collecting all assets that required updating through a script. This step allowed me to assess the scope of the undertaking. I tagged and tracked each asset in my local database (a JSON file) with the appropriate process, the Maya file associated with that asset, all the textures linked to the asset, and some other data that I can’t quite remember right now.

Side note:

However, it’s important to mention that this approach wasn’t entirely foolproof and a one-size-fits-all solution. With over 5,000 files touched, all of which were linked to levels, sets, cinematics, or prefabs, the process became even more fragile and complicated, particularly during production when everyone was striving to complete the game. Coordinating the initiative poses a challenge, emphasizing the significance of allocating sufficient time for testing and validating the changes. To address this, it’s crucial to split the changes into smaller check-ins.

Let’s continue!

While updating the files, I took the liberty of optimizing content whenever possible. I discarded any Specular maps that weren’t necessary for dielectrics or any other unnecessary maps. Determining the need for dedicated Specular channels could be challenging, especially since not everything was authored using PBR techniques. Relying solely on Python libraries and image processing wasn’t always sufficient. Additionally, I converted all PSDs to PNGs to significantly enhance Perforce syncing times, image processing, and DDS exporting times.

In addition to texture optimization, we also had to identify assets that required recoloring treatment. For instance, if an asset contained rock or stone materials, we wanted to ensure that it (visually and color-wise) matched the other rocks by applying the same coloring treatment. In most cases, I was able to quickly generate a mask for other assets through a manual process. I could either extract it from the PSD layers, bake out the UV layout and use that, or simply mask in Photoshop and have that linked during the Substance file generation process.

Conversion

To begin the texture conversion process, I utilized the Substance Automation Toolkit API. Certain aspects of this process were defined in a shared Python library that I had written for it. This library is explained in more detail in another blog post titled “Texturing for Rocks.” This shared graph would clamp the Color values to be more PBR compliant, clamp the Roughness ranges due to the engine shading model, and run the AO through a Curve node to compensate for some manually authored AO that was too dark.

Why the Substance Automation Toolkit? Creating a new file using the API is a straightforward process. It’s easy to replicate, and we can open the Substance file in Designer and export it if necessary. Batch exporting is also an option if any changes are made to the shared Substance graph.

Maya

The next step was to update the textures linked in the engine and reexport the DDS’s. This was accomplished using the internally created library, which resulted in a relatively fast process.

Initially, I attempted to use a headless Maya version, but this approach did not work because the viewport renderer did not initialize the shaders and shader-defined information required for evaluation and update. As a slower alternative, I opted to run regular Maya in a batch process. In this process, I provided Maya with a list of information per Maya scene and updated the shaders. Upon loading, I retained the old variables in memory, updated the shader, and then updated the variables accordingly, compensating for any differences, such as variable names or ratios, like tiling for detail maps. Finally, I relinked the textures to the shader once more, ensuring they were correctly associated with the mesh.

From there, the assets were exported to the engine. Unfortunately, I cannot provide further details due to the proprietary nature of the engine.

Once the assets were exported, I was able to link them to my test level, enabling me to evaluate the performance (GPU) in-game and assess any issues that may have arisen during export or the conversion process. Before checking in, I conducted a quick test process to ensure everything was working as expected. I used our internal tools and double-checked if none of the previously set settings or draw distances were broken. The further into development you are, the more vital this becomes.

Conclusion

The way I present the steps in my explanation is also how the process functioned in Python. Evaluating images using PIL was the quickest step, while Maya posed the most significant challenge. Re-exporting assets from Maya was the slowest and most prone to crashes. Multiprocessing consumed so much memory that I experienced several blue screens. It was a valuable learning experience, as I learned how to automate certain steps and refine the Python code to be more compatible in different scenarios rather than a one-time use solution. However, it became apparent that full automation for this process was too risky due to the many odd cases and asset setups that had to be validated by eye. 

The ideations and iterations that emerged from these processes, served and continue to serve as the foundation for more automated and/or procedurally driven processes.

A special thanks to Chris Thompson for proofreading and to Guerrilla for allowing me to publish and share this information.

Writing technical documentation for production readiness

General / 03 February 2025

In this blog post, I’ll share my thoughts and expertise on the under appreciated yet crucial process of writing documentation within the industry. I’m referring to the broader implications of outsourcing assets and texturing pipelines, excluding the financial aspects since they’re not my area of expertise. This topic likely extends far beyond the scope of a single blog post, and I may delve deeper into it over time.

Objective of Documentation

Clear documentation is essential for internal teams, new members (including partners and outsourcing vendors), and project management. It serves as a comprehensive reference for project information and workflow, eliminating misunderstandings and inefficiencies that save time and money. No one wants a tool or system set up by someone who left the company without proper documentation, leaving them uncertain about its limitations, setup, and potential improvements.

The key to its success lies in regular updates. This ensures that outdated or obsolete information doesn’t accumulate, potentially leading to code rot. Documentation should be detailed enough to explain the main process but concise enough to be easily accessible when needed, such as onboarding a new developer or updating it.

Analysis

  • Ambiguity regarding the documentation’s intended purpose
  • Inconsistent terminology causes confusion
  • Repetitive information encourages skimming rather than thorough reading
  • Information scattered across various locations 
  • Is it sufficiently clear to non-native English speakers (depending on your team and your vendors)?

Iteration

The following points will undergo a continuous process of iteration and evaluation in collaboration with your collaborators and team members. The ever-changing landscape of tools, workflows, and tech needs us to keep adapting and improving our development process.

  • Thoroughly review all the documentation, following each documented step-by-step to ensure its accuracy and identify any missing information.
  • Conduct buddy checks to verify that everything makes sense from multiple perspectives.
  • Organize the content based on its complexity, considering that not all artists need or would be comfortable exploring more complex topics.
  • The information is available, but it is currently separated from the fundamental concepts.
  • Evaluate any workflow issues, blockers, or missing features in shared content, such as shaders or textures/material library.
  • Understand your target audience: Are you writing the documentation for a developer who is expected to have basic knowledge of a tool, software, or engine, or are you writing it for someone who has never used it before?

Lessons Learned

If I were to start fresh, I would improve the approach by adding more clear images, callouts, or even videos to reduce confusion, which is especially helpful for visual learners. Before starting the project, I would suggest creating a layout and showing a proof-of-concept for the potential setup. Furthermore, collaborating with different departments will help us delegate and coordinate specific documentation tasks.

Example

Texturing & Materials (Main Category)

---> Basic Workflow (Subcategory)

    -> Simple Material Blend Workflow

        -> Bespoke Texturing Workflow

    -> Advanced Workflow

        -> Height Blend Workflow

Conclusion

Be open to rewriting, re-editing, and redoing a substantial portion of the initially written documentation. Don’t be overly attached to what’s been written. With a well-designed revisioning system, you can always undo any changes or unintended problems. View this as an opportunity to reflect on your personal growth and improvements throughout the process.

Horizon Forbidden West: Batch Baking, Texturing, and Exporting Rocks

General / 22 January 2025

One of the most significant initiatives I’ve had the pleasure to be involved in during the development of Horizon Forbidden West was automating the rock asset baking pipeline. This process involved setting up a semi-automated (rock) texturing process, writing a Substance Python library (built on the Substance Automation Toolkit), and more.

Why was this approach significant?

  1. The game would feature a multitude of unique rocks, and all these rocks would need to be treated equally to ensure visual consistency.
  2. Any texturing (Substance) changes could be easily reapplied by batch reprocessing the Substance files only.
  3. There’s only one shared setting to bake all the rocks for visual and data consistency.
  4. My ‘database’ (read: JSON file) successfully tracked all texture resolutions, including those for baking and the in-game target resolution.

Database

The automated process aims to streamline the environment artist’s workflow. They only need to define parameters for naming textures in a small database, JSON file, specifying which component should be packed onto a single sheet, using a custom cage mesh, and setting the target resolution for in-game use. The tool will automatically adjust other parameters during the first run.

Context

In this context, a “set” or “component set” refers to a collection of multiple assets that need to be combined onto a single texture sheet. For instance, during the initial setup of the automated system, an artist would have to bake three rocks as individual assets. They would then have to manually merge all the mesh data to reduce the amount of draw calls and achieve the desired target texel density. However, through the automated pipeline, I was able to merge all their bakes together and apply the Substance graph to the final result. The only requirement from the Environment artist was to perform proper UV mapping beforehand and ensure a well-organized layout.

For each rock asset, we had to bake out various data, including Normals, Ambient Occlusion, Curvature, World Space Normals, UV mask, World Space Direction, and two others. If these were baked at a high resolution of 4k, it would significantly impact the performance of Substance. To address this, I combined all the mesh bakes for each type of rock into a single call. For instance, four different rocks would have their own AO maps, which I merged into a single sheet (automatically). That combined result would be exported as PNGs and integrated into the Substance graph for texturing.

With this automated setup and tracking of assets, we could easily rebake, retexture, and reexport all the rocks if needed. This wasn’t often required, as the environment team did an exceptional job of providing high-quality bakes. However, having the option to verify all in-game content ensures that it’s always up-to-date, and is a great feature.

Texturing

For the rocks, we utilized a shared Substance graph to input all the baked mesh data. This enabled us to generate breakups and variations on a per-asset basis. For generic rock details, we employed tri-planar projections that were appropriately scaled to their real-world scale. For instance, cliffs had a larger repeating rate compared to pebbles. We also incorporated other types of variation and breakups, such as occlusion and curvature, in conjunction with cloud or Perlin noise. However, the process is more intricate and detailed than I can fully explain here.

Most of the assets’ uniqueness stems from the World-Data that is hand-painted by the artists and designers or generated offline. In combination with the addition of shader features like Medium and High Frequency detail maps, even this is defined by parameters such as the asset’s location in the world.


CLI

I had developed a command-line interface (CLI) in Python that allows artists to easily drop and drag their low-poly mesh onto a .batch file. This file offers a couple of options:

  1. Transferring high-poly to low-poly data for single assets
  2. Transferring high-poly to low-poly data for all assets within the set
  3. Only reprocessing the texturing pipeline
  4. Exporting the assets into the game
  5. Performing all of the above options

Problems and solutions

  1. Transferring detailed information from high-poly to low-poly models using the Substance API was a bit of a challenge. Certain assets were too complex and caused memory issues because of the way I had set up the queue. After testing and iterating, I re-examined my threading setup. Upon further exploration of the Substance API, I realized that I could load in the meshes once and bake out all maps simultaneously, rather than doing it individually. This change eliminated the need to load the high and low poly meshes into memory for each thread(!). Looking back, it seems obvious, but it was all part of the learning process, adaptation, and continuous improvement as a technical artist.
  2. Writing the internal Substance Python functions was a bit of a challenge. Learning a new API while simultaneously writing a tool that is somewhat production-ready is not an easy task, but it’s a valuable experience! You get direct input from your end-users, and being able to troubleshoot with your team at someone’s desk together is a valuable asset. In addition to writing the internal Substance functions, it was also used to batch process and texture other assets, such as the face textures in Horizon Forbidden West.
  3. None of the pipelines were finalized yet. The Substance graph for the rocks was still in its early stages of development, but that’s part of the iterative process and trusting the workflow we envisioned.

Next Steps

I began developing a graphical user interface (GUI) around the tool to manage rock bakes and provide artists with the flexibility to set custom parameters. However, this endeavor required a solid foundation of code and a well-defined roadmap outlining the necessary features, based on the feedback I had collected from the team.

Scaling up the process to ensure visual consistency across all assets by using the same bake settings would streamline our workflow. Currently, we manually send out bake settings to our partners, which can lead to errors. This tool would eliminate these potential errors, making the process more efficient.

A special thanks to Chris Thompson for proofreading and to Guerrilla for allowing me to publish and share this information.