You guys for coming to enthuse about ray tracing along the way, ponder the problems that we've attempted to tackle, and problems that the industry as a whole will attempt to continue to tackle. My name is Natalya Tatarchuk and I lead Unity's graphics group. Now, this presentation is focused on the lessons that we've learned throughout the process of doing a production-oriented approach for integrating ray tracing into our rasterization pipeline and I'll talk about that from the perspective of the engine lessons that we've learned as well as some of the things that are related to content creation. We'll talk a little bit about the pitfalls that we've explored, questions that we did encounter and may not have solved that I want you guys to help. And also some of the thinking about where we go from there. There are two major contributors as along me to the presentation, Sebastian Lagarde and Anis Bennaceur from Unity Technologies. They will be
here for questions at the end. However, everything I'm presenting here today has been a real team effort and a lot of people contributed to the algorithms and systems that I'll be covering. I'm here to represent. But before we dive into the details of the technical presentation, I want to briefly share the project we've actually used to drive it, pun intended. Invent yourself and then reinvent yourself. Don't just swim in the same slough. Invent yourself and then reinvent yourself and stay out of the clutches of mediocrity. Invent yourself and then reinvent yourself. Change your tone and shape so often that they can never categorize you. Reinvigorate yourself and accept what
is. But only on the terms that you have invented. Be self-taught. And reinvent your life because you must. It is your life. And its history and the present belong only to you. So, what did you just see? Uh, this is actually the demo that we're going to dive here throughout all of the presentation and I've lost the control of the screen. Always exciting. Drivers will solve ray tracing, will solve all sorts of complex problems about cloud gaming and so forth, but presentation screens and driver control will remain to be a challenge. Um, call to action. Okay, so the demo actually is at the heart of um the outline of all we'll be covering. We'll
talk about the rasterization pipeline. We'll talk about integrating the ray tracing into our um high definition uh rendering pipeline. Some of the effects that we used for this particular demo. We'll talk about performance and some of the um sort of thoughts about where DXR may want to go. But what did you just see? Uh, what we've done in this particular project is we've filmed a real world car on location and recreated this scene with we captured the lighting environment, the scene itself using the exact camera and lighting conditions, and then using the official product design uh model with all its crazy geometry, we then recreated the CG vehicle to match the film. And then we transition the shots in the video from real world car, like what you see on the left here uh to the ray traced car uh seamlessly. And in the film we rendered this garden interactive rates on RTX 2018. Sorry, 2080 Ti with a lot of different
ray tracing effects, refraction, area lights, shadows, ambient occlusion and so forth. And that's what we'll be covering. And here is example of some of the more complex visuals going on in the demo with a complex transparency and refraction. And you're seeing a comparison against the real world filmed version. The car was quite complex. It was very dense geometry and of course it's actually the vehicle that's used for production. But what the reason we actually use this aside from well rendering pretty cars is always a good looking result is its density is actually quite representative of a lot of real world scenes. There's a ton of silly meshes, very dense geometry, lots of draw calls, lots of different materials. While it's not identical to let's say you know, an open world forest scene, it still has quite a lot of moving parts. And there are moving parts. So first let's take a brief look at our high definition render pipeline because that's the basis for our approach.
It is a physically based rendering pipeline. We use unified lighting. What that means is the same lighting features apply to opaque, transparent, volumetric elements in all of the content in the frame. We use coherent lighting throughout. All of the light types have to work with all of the materials as well as with all of the options of global illumination, light maps, light probes and so forth. This is super important for content creation. And as you see throughout this presentation we strive to maintain the same principle for our ray tracing approach as well. And while certainly you know, we we think of ray tracing as a panacea that will solve all of our rendering problems, there's some really interesting challenges with maintaining this unified approach. And in HD in particular, we also worked hard to avoid double lighting and double occlusion. Now, one of the important properties for our implementation, which may not be true for a, you know, specific game, is
we need to be able to do either forward for VR applications for example, or deferred approach. And again, when we were looking at ray tracing, we needed to think about how that's going to affect uh the particular set of effect um choices. With our HD pipeline, we have classification of materials. They get output to G-buffer. Uh so, standard opaque, subsurface scattering, clear coat, iridescence, and anisotropic. They're all stored um in the G-buffer with a, you know, a bit. Now, some materials in our deferred path are so complex or need additional information. For example, we want to store specular tint that we need to still render them as forward. Of course, our favorite transparents, but then beyond that, we have a number of organic materials, skin, hair, fabric. Um uh sorry, not skin, eyes. Uh they still need to be rendered with forward. And again, that that has some interesting implications for being able to do that in a rasterization pipeline. The ray tracing pipeline that we built
for HDRP is actually built directly on that on top of that pipeline. We plugged all the low-level uh layers in. And in this presentation, I'm actually not going to talk about how we integrated DXR to the low-level renderer. That's kind of a topic of a separate uh presentation. We did um the layer and that we exposed to C#, so in in Unity, you can actually access much of the rendering uh pipeline control through C# and shaders. And so, after that, majority of the work on the ray tracing effect happened on that level. Uh which of course meant that we can iterate really quickly uh on the effects once the architecture was established. And just a quick thing, I'll constantly be saying ray trace effect. Really, it just means uh you know, a bucket of uh container for anything that goes on to render a particular ray trace algorithm, be it ambient occlusion or shadows and so forth. So, in our case, again, I mentioned that we need to be really flexible with regards to forward and deferred uh support because we want ray tracing to be used regardless of the particular
directionality. And so, for uh for ray tracing, we start by um shooting secondary rays from depth buffer uh in normal buffer. For opaque, of course, this is straightforward. We can use G-buffer as a starting point. Now, with forward objects, we use a pre-pass and we use that already for screen space reflection uh to output depth and normal during the pre-pass. And that's the information we can start secondary rays from. So, in some ways, uh since we already have a good implementation of SSR, it actually needs the same data as ray tracing for a lot of the materials, as you'll see. Of course, the next thing that we know is we'll never be able to shoot thousands and tens of thousands of rays that you'll see in a Weta Digital Manuka or similar offline um you know, renderers in order to maintain performance. And this theme is repeated throughout anybody who's talking about real-time ray tracing. Reducing noise through denoising and temporal accumulation is key to achieve frame rate. So, we mentioned we're going to start from uh
uh for ray trace from um primary rays from camera, shoot it in screen space. This is basically to obtain visibility and I'll discuss where we use it. And um for secondary rays, we start from the first intersection with the primary rays. And that's the G-buffer uh or pre-pass approach. In reality, every ray has to be thought about quite thoroughly. It's quite expensive to implement it, especially as we get to ray tracing. Now, of course, as we shoot more rays, the resulting quality is higher. Um the simple truth is if you don't shoot a sufficient number of rays, you'll end up with a terrible-looking um huge degradation of quality, terrible looking effect. Now, we supplement that with denoiser to keep the rates uh fast. However, one important thing about denoising is we need to denoise every single effect because they actually have different qualities. Uh and we need to make sure that we account that. And so, we're kind of in this sweet spot of trade-off between the quality of the actual denoiser, the cost it will take
to run it, versus the cost that it will take to run to run more rays, evaluation of rays being the expensive part. Now, we could choose to go to a high-quality denoiser. Nvidia has a very sophisticated denoiser available that uses light positions, specular roughness. It needs more information. So, one, you'll pay the cost to output this information. Two, you'll store it. Three, you'll sample it. Four, the actual algorithm of the denoiser is heavier. And of course, that might in our case, when we evaluated it, dominated the cost of uh ray execution. So, all of the savings we did by not shooting more rays were gone. Um and so, that ended up not being in uh a win for us. So, our approach is launch minimal amount of rays, then reduce variance with good sampling. And we'll talk about some of the techniques there. Accumulate temporally because that allows us to um you know, get more contributions of rays. And then the end result of that goes through the denoiser. It's very straightforward uh to understand the cost of rays. Uh it varies linearly with length. Uh we
actually did uh a number of performance profiling to um determine that. And for the live demo that you will see um you know, in a few uh minutes here, we actually use one ray. Uh for example, uh for regular effects, for some of the more expensive uh effects like reflection, we use quarter uh resolution. And then for AO and shadows, we end up using four rays. Now, of course, this is a you know, sweet spot of perf versus quality. And so, with respect to denoising, like I mentioned, we have to do it per effect, so we went with a very inexpensive approach that gives us good quality. Now, um I want to say, so the way that we are approaching that is uh we do it in temporal, and then we use a joint bilateral filtering. With regards to temporal, just to clarify, it's not a TA filter, very similar to the approach that Tomáš Skowron is uh um describing last year's Digital Dragons presentation. It's only inspired by TA. And so, what we do is we run one sample
per effect, which is accumulated over frames, and then we run um eight sample TAA. So, we accumulate eight frames histories worth. Uh and that's actually what we are doing to kind of supplement the temporal. We're not trying with this element to remove aliasing. We're simply trying to accumulate pixels uh to match history in the material properties. And at the end of the frame, like I said, we have the property. Uh for each effect, we need to um run essentially the custom history per effect. So, this is where um the uh specificity about each effect comes into place. We actually store uh history buffer per effect, one set per EO, one set per um area shadow lights. It gives us better stability. Of course, that comes at the cost of additional memory, uh as we need to add memory for the history for accumulation. Uh but, that memory really allows us to reduce um number of rays, thus increasing performance. So, it's kind of
a trade-off. We use joint bilateral filter throughout, um and in this case, it's using normal and uh depth as opposed to lumens luminance that you'd see with a bilateral filter. It's a nice and inexpensive filter implemented as a separable computator filter. Uh and normals and depth allow us to solve sort out the discontinuity issues. And as I said, we we output them either through G-buffer or pre-pass. Um and so, that allows us to take advantage of opaque and forward materials. Now, the fun part is, well, the permutation of materials that we've mentioned early on, transparencies have been a pain and suffering for people who are doing rasterizations for decades, right? Well, I'm sorry to say, but that doesn't quite go away with ray tracing. Uh, of course, if you care about performance. In this case, since we don't have depth, we actually can't really use anything for that in this particular case we're not outputting. We'll talk about how we sort of transparency in a subsequent path.
With regard to the denoiser, we provide artists ability to control the filter. For example, the demo the video that you saw of the demo that we rendered for the matching of the real car, they were using quite large filters. Of course, that means that, you know, the cost is higher. And the content creators can basically control the cost of quality versus trade-off, and we intend to make it scalable based on you know, essentially platform configurations. And that allows us to really amp up the quality on a per effect level aside from just per platform level. So, if you wanted to really spend more time you know, samples in a particular algorithm, you can. Now, with simple bilateral denoiser, stability of results is very highly dependent on the actual qualities of the samples that feed into the denoiser, which is why sampling is crucial in order to generate a good frame. When you have good samples, your denoiser result will be much more stable. And Matt Far in last year's 2018 Advances talk gave a really wonderful presentation on the
intuition of, you know, some of the approaches that you need to think about to achieve low variance. This is important to do before you apply the denoiser. So, next we're going to touch on sampling. How do we approach reduction of variance? So, ultimately, everything is related. Everything that you're, uh know, in the limit surface essentially, we take infinite number of samples without any denoiser. The signal converges and our result is unbiased. That's of course the holy grail. Uh but as we imagine, it's quite not real time. That's the you know, hours of frames in the offline renderer. So, we apply denoiser, congratulations. Well, that introduces bias. And so, the better we actually you know, stratify our sampling, the better the end result will be with regards to reducing bias. And so, with even though we actually might pay more cost in the way that we're doing selection of rays, it's more than offset by the resulting quality improvement for the rays.
And it's the same principle as multiple important samplings, where you have essentially a lobe and you bias the sampling direction toward that lobe, uh converting the texture to probability distribution for that lobe representation, and we sample from that sequence and then convert it to PDF, um we actually execute similar approach uh for all of our ray launches per effect. So, to hedge your bets, pun intended, we use the following sampling strategy. We rely on Owen scrambled sequence, which is actually described in Siggraph uh 2003 talk on Monte Carlo techniques. And essentially, Sobol sequence is a really widely used low discrepancy sequence for numerical solving of multiple integrals, which is why of course it's uh relevant to Monte Carlo. And a lot of the quasi Monte Carlo computations rely on using this sequence and then scrambling it through permutation that
allowed us to to him originally to maintain the low discrepancy. Now, each extra sample increases the coverage in an optimal way and thus allows sampling to be temporally coherent. And next frame continues to build a sequence, and this is actually the sequence that you see that we do in time. Each pixel uses identical sequence. We just store it in a texture and we use a screen space blue noise to break up the pattern. It ends up being coherent in screen space and here is the example kind of the pseudo code. We generate a ray direction using X sample in one direction. That's the first row that you see to get one sample and then we use the second row in Y direction for the second sample. And this allows us to use two dimensions everywhere except with a special case of area light where we use actually three dimensions. We accumulate up to eight frames and inside one effect we can actually go up to 32 samples because of our current implementation stores a 256 by 256
square texture. But we also can play with changing the dimensions to basically go with a lot more samples by doing a 2 by 10 to 24 so that you can actually end up with 128 samples if you wanted to so kind of your value will matter. And that can mention scrambling happens with blue noise. We begin by grabbing the blue noise with the current pixel position from precomputed texture. It gives us the XY noise. Then for each coordinate we sample the own scrambled dimension texture like I've described scrambling the resulting blue noise. And then the scramble of the blue noise analysis to reduce the pattern repetition. Then the blue noise is essentially allowing us to ease the filtering of the denoiser since it provides a very common filter for denoising. And so this allows us to yield a temporally coherent sequence with a number of samples and so it accumulates in a stable manner. So if we look at you know now we've covered kind of like we do noise, we
figure out good sampling and then we reduce you know the resulting bias. Now how do we approach ray trace effects? The general architecture is quite straightforward. So if we look at DXR we have a number of different shaders, ray generation, closest hit, any hit, miss, and so forth. And then there's a surface shader. In a surface shader, there's a couple of options. Of course, people get really hyped up about ray tracing, and I implemented my own power ray tracing all age myself, you know, every one of you implemented it probably, too. Um it was awesome. We got transparencies, reflections, textures. We felt really good. Uh and yeah, sure, it solves everything. Real production content is a tad different from them spheres. Uh you know, you have immensely varied material types, and accounting for surface shader that I was just showing you is a must. However, we now need to deal with the vast permutations of material types, opaque, uh opaque alpha tested, so you see lots of that here, transparent, eyes, and so
forth, double-sided, and many more. As we go through our effects on ray tracing, keep these in mind because we're actually trying to solve it for a production that you know, isn't limited to opaque spheres. So, a good way to think about ray tracing effects as ambient occlusion. It's kind of like the hello world of ray tracing, so to say, which is why everybody starts it. And when we say hello world, we really mean ambient occlusion for opaque, non-alpha tested objects, right? Um we trace the ray from G-buffer position in the direction of the normal for opaque, non-alpha tested objects from the, you know, very straightforward. And then secondary rays used to compute the ambient occlusion. We pick the number of samples, that's just a parameter in a shader. Sample the hemisphere cosine, this is the standard approach, and I forgot to include the proper reference from Matt Far amazing book. Simply requires a random number profile, you know, provided to again to feed that the noiser, and we use the own sequence. So, in terms of um the hit, it's
actually very straight straight forward. We know if we hit something, for opaque surfaces, this means we actually don't to con- continue. It's already occluded. It's purely a visibility computation. And so, we can just use any hit to evaluate this visibility. Simple path. And shaders are just for you for reference for later. We we actually don't need to spend time on them. Now, for AO, we need to choose distance. If you choose infinite, that's your sky um occlusion computation, right? Small distance is more like the regular AO, in some ways approaching the uh sort of SSAO-ish type of uh hemisphere distance. Now, the distance drives the path performance ultimately, and so that's your optimization. Longer rays are far more expensive, as you need to traverse more BVH. The cost of ray, like I mentioned, was linear. And so, um we need to think really, you know, thoroughly about the distance that we provide. And then the denoiser actually correlates to cost as well, because it's uh dependent on the size of the kernel.
Cost is linear based on the size of the kernel. So, why do we even bother with AO? Cuz I thought you said, you know, I said earlier we're going to do shadows, right? That should technically take care of all of the occlusion issues. Um if we have strong environment lighting, and that's the example of HDRI skies, right? Either HDRI skies or light probes, um global shadows will show you the sort of crease details, and of course the major movements, but you'll lose a lot of the information from far away. Because, guess what? We didn't pick the infinite distance, right? Like we're already constrained to some particular uh area of the scene. And that theme gets repeated throughout, because you know, as we optimize performance, we walk away from ground truth, and then we need to make up for the lack of ground truth, and this is the example. So, AO can really be a good supplement for the areas where you miss the um occlusion for the larger elements of the scene. For comparison, this is screen space uh ambient occlusion in um HDRP on the car um asset. And here you see
the ray traced occlusion. Now, note some of the over-darkening in the images. That's That's our friends, the transparents. Uh and we'll touch on that in a second. But, of course, you can see as I kind of switch back and forth how much more accurate occlusion uh results in terms of uh ambient occlusion. So, yes, transparents. What if we want to have more than just opaque? Okay, double-sided material is actually not too bad. We pass the flag to the API, uh and that basically disable calling in the acceleration structure on the DXR side, and we're good. Okay, that was kind of easy. Um transparents, right? Like we thought it was going to be totally doable. The problem is in in ray-tracing path, it's still quite a bit of a pain. Uh there's not really um actually an easy way to uh do this that's performant quite yet in a real-time because we need to account for um accumulation of light, and we need to account for rough refraction effects. If the object is 50% transparent, we need,
for example, for AO, we need to accumulate 50% of AO, and then continue to keep accumulating because some of the light propagates through the media. And if the object is rough, we have to actually then account for the redistribution for, you know, the This is the example of the spread um here. And of course, perf hit for doing this many rays is actually not We're not quite there in terms of hardware um to be able to do this at real-time. So, we need to special case transparents. As I mentioned, alpha testing is crucial for real-time production. Now, artists actually author alpha tested as just, you know, subgraph in the shader graph. We don't know what they're going to evaluate or what crazy, you know, surface elements they're going to pull in. And so, we need to add support for alpha tested objects in a generic way that allows us to sample this material model that is not in any manner of hardcoded. At ray-trace, what we do is we evaluate the sub graph from the um alpha tested
evaluation. That's what you see here. And uh that evaluation increases the cost of the effects, right? Because we now need to add it to the any hit surface shader. And all of the effects like shadows, AO, etc. need to support it in order to render it correctly. So, the hit processing needs to account for alpha's threshold. And what this means is all of the effects need to actually plan to account alpha testing holistically, um which of course increases the complexity of the effects. And here is the example. You can kind of see the little um uh foliage element and it's doing uh proper alpha testing for reflections in the mirror as well as uh shadows and and and so forth. Now, we are this is the example of double uh sided geometry. So, we handled ambient occlusion ex- examples, so we handled visibility, good. Uh we handled some surface property alpha tested double sided. So, let's kind of think about the next build of the effects, the opaque uh the um
uh shadows. And reflection. So, if we think about reflection, this is the contribution of indirect specular. The effect starts bringing in a lot of the complexity for production pipelines. We typically want to bring them uh to replace screen space reflections because there is a lot of artifacts and it's a very challenging effect to do, but then we need to think about all the material limitation. There is another thing that we need to think about it from perspective of engine architecture, which is um well, we're really really accustomed to doing visibility on our frustums, right? Like it's fantastic. My view, I declare it, I run the visibility, be it occlusion calling or whatever. I'm really proud of myself. Um yeah, that doesn't work anymore because now the object that I'm going to be reflected may be outside of my cluster uh visibility, and that visibility is actually not accurate for the um particular uh situation. So, that's that actually brings some of the complexity and we need to think about essentially using a cluster volume
around the camera for calling. What we also need to think about is store the data in the voxel around that camera because we need to bring the lighting data, uh the reflection probes, the planar reflections, decals, all of the payloads that we typically associate with a given view into the new structure because our visibility representation and results of that changed. And in terms of um lighting, we also do have to consider the same thing, right? Like when we're computing a reflection, we need to compute the lighting of the reflected object, which means we need to account for the light that's outside. And so, um we need to ensure that we also evaluate the same identical light loop and I'll touch on that in a second uh in regardless of whether we're doing the primary, this is our material lighting evaluation, or we're doing secondary, this is our, you know, reflection accumulation, for example. And so, for indirect specular, what do we do? For every bounce, we evaluate lighting and we reuse the same code as
the actual regular light loop, except for reflection hierarchy. Now, ideally, uh depending on LOD, you actually may want to use a simplified material for reflection uh for you know, improved performance cuz evaluating the full stack of the material for multiple bounce can get really costly. Or you also may need to account for reflecting complex material, so anisotropic subsurface scattering. This is the light loop evaluation from HDRP uh that we use in rasterization. And so, we start from the sun kind of like top-down. We evaluate all directional lights at the top, then we evaluate all the punctual lights, spotlight, point light, and all of the area lights, so lamps and so forth. We add um sunlight, sky cloud, the reflection probes, HDRI, and we uh finalize the sky evaluation. We do a very similar evaluation on ray tracing. The only thing that's different is now we'll replace the reflection probe with the actual ray traced reflections. And so if I kind of go back and forth,
you can see that that you know sphere disappeared. And for performance reasons, we actually evaluate sky in the same manner as we do with rasterization by using pre-integrated cube maps. So this is pre-integrated AGRI, and there's some snags on that one. For game oriented ray tracing scenarios, when we can we render out the full frame path tracer, we'd actually do the proper computation of the HDRI in order to do this in a much more accurate manner. So as I touched on that, we need to worry about visibility with regards to ray tracing. So primary we're still good, we're still in the view frustum, so visibility is good there. Secondary and beyond, we really are in a free fall. Like this is where we start getting into production specific solutions, right? Camera orientation, because you cost will basically will you know vary both with regards of how big your acceleration structure needs to be, and how long it's going to take to traverse it.
And we need to account this on the engine front. With ray tracing, it's in global space, so we define a region around the camera. And evaluate the lighting environment globally for all of the elements that are outside. So we talked about the fact that you actually have to change the low-level architecture on that front. And there's some flexibility that you need to think about how you're going to structure again. That can be an artist specific effect, or it can be a level of detail control, right? Like depending on the complexity of the situation, you may want to scale down the the region where you're going to be evaluating your global effects. As it all affects the performance. And this is the rough outline of what you would actually need to be considering when you're building the froxel the the voxel around your camera with all of the payloads. So, this is an example of evaluating the lights representation. It's basically accumulation of lights hitting a particular area. Um and in terms of parallel to the
rasterization, this is kind of like we have the probe as the same parallel parallel concept. Interesting bit about decals with ray tracing cuz we're we're again we're doing a production focused system and while artists love using decals, they allow them a really quick and and customization to material properties of the surface. And decals is a really interesting case for reflections, right? How do we enable the contribution of decals in the correct manner? When do we determine, you know, are they visible? Are they not visible? Do they still reflect? And how would they actually work with ray tracing? So, what we did with rasterization, so if you look at um Sebastian's uh Siggraph 2018 presentation from last year's advances course, he defines that we basically maintain a froxel around the camera where we keep the payload for decals. And so we can kind of think of the thing that we kept in the froxel for rasterization becomes the voxel representation for ray tracing world. In
the rasterization world, we store our decals in a spatial structure around the frost froxel. Now, in ray tracing, we now put it in that camera aligned voxel. It's still used for partitioning, right? So, we still can do cluster decal implementation for acceleration, but it's not centered around the camera. It's a spatial representation. And that essentially allows us to then find the appropriate decal and now we need to think about how to evaluate it. And of course, artists evaluate properties from the shader graph. In HDRP, we apply decals whether they if they're opaque by just rendering them as the D buffer decals. And for transparent decals, we do cluster approach. This actually modifies the appear the appearance of the objects directly in the G buffer for deferred or peaked and alternatively, of course, in our forward pass, it will modify it as part of the shader evaluation.
With ray tracing, we basically can follow the same route. Everything goes through the cluster decals and ray tracing. So, the D buffer decals are gone. We evaluate the material properties again evoking the shader graph functions. We applied the resulting decal to the material by modifying it, stored in a similar manner, and then evaluate the light loop to make sure that we're computing the proper lighting. Again, the only changes we're operating on voxel. The only tricky thing is we cannot support extrusion and decals in this route. And it's not even clear what do you do with regards to acceleration structure intersections in the ray tracing. So, this is something that we need to be mindful when building content that's designed for ray tracing and rasterization because they're not interchangeable. The other thing that we want to think about is anisotropy, right? And and in general, just thinking about reflection with different BRDFs. So, at each intersection, we need to consider in the reflection pass the
direction of the ray needs to account for needs to be actually be driven by important sampling. And the reason we want to do this again is to reduce the variance. So, we have to make sure that it follows the distribution of material. Now, this is an example of an isotropic, right? And you see its reflection in the subsequent object. And we need to know the actual and you know, NDF of the material in order to create the distribution direction. Thus, we need to know the material properties. At the same time, if we want to know the BRDF, well, that means two things in a fun bit of, you know, current API design. You either need to have some sort of uber shader because, well, we don't have subroutines, or we need to figure out some alternative way to get at the material information for the underlying system. And like I mentioned, we did store a lot of the material IDs in G-buffer, so congratulations to us, but forward materials, we don't really have a way to classify. This is an
example of a lot of the, you know, fabric, hair, and so forth. So, what we do is we import and sample with as an approximation of the BRDF by using GGX, and this is the example here. That actually gets you pretty far. You can represent a reasonable approximation of anisotropy uh because we're really only picking the sampling direction, right? And it's a reasonable approximation of the NDF. However, with clearcoat, we need to account for it by using roughness in the clear, and this is the example here. And so, this is the result kind of of the clearcoat and its reflection. And similarly for specular color. So, we need to ensure that we're evaluating the um light loop like we were saying for the specular. And what we do is during the trace ray, we evaluate the material, we apply our decal, uh you know, as we were discussing, and then we do the full light loop for the decal as well in order to get the accurate uh quality of
the final lighting for that particular object. And what this means is allows us to have the surface shader at the appropriate, you know, sort of uh point for faster performance. And you'll have all of this stuff for for reference later on. Now, you have to be careful about another aspect which comes along with, you know, real production-centric uh worlds. You need to mind your cascade shadows because remember that we're not going to evaluate everything in the infinite distance. Well, we can't afford it. So, if you have a giant view, you might still want to rely on cascade shadows. And they will encompass a region around the camera. And so, now you have to deal with the merge of the cascade shadows around your camera and your glorious perfect reflection ray traced shadows. With cascade shadows of view dependent, and so if you reflect outside the boundary of cascades, you actually won't even be picking up the right shadows from that super giant object that that is coming in outside. And so, you really need to be kind of
paying attention to that. So, what also happens if you have a metallic object inside a room full of mirrors? Right? Multiple balance are very expensive to evaluate. And how do you account for more than second balance in a performant way? In our case, we ensure that you you know, obviously if you have something like a reflection of a knob that needs to reflect another metallic object, you'll want to blend multiple times. In general, we want to evaluate reflection from one bounce because of the cost of the reflection shader. But, in some cases, we can actually think about falling back to rasterization. And how did we approach in rasterization? For metallic object, we can use specular term as diffuse, right? So, we basically use Fresnel zero in our implementation as essentially diffuse Fresnel objects. For second bounces, we fall back to Fresnel zero, right? Which gives you the sort of the color of the object. And if you don't do the you actually will over darken the
object. It gives you the black. And this is the example, maybe hard to see, but that that knob that you see on the panel actually is the example of bringing in that tint for the metallic object to blend appropriate darkening. So, as we mentioned, reflection is a very expensive path path. We need to optimize it. So, we default to using quarter resolution for speed. This is very similar to what EA seed has done. And what we do is we select one of the four samples per frame, evaluate the lighting, store the resulting index, sample direction, do the probability distribution function, and then up sample with all of the BRDF information for filtering. So, this is accounting for variance, roughness, and so forth. Then it follows the same temporal accumulation and bilateral disjoint based on the radius of the filter that we specified. And so, here is the example of roughness, normal. We we use the full resolution results, and I'll do a little back and forth. So, you can kind of see what happens
with quarter res. In reality, if you're moving reasonably fast, not too bad, and performance is pretty good in terms of getting the the benefits for that. Okay, great. Done with indirect specular. Let's talk about shadows. Rasterization, of course, is a really well-known and fast approach for when we evaluate you know, shadows. We just do a simple shadow map. With ray trace, we actually can do a lot of these approaches as well, and evaluate rasterized shadow maps during reflections, because that's faster. It's an optimization for that particular pass. Because you may not need the actual precise glorious ray trace shadows. The recursion can murder your performance. So, for shadows, we have the full screen buffer for the shadow if you want high resolution. Or if your light is very soft, you actually can get away with less. Um and this kind of like experiment based on your particular production screen. It is required to be full screen for area lights and for sunlight.
We use ray traced screen space shadow map for sun. This is the disc light. And note that this again breaks for transparency, because if you go through the transparency, you need to handle the opacity correctly. The object is tinted in a color shader, which is, of course, expensive to evaluate in that pass. And in general, I mean it's extremely expensive to evaluate to begin with. Um if you consider again the the rough refraction, so even in offline it's not really to support it. And for reflection, we use a combination of screen space shadow shadow maps, so that's the sun area lights, as well as shadow map for punctual and area lights. Next, area light shadows. Uh of course, they're quite uh intensive to evaluate. We can switch between rasterize implementation of area lights or between retrace per light. We also can do this dynamically, so we allow a lot of control based on LOD or based on artist uh preference. In future, we'll also experiment with importance driven techniques to kind of
see if we can do this in a more automatic manner. Um when selecting rasterization, we use Eric Heitz's linear transform cosine approach uh plus exponential shadow maps for area lights. And um you want to make sure that you use a combination like I've described before to combine it in a safe way manner. So, retrace area lights are actually correct as the glorious ground truth, but quite expensive because you need to launch a lot of rays to cover the area, especially for those giant area lights. In the video, you probably saw this enormous textured screen, which is one enormous um area light. And so, as an optimization, we can evaluate visibility independently, which is essentially a shadow map uh separately from direct lighting. And what we do, it's a very similar principle to rasterization. Uh we render visibility to shadow map using uh exponential shadow maps, and then apply lighting. And um of course, the regular shadow maps are incorrect in this case because
they're not accounting for BRDF for the material, but again, you're thinking about performance and limitation, so it it is a reasonable uh compromise. You uh in our case, we fall back to the approach described in, um, Eric Heitz's, uh, presentation, well, Morgan, Eric, and, uh, Stephen Hill's, um, presentation from last year's SIGGRAPH, which allows for better presentation for separate visibility that actually takes into account material and is a great reference with a lot of code and detail. And then, as I mentioned, we evaluated our lighting with LTC. So, this is the example of a naive implementation for computing area light shadow without the LTC approach that we've just mentioned. And notice that it's, you know, it's not really representative of the complexity of the vehicle. So, I'll go back and forth between the approach from Eric's, uh, LTC implementation. Pretty close to ground truth. So, that complexity in evaluation actually really matters in regards to quality.
For texture area light shadow, we require a full screen space buffer. We pack it for efficiency. Now, we actually need to, uh, have access to full material properties in order to evaluate BRDF. So, in our case, we restricted to the G-buffer uh, renderer. Uh, we approximate the standard materials for performance. So, of course, you know, all of the special effects of anisotropy, uh, etc., fell out, but it's okay for for the current for performance reason. And, uh, essentially, along with the direct light evaluation. So, what we do, uh, we need to then look at optimization for area light shadows. We launch rate and evaluate every samples. So, the shadow that we're doing is denoise, so we can't actually approximate the BRDF. And like I said, if we choose the standard material, we end up with a fairly reasonable approximation uh, that gives a good quality without sacrifice to the final shadow quality, um, shadow experience.
How we pack this texture is we use an RGBA FP16 screen space buffer, and we packed three components. Essentially, the first component, and this is described in more depth in Eric's representation. We packed illuminance of direct lighting. So, we launch in our case a sample directly not using linear transfer cosine. Then we combine the um illuminance with testing visibility. So, this essentially brings in the shadow term. And then the final term we packed is the full evaluation of LTC. Invite you to read the paper as it's really, really great. We need to for texture your lights in order to get good quality and stability, denoise each term separately. And so, this is required in order to get stability, but of course it means you have to separate the signal and run two passes. Uh we for efficiency of implementation did it in a single pass in compute for performance, which is much better. Uh and we also use
the U, the combined LTC evaluation result as a parameter for guiding the denoiser in order to essentially in a similar manner that we would use depth and normal in a the other set of effects to be able to deal with discontinuities. And then we divide the illuminance by the SN in order to update the final shadow result. Now, one of the issues with area lights of course is they're quite large and the samples can go in a wide range of directions. So, we need to think about coherency of rays in order to obtain good performance. And it really is one of the biggest cost for this particular effect. Uh as you can imagine, the caches get quite trashed in the process. So, the biggest win for area light evaluation is to think about how we can improve coherency of evaluation. In our case, what we do is well, actually let's look at the naive approach, right? So, we want to do four samples for the area light and we shoot them in basically foreign and
directions. No guarantees of coherency whatsoever. God knows what point in the cache you will hit. As you can imagine, it gets quite poor performance. The alternative is to split the evaluation into four passes. And each ray per pass will use an approximately similar direction, which allows us to to get much better coherency for the execution of that pass, resulting in much better performance despite the the non-obvious fact that we're going for four passes. And this is actually an example of the result in pics evaluated on our RTX 2080 Ti at 1080p. And so the shadow for area light, this is that giant, you know, 2/3 of the screen area light, executes at about 4.8 milliseconds. But if we do the non-obvious four pass, you know, separation, you actually save almost 2 milliseconds, which is quite quite significant. Transparency and ray tracing. Transparent are always problematic. Not possible to denoise, no G-buffer.
I thought it was supposed to be a glorious promise. We also need to render from primary rays, so it's expensive to evaluate. So, with regards to handling them correctly, what we need to think about is we need to handle transmission, and we need to handle rough refraction. Of course, this is expensive even in offline world. First thing we're thinking about is when you're hitting a transparent object, you need to launch a ray, right? So, we need to render primary visibility with ray tracing because we don't have a G-buffer, right? Like we don't have the starting information. So, the very first you've just realized is yay path tracer, right? And what about performance? When hitting that object, we need to think about how we're going to manage path tracer primary evaluation versus rasterization cuz we also said we don't want to do primary ray for all of the opaque objects, right? So, what we do is we allow essentially switching per object per effect of sending the primary ray either through rasterization, so differ G buffer or whatever flavor of
the day forward etc., or send it through ray tracing. And so here is the example of scene where that's actually really needed because you'll see a lot of you know complex transparency happening on the gear shift. Primary ray objects are rendered in a depth pre-pass. We output a mask for every element of the transparent object that we use in order to discard rays that shouldn't be actually used for primary ray rendering. This is our optimization, right? And furthermore, we also provide a parameter for primary visibility ray to the artist to control max recursion. And this is related to the refraction effects. And what we do then is we launch rays in screen space from near plane in the direction of the far plane, right? Like for the visibility competition. And so illustrating kind of like the the approach for max depth, what we do is we retrieve the index of refraction and calculate the refraction vector. We calculate the refraction, we launch rays, store the results of that
refraction, continue onward, calculate the reflection, launch rays, store the results of that reflection, and in each point we evaluate light loops, so reflection value, refraction value, and so forth, and then we accumulate the final result and return. And the light loop has to take all of this into account, the results of reflection, so every ray that I've sent through, results of the two refract passes until we essentially reach the max depth. So quite a lot of complexity, so these of course you know, should be used with wisdom and and sense. Um So that is actually what allows us to support multiple reflection and refraction for transparent objects. As I've mentioned, we only support smooth reflection and refraction that whole scattering of the rough refraction good next year's GDC. Um quite expensive and there is a really good um good analysis from Colin's presentation uh on the subject from last year's Siggraph uh
intro to uh ray tracing course. So, here is an example of uh I think nine level uh passes. So, we're kind of going through multiple bounces accumulate accumulating result uh for the final refracted and reflected. Now, note that this doesn't support total internal um reflection. On Fresnel. So, we evaluate when we evaluate light loop, we actually fall back to a lot of the rasterization evaluation. So, we evaluate Schlick Fresnel. And Fresnel is evaluated at every bounce pass. This The reason this doesn't account for total internal reflection is because well, Schlick Fresnel is just an approximation. And it's only really good when IRR is between 1.4 and 1.7. So, you know, quite limited in its representation. True Fresnel is something that we use for the full, you know, expensive glorious frame path tracer. But for uh our pipeline with a more game oriented uh rendering for the live, we actually can't represent it in the PBR. So, we
pre-integrate the cube map uh you know, based on the fact you remember the HDRI cube map that I mentioned in the light evaluation, we pre-integrated based on the fact that it's Schlick. And that's the reason why I mentioned earlier that if we you know, if we're doing the full path tracer, we need to do proper evaluation because that's where we need to actually go back to the Fresnel in order to handle the total internal reflection. Of course, that will come with significantly more performance cost. And uh when quality is more important than performance, fantastic, uh go wild, use the true Fresnel. And when you're in a more performant route and you really need to think about, you know, dynamic elements of the scene, we actually one of the benefits is we get performance, but the other benefits is you also match more of the look to your uh evaluation from the rasterization pass. So, one set of assets is also something that's worth thinking about. So, this is the example of the total internal reflection. In fact, that you know, we're basically saying we're
punting for this. So, in our case, you know, actually sorry, it's a little bit repeated. So, we mentioned that we want to have ray tracing or rasterization. What we've enabled is we allow actually switch between full ray trace, so primary visibility or rasterization either placed on camera. It can be used for rendering that specific view entirely, you know, AO with ray tracing, etc. Or you can actually have a view that's defaulting to SSAO. So, a lot of flexibility in that regard. The area light is something that's switchable back and forth based on either the object or distance. And similarly, you can also mix primary ray objects beta pico transparent. So, it actually doesn't really quite matter with regular, you know, deferred and and more standard pipeline objects. Of course, ray tracing path, you know, for anything that you've selected is more performance intensive. And so, you need to think about how you're going to
fall back in rasterization when outside the region of the camera. And we talked about how we actually need to rethink about how we're approaching camera invisibility. What we do is we fall back to ray trace reflection in our reflection hierarchy. So, we Look, here's the example. Basically, this is your frustum view and the areas where you define rasterization fallbacks, you can kind of see we fall back to SSR further away. And we define the area where we're still computing global reflections around kind of like proximity of the camera. Again, driven by your quality and performance needs. And then further away, you start falling back to more and more and more LOD approaches for SSR and similar approaches or even drop out that and if you need to. So, when we use primary visibility ray rendering, it's very similar to opaque objects. Uh they're rendered in depth pre-pass. I'll put a mask like I said. If the transparent object is not registered in the acceleration structure, it actually doesn't have
ability to appear in our reflection. And so, what we do is we treat the primary visibility object as an opaque object for intents and purposes of that. And what that means is the primary ray transparent can see other object and you don't actually don't need to require any compositing. But that means we're back to sorting issues. Because we don't intend to render, for example, particles into an uh you know, a giant acceleration structure in full glory, we're not going to have to deal with where is this oh sorry, you know, opaque primary ray sorry, primary ray transparent object with respect to everything else in the scene when we're actually rendering the transparent part. So, there's some things that we need to think about for transparents going forward in order to make sure that it's really robust. The next thing I wanted to hit is indirect diffuse and I realize I'm a little bit short on time, so we might go through this a little faster. The very simple thing that we've decided to do with indirect diffuse is replace sampling of essentially precomputed in
lighting illumination such as light probe or light map with either a brute for force approach where we do a ground truth approximation. Basically, do the full cosine integration of the scene and denoise. That allows us to compute really high quality but really expensive indirect diffuse. And this is the example of light map that's been baked and the resulted ray traced indirect diffuse which is actually fully dynamic. Now, the approach, you know, brute force approach is very close to ground truth, so it's fantastic for more expensive frames. And in fact, the the video of the car that I showed you was using that approach. But we can also look for the cheaper alternative. And what we do there is we generate light probes on the fly, actually in a similar manner as we do offline when we bake light probes. We store them in a 3D texture, and this can be of course amortized over multiple frames, and then we combine the resulting indirect diffuse with ambient occlusion.
And so a light probe group, and this is the example of a light probe probe group that's set up in a car, it's super easy to set up, right? And it's just used solely for the purposes of rendering into that information for indirect diffuse. You can also ensure that to put more of them in a regions where you want more fidelity. Now, you can use a different acceleration structure for the light probes in order to have a faster computation. So we actually use proxy geometry in order to speed up the uh indirect computation. And we encode the light probes by using a stage uh second order coefficients. It allows us to capture low frequency diffuse, so fantastic. And we can get away with this, you know, proxy lot crew geometry because it is really just, you know, uh low frequency diffuse. And in our case, we trace about a thousand rays for each light probes. Each ray traverses the acceleration structure that's using that generated proxy uh geometry. And when the triangle is hit, we evaluate lighting at that point, and then convolve the resulting color into the corresponding SH
coefficients. And so again, this is actually the same approach that we used to generate offline beacons, just, you know, less probes and and slightly different geometry. And uh we use a compute shader to resample the SH probes coefficients into the volume texture, and uh the closest four probes for each voxel in the volume texture ends up being rendered um and interpolated uh around the particular voxel. Now, the final volume texture is set on an LPB component, right? In the custom volume texture, and then every single object can actually sample from that particular element, evaluating the SH coefficients, and then computing the final GI value. So, this is the example of how the SH coefficient texture looks when we go through all the layers. And as you note, it consists of nine coefficients for each RGB channel, but we also can back them a little bit more efficient into the seven RGB channels to an atlas of a 3D texture.
So, of course, there's a ton of performance consideration. For the sake of time, I'm going to skip them. The main thing that we want to say is around 27 probes accumulation is about 3 ms on 2080 Ti evaluation, and there's a bunch of optimizations for, you know, a brute-force approach of optimizing all of them. However, if we use essentially more SIMD evaluation for compute cores, we can split the evaluation of a single probe into series of samples, so that a single ray invocation can do a subset of all of, you know, we said that 100, 1,024 samples. And so, we actually parallelize execution for within a probe for every single ray. And when we do that, with the resulting parallelization, we were able to cut down the GPU cost to 0.5 ms for about 40 pro- probes, and that's what we use in the live demo. So, one note on acceleration structure. I already mentioned that we're using cheaper geometry. Now, there's some fun
bits about the API with regards to LOD, but to work around those fun bits, we essentially allow an acceleration structure unique per effect. And so, you can use it to drive completely different geometry. So, for, you know, GI, we're using proxies. For reflection, you might want to use a higher fidelity pass. It allows us to separate LODs. It allows us to separate scene representation. And we give this flexibility both on the effect and on the you know, specific. I have a point to touch on the for the designers of the APIs on that, but this is kind of a workaround in that. And we also allow artists to flag objects to say whether they go to a particular acceleration structure or not. It's essentially is done with just a bit fill for each ray trace effect per volume. And we can have unique configurations per effect. And what it does is actually can allow us to create an acceleration structure that's dedicated to a particular effect. What that means is like for example for these headlights, we have a really complex
effect, lots of bounce and refraction and so forth. So we've made an acceleration structure that only encompasses this particular headlamp. By again that exclusion route. And so yay, they're fantastically really high quality. We can do more bounces. Whereas if we were to encompass the entirety of the world, well, we know how that's going to end. Similarly, we also support light layers, which is something that our artists feel really keenly fond of for any type of light with ray tracing. And it's actually the same way that we support it for rasterization. Same light representation means same content. And then they can go to appropriate evaluation. Must keep a little bit on evaluation of light layers. One important thing that we were focused on is how do you represent the shaders for ray tracing with a shader graph? Because of course we want to enable artists for maximum freedom. And you can kind of think about ray trace shaders as a flavor of a sub graph that's encompassed for a specific LOD. And each effects would then have a
separate shader table because of course you need to ensure you're actually compiling and linking and able to evaluate that. And with shader graph we can bring a ship separate shader sub graph per effect. This is also going to be used to be able to switch based on distance as well as effect. So a couple of things about the demo Um, show you in a second. On 2080 Ti it was running at about 60 FPS. I'll run it on my laptop. We'll see how it does with running for an hour. Um, apparently I live on the edge. This car is about 28 million polygons uh with tons of effects. We did do a pass and this is something that you need to mind with ray tracing for sure. Uh, as there are a lot more uh parameters set, you need to think about optimizing meshes to avoid uh you know, to basically do merging and so forth of uh smart things for the draw calls. It was a little bit of an insane geometry, right? And this is sort of the level of silliness because this was a product design car. They modeled the actual numbers for the text and the panel sub, you
know, uh this is like uh the the radio part. Silly, but it has representation for some of the complexities that we'll see see in a good level. I I think 28 mil with a ton of details is actually fairly decent workload representing this on next gen games. And so, this is just the example of the density of the geometry. And the performance numbers, I'll I'll let you kind of peruse them later on. Um, captured in 208 uh Ti for this wide shot that you've just seen. You know, as you can see the reflections is actually, you know, about 5 milliseconds is where we spend our being uh quite quite extensively. And then the ray tracing itself is only 14 milliseconds. So, it's actually not bad despite the fact that it's uh heavy polygonal model and an enormous textured light. So, let's live on the edge and um see what how this will work. And of course, if we're going to get kicked out of the room, you're you can come over and check it out on the live
laptop. And so, this is that wide area light that you were seeing and sort of thing I can show you the comparison to the ray traced effects uh versus the screen space reflections. And uh you know, you can start seeing some of the fidelity that comes in um with that effect. The primary visibility ray is actually a lot better of noticeable in the interior. So, if we go to this region, the um gear shift box, and pardon my car terminology knowledge, cuz I'll probably name something incorrectly, has a really complex transparent and refractive uh object. If I turn the visibility ray, you can kind of start seeing and in fact, if I go a little bit sideways, you can start seeing the loss of quality of being able to get the internal effects within that object from the bounces that are happening. And if we look at uh reflections, again, that's something that we can kind of start seeing all of the dynamic elements of them.
Okay, I'm being hurried on. It's okay. Thank you guys for not displaying extreme impatience. Albeit, the conference is less tolerant. Okay. All right. So, a couple of small performance observations. I have about maybe 5 minutes, and then I'll finish my rant. Um we noticed the surprisingly payload had really small impact. We did a lot of tests, uh you know, varying 256 versus 1024 payload changes. It was negligible in terms of performance, and we actually found the same feedback from Microsoft and Nvidia when discussing. Cost was roughly linear with the numbers of rays. And of course, as I mentioned earlier, ray count was king, right? Uh it's also super important to get rid of as much work as possible from the ray trace shader, splitting work between compute and ray trace. So, doing pre-pass is a good thing to do for performance reasons. Um on our side, what are we doing next?
We're going to support particles, and it's going to be fun. We're going to discover a lot of interesting challenges. We're also going to support more sophisticated GPU animations. Um you saw some of the animations in the car, but the really fun bits exist when artists do vertex animations in the shader graph. Lots of interesting challenges there with regards to BVHs and and update of acceleration structures. And of course, even more optimizations. Um, so for the 5-minute rant, this is sort of if there are any IHVs, people, or people who talk to IHVs and or APIs, I wanted to mention a couple of things that are worth thinking about with regards to current API and or state of algorithms. So, with regards to denoising, we said we're going to denoising um per effect. We chose a cheap denoiser, more of them per area light, etc. Congratulations, profit runs fast, we're good. It's still not super well understood what is the actual intuition of when you want to choose, let's say, denoiser per frame versus a denoiser per effect. I
would love to see more uh you know, deeper research in that domain of understanding of how the final denoiser Okay, matches. Maybe I'll run that outside of the door. So, a couple of things I'll just manage them mention them. We have global acceleration structures and global reflection tables. How will this work with streaming? How will this work with LODs? How will this work with shaders that need to be compiled iteratively? How will this work with all of the dynamic objects that we want to procedurally create? Lots of challenges there. How will they work with GPU updates? I can't update the BVH structure on the GPU. So, there's a lot of deep thinking that needs to occur on that front before we can really start thinking about truly integrating this in the proper way. Then of course, we need to think about ray tracing and texture cache coherency. The main thing I want to mention there is we think we understood texture streaming, right? Like we can predict when the objects needs to stream in pretty well. With ray tracing, guess what? You're out of luck.
You don't know when the texture needs to be streamed in. It's out outside of your camera view. So, So, some really good thinking that I think is going to happen in the next 3 to 5 years and how we combine the generality of our dynamic worlds, open world streaming, and so forth with the lockdown of the black box always loaded and compiled at runtime BVHs that are not in our control. So, in general that that's a pretty big challenge. And mesh shaders and and ray tracing and anything that does GPU generation and compute shaders or GPU-driven pipeline, how do we do, you know, BVH updates in that case? This remains a really fascinating field. I'm sorry. One little thing. DDX DDYs, they were our friends. We knew them so well. Well, with ray tracing this is a problem. We kind of worked around it. There's an NVIDIA paper. I I advise you to look it up on mip map LOD. You can always fall back on mip zero. That's okay.
Um and then of course support for beams. So, I told you we're doing a bunch of stuff to primary ray race. Congratulations to us. How do we accelerate it? We want to do beams through the accelerator acceleration structure. We know their coherency not supported through the API. End of rant. So, there were a lot of people who contributed to this demo. Couple more minutes. They need to be named. They are fantastic. They did really hard work and the results are amazing. And this is both artists, engineers, and so forth. Um one small plug. We're hiring. If you want to solve all the hard problems I just mentioned, come join us. We're across the world, probably where you are we are at too. So, end of rant. Thank you for your patience. And if there are questions, I'll be at the end of the um talk.
Auto-generated captions from YouTube — names and numbers may be misheard.
Distilling the key insights…
Game Developers Conference · 2026
Jonas Tyroller · 2025
Comments