[{"content":"During my time at HITLab Howest, we were asked if it would be possible to create buttons that had no physical shape or feel. They were needed for a test to see if operations for blind children had a positive impact. Those children had to react to a visual stimulation/light but they needed a way to track correct or false interactions. Physical buttons were not allowed since the test subjects bypass their vision by feeling the buttons.\nAfter researching different solutions and devices we managed to create a working prototype using a projection beamer and the Microsoft Azure Kinect. At first, we initialize the depth of the surface and calibrate using anchor points and bounding boxes. Those boxes keep track of their initial depth values and each frame the current depth value is compared. If a bounding box is filled with a certain difference in depth, we mark this box as pressed down.\nWhen the depth returns to the initial value, the button is then marked as released and a ButtonPressed event is then called.\nBy making sure the difference in depth is below a certain threshold, we can know the distance between the surface (initial depth) and the hand of the patient.\nYour browser cannot play this video. Download video.\n","externalUrl":null,"permalink":"/portfolio/virtualbuttons/","section":"Portfolio","summary":"Touchless, shapeless buttons for a study with blind children at HITLab Howest. Built with a projector and Azure Kinect depth sensor.","title":"Buttons using a Microsoft Azure Kinect","type":"portfolio"},{"content":"Volumetric light effects are caused by light being scattered in a humid or dusty environment. It’s a beautiful looking natural phenomenon also known as ‘god rays’. To challenge myself, I tried implementing this effect in real-time in the Unity game engine based on the same technique used in Killzone: Shadow Fall from Guerrilla Games.\nTechnique overview # For each light, create a custom render target (half or quarter resolution for better performance). Render a shape that represents the light’s volume. For point lights we render a sphere and for spot lights a cone. Directional lights are rendered with a fullscreen quad as the volume covers the entire scene. At every pixel, calculate a line segment of the view ray that intersects with the light’s volume. Raymarch along this line segment and sample multiple light contributions. A dither effect is used to offset the ray origin. In the post processing stage, we add the volumetric light render target to the light buffer. References # GPU Pro 5 - Volumetric Light Effects in Killzone: Shadow Fall by Nathas Vos\n","externalUrl":null,"permalink":"/projects/volumetric_lighting/","section":"Projects","summary":"Volumetric light effects are caused by light being scattered in a humid or dusty environment. It’s a beautiful looking natural phenomenon also known as ‘god rays’.","title":"Volumetric Lighting","type":"projects"},{"content":" One of the most useful features in a shader can be texture blending. The most common example of this is terrain shaders. Those are generally controlled by splatmaps, where each channel tells where a certain texture should be visible.\nAnother way of controlling the visibility of textures is by using the vertex colors. This is very cheap, but result in a lower quality blending if the mesh or terrain has a limited amount of vertices.\nIn the sections below you will learn other types of blending to achieve a high quality of blending with a generally cheap performance cost.\nDefault Blending # The first way of blending is using the basic lerp function, also called linear interpolation.\nThe function requires 3 inputs, value A, value b and value t where a and b are both colors and t is the value that controls the blending.\nFor example, if the value of t is 0, then the result will be a. When t=1 then you will get b as the result. All other values for t that lie between 0 and 1, will be interpolated accordingly.\nYou can see that the result does not look very good to the point it looks even unrealistic.\nfloat3 DefaultBlending(float3 textureA, float3 textureB, float blendValue) { return lerp(textureA, textureB, blendvalue); } Height Based Blending # Let’s now look at a different type of blending. Instead of blending using a single value, we use the height map of the 2 textures. By comparing those two height maps of each texture and result the one with the higher value.\nfloat3 HeightMapBlending(float3 textureA, float heightA, float3 textureB, float heightB) { if(heightA \u0026gt; heightB) { return textureA; } else { return textureB; } } You can see that the ground now blend nicely between the cracks of the cobblestone. But by looking at the code you can see that we use an if statement.\nIn general those are not bad in shaders if used to compare to a constant, but here we compare two dynamic values (heightA and heightB) so both sides of the if-else statement will end up running. Another problem with this method is the sharp contrast between the two textures.\nIt would be nice if we could blend smoothly between them. The linear blending from left to right is also gone so we need to get that back as well.\nAdvanced Height Based Blending # Let’s first remove the if statement. If we look at both of the height values, and we know that they are normalized, subtracting them will give us a hint which texture has a higher value.\nIf the result is \u0026gt; 0 then texture A should be shown, if the result is lower than 0 then texture B should be shown. By using the step function we can set the result to 1 if it is higher than 0 otherwise we get a 0.\nNow let’s reimplement the smooth blending. Whenever you need a smooth value in a shader the smoothstep function comes in mind. This is the same as the previous step function only giving a smooth value between the upper and lower limit. In the previous step function we used 0 as lower limit but let’s change that into a variable, this way we can control the blending more freely. But what should we use as the upper limit? Let’s add a low value to the lower limit and use that result as the upper limit. That low value should be a variable as well so you can control the smoothing itself.\nThe final step is that the blending from left to right should be back as well. We know that the final result of the subtraction resulted in a value between 0-1, we could multiply that by a mask or other value. In this case I use the red channel of the vertex color.\nWe now have a nice smooth blend from left to right with the dirt filling up the cracks of the cobblestone.\nfloat3 AdvancedHeightmapBlending(float3 textureA, float heightA, float3 textureB, float heightB, float blendFactor, float sharpness, float3 vertexColor) { float heightDifference = (heightA - heightB) * vertexColor.r; float blending = smoothstep(blendFactor + sharpness, blendFactor, heightDifference); return lerp(textureA, textureB, blending); } References # The Technical Art of Uncharted 4\nPhysically based wet surfaces\nTexture assets are from: Poly Haven\n","externalUrl":null,"permalink":"/projects/heightmap_blending/","section":"Projects","summary":"Texture blending techniques for shaders based on height textures for a more realistic result.","title":"Heightmap Blending","type":"projects"},{"content":"Since physically based rendering (PBR) is now a standard in most games we try to simulate better and more lighting models that cover more material types. With the classic lighting model, everybody chose to darken the diffuse term and boost the specular term. With the method explained below you can provide a way to simulate wet surfaces while keeping your material physically accurate.\nWe can easily distinguish between a dry and wet surface. The best visual cue is that wet surfaces look darker, have a higher specular value, and exhibit subtle changes in saturation. This behavior is made by rough/porous materials (brick, clay, concrete, plaster\u0026hellip;), Absorbent materials (cotton, fabric), and organic materials like hair. Materials that are smooth don\u0026rsquo;t change however since they are already reflecting a lot of light. Darkening The Albedo # When looking at all the visual cues of wet materials, we can see that the surface looks darker. This depends on how porous the material is and how much light is absorbed. Another thing we can see is that brighter diffuse values stay bright but everything else darkens. At first, it seems reasonable to multiply to diffuse factor with a scalar value depending on the wetness. The problem with this method is that bright diffuse colors lose more color than supposed to and the materials appear rather black instead of darker.\nA better way to darken the diffuse is to square it. This makes the brighter AND the darker colors lose less value while others at around 50% will be adjusted more accurately. Below you can see a graph that shows the difference between a squared diffuse and a diffuse multiplied with a low value (0.35 here).\nThe difference between the normal diffuse (left), the diffuse multiplied with a low value (middle), and the squared diffuse (right) Input Properties # To keep the input intuitive for artists, we use as few material properties as possible. To control the wetness on the material itself we use a single greyscale texture that the artists can paint to keep visual consistency, physical correctness and still keep things intuitive.\nWe also use a single wetness float to scale the mask so we can dynamically change it via gameplay. Below you can find a visualization of multiple wetness masks.\nTip An easy way to create a wetness map is to start with an inverted height map and apply some contrast to it!\nExample materials with their wetness texture Property Type Description Wetness Float This drives the transition between the different phases and also acts as a scalar value of the wetness mask Wetness Mask Greyscale Texture This controls the material wetness Tip Since we want porous materials to darken when they\u0026rsquo;re wet, we could give this as a second property to the artist. However, since the porosity is based on the materials roughness value we can use the roughness map as input for this\nThe Shader Code # Here’s the shader code. The artists have control of the wetness parameter whereas the porosity parameter is the material’s roughness value. Wetness drives the transition and the porosity darkens the base color. To darken the material we lerp to a squared version of the base color. This is a reasonable approximation of the darkening and more accurate if we would multiply the base color with a low grayscale value.\nWe also force the vertex normal to point straight up of the wetness value gets above 98%. This convinces the player that the surface of the pool is flat like water.\nfloat Remap(float input, float minimum, float maximum) { return saturate((input - minimum) / (maximum - minimum)); } float3 albedoSqr = albedo * albedo; albedo = lerp(albedo, albedoSqr, Remap(wetness, 0.0, 0.35) * porosity); roughness = lerp(roughness, 0.1, Remap(wetness, 0.2, 1.0)); ao = lerp(ao, 1.0, Remap(wetness, 0.45, 0.95)); normalTS = lerp(normalTS, float3(0.5, 0.5, 1), Remap(wetness, 0.45, 0.95)); vertexNormalWS = lerp(vertexNormalWS, float3(0, 1, 0), Remap(wetness, 0.98, 1.0)); References # The Technical Art of Uncharted 4\nPhysically based wet surfaces\nTexture assets are from: Poly Haven\n","externalUrl":null,"permalink":"/projects/realistic_material_wetness/","section":"Projects","summary":"Physically accurate wet-surface shading for PBR. Covers why wet materials look darker and shinier, and how to reproduce it in a shader.","title":"Realistic Wet Materials","type":"projects"},{"content":"Almost every material in the real world is slightly translucent, this means that light enters the material and is scattered around and then exits the surface. In computer graphics, this is mostly done with ray tracing. This, however, is a very expensive computation to use in real-time so other methods have to be used. I have updated the Unity’s standard shader based on Dice’s implementation which presents a fast and scalable approximation of translucency for a convincing subsurface scattering look.\n# Technique Details # To simulate the basic light transport inside an object we begin by using distance-attenuated regular diffuse lighting, combined with the distance-attenuated dot product of the view vector and an inverted light vector. However, this does not take the thickness into account. To properly compute the light transport inside the shape we could use a local thickness map, since they allow us to compute the distance traveled by the light from the light source through the shape and to the pixel. This map uses dark values where the object is opaque and bright values for translucent.\nfloat3 ambient; // Ambient value that is visible from all angles float power; // Power value for direct translucency float normalDistortion; // Shifts the inverted light vector towards the surface normal float thickness; // Precomputed local thickness map. Used for both direct and indirect translucency float scale; // defines how much light will go through with the back translucency float3 lightDirWS = lightDirWS + normalWS * normalDistortion; float VoL = pow(saturate(dot(viewDirWS, -lightDirWS)), power) * scale; float3 translucency = lightAttenuation * (VoL + ambient) * thickness; outColor.rgb += albedo * _LightColor * translucency; Computing Local Thickness Maps # The workflow of creating a local thickness map relies on a normal inverted computation of Ambient Occlusion (AO). Since AO determines how much light arrives at a surface point, we can use this information for the inside of the shape. Simply inverting the AO map gives different results than the workflow explained below.\nThe difference between thickness map (left), ambient occlusion (middle) and the inverted ambient occlusion (right) Flip the normals and make sure your mesh is properly UV-unwrapped. Set the Dark color to the maximum value you want for translucency (white - max, black - min). Set the Bright color to the minimum value you want for translucency. The Max Distance is the distance traveled by the rays. If you have a big object, make the distance larger. The opposite for a small object. References # Approximating Translucency for a Fast, Cheap and Convincing Subsurface Scattering Look\nCreating Thickness maps in 3ds max\n","externalUrl":null,"permalink":"/projects/subsurface_scattering/","section":"Projects","summary":"Almost every material in the real world is slightly translucent, this means that light enters the material and is scattered around and then exits the surface.","title":"Subsurface Scattering","type":"projects"},{"content":"After years of working in tooling and shaders, I put myself to the challenge of finding out if it\u0026rsquo;s possible to place thousands of objects using the GPU. Because GPU-driven rendering is the industry standard nowadays, and after having written one myself, placing objects by hand can be cumbersome.\nArbor is a GPU-driven object placement system for Unity, designed to place thousands of assets across large regions. It is fast enough to use in real-time and artists can use multiple tools like the Graph system, Paint tools and spline masks to control the placement.\nYour browser cannot play this video. Download video.\nA full region generates all items in about 0.5ms (or ~2000 fps). Because the entire pipeline runs on the GPU, no CPU stalls are present.\nWhile editing, only regions that are changed are updated once per frame, keeping everything as smooth as possible. The problem # One of the biggest problems in Unity is scattering assets across large scenes. After a while problems like serialization, lag or slow-downs arise, making editing the scene a bad experience. In previous tests, I tried generating points using Poisson-Disc sampling and then placing Prefabs in the scene using rules like height, shore distance or randomized scales. This works well but after ~15 000 assets, serializing all the data stalled the editor for a couple of seconds. The spawned prefabs and the points could get out of sync if merge conflicts happened, requiring manual fixing. One of my main goals was making sure that artists and designers could use existing tools with minimal setup to change whole regions. Changing a single scale or spawn rule should not require them to wait for a few ms when the whole scene was being updated.\nThat\u0026rsquo;s when I got the idea to move the whole system to the GPU. The previous solution already output transform matrices for a prefab, making it actually fit perfectly for instanced rendering.\nHow it works # Everything is based on a graph system generating different density textures for different layers. The node graph combines different masks, textures and procedural inputs into a density texture that drives what is placed where.\nFor artists and designers, there are different masking tools that can be used.\nA Node graph tool to combine different types of masks and textures. A spline mask tool can turn splines from the scene view into a texture using a width-controlled tool. Perfect for creating paths or roads. A Paint tool lets users draw into 2D textures. A Region + biome editor lets you define the density per layer and the randomization offset. A biome holds the configuration settings for different spawn items, each with their own rules. This lets you reuse biomes with different regions. Everything is managed on the GPU, even the graph system textures are generated using shaders. Using compute shaders, point generation happens using append buffers. Using indirect dispatches, additional point data is generated, like distance to shore regions and normal directions. Using rule-based object selection, items are then selected and sorted using a GPU Radix sort.\nFinally, each item has its matrix generated and put into a buffer for instanced indirect rendering.\nBecause the whole system is deterministic, the data that has to be serialized is minimal and everything else can be generated when needed.\nUnder the hood # The generation pipeline is built as a series of independent, ordered GPU stages.\nEach one is a self-contained compute dispatch. An orchestrator decides what has to be regenerated and generates the required context. One of the more interesting problems was a GPU readback that was stalling the CPU for ~0.80ms out of every 1.3ms regeneration. It was needed to fetch an exact point count so the next buffers could be sized correctly.\nThe fix was to stop asking for an exact point count and instead allocate all buffers for their worst-case size. On the GPU, we then read the real count from the count buffer and every dispatch can run using an indirect dispatch. This one change reduced the generation time from 1.3ms to 0.5ms.\nThe points are generated using a 16x16 ordered dither texture that is tiled over the region depending on the density. A density of 1 essentially means each point is separated 1m from the next one. By calculating the number of tiles that are required for generating, we can then calculate the maximum number of points the region can hold. On the GPU, each threadgroup has a size of 16x16x1 threads, where each thread handles a single point from the dither texture.\nBy dispatching a threadgroup for each tile, we can nicely calculate the world position of a point by its GroupThreadId and its GroupID.\nGet the tile size and tile count based on a region densityprivate void GetTileData(float density, Bounds bounds) { // density of 1 means 1 point every 1 unit (1m). // since the density texture is 16x16 points, we need to scale this by 16 // density 1 -\u0026gt; tile size of 16x16 // density 2 -\u0026gt; tile size of 32x32 float2 tileSize = 16f * density; uint2 tileCount = (uint2)math.ceil(new float2(bounds.size.x, bounds.size.z) / tileSize); return (tileSize, tileCount); } Calculating a points world position in hlslfloat3 LocalIdToPositionWS(uint3 GroupThreadId, uint3 GroupID) { float3 positionWS = float3(GroupThreadId.x, 0, GroupThreadId.y) * density; positionWS.xz += density * 0.5f; // offset them by half a pixel. this makes sure the world position is based on the center of the pixels instead positionWS.xz += tileSize.xy * GroupID.xy; // offset the world position by the region origin point positionWS.xz += REGION_MIN; return positionWS; } What\u0026rsquo;s next # Spawn item variants, sometimes you want different items with the same rules. With the current system you have to keep copying and pasting the rules between different items. Painting density directly in the scene view, rather that on a flat 2D texture. References # GPU-Based Procedural Placement in Horizon Zero Dawn\nFidelityFX Parallel Sort 1.3\nBlue noise textures + some extra information\nAssets are from Idyllic Fantasy Nature\n","externalUrl":null,"permalink":"/projects/gpu_driven_object_placement/","section":"Projects","summary":"Arbor is a GPU-driven object placement system for Unity, designed to place thousands of assets across large regions.","title":"GPU Driven Object Placement","type":"projects"},{"content":" Hi! I\u0026rsquo;m Cédric, a technical artist from belgium.\nI\u0026rsquo;ve spent about 7 years in game development, mostly working on graphics programming and editor tools.\nOn the graphics side I write shaders and rendering features, and I also build procedural content and tools in Houdini.\nThose are what I keep coming back to.\nI love learning, whether that\u0026rsquo;s a rendering talk or a workshop on something new. Outside of work I go for walks in nature, practice guitar, and puzzle over sudokus and Rubik\u0026rsquo;s cubes.\n","externalUrl":null,"permalink":"/about/","section":"Cédric Van Huffelen","summary":"","title":"About me","type":"page"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"In recent years, several major emergencies have taken place in Belgium. Think of floods, major storms, chemical accidents and the attacks in Brussels. To be able to respond quickly and correctly, a good knowledge of the risks in your environment and the right reflexes is crucial. The National Crisis Center has prepared a fascinating interactive three-part curriculum for primary school students. The package is supplemented with the exciting online game BE-Ready, which prepares the students in a playful way for every emergency situation.\nDuring the development of this game I was responsible for the following tasks:\nProgram different minigames Maintain the company\u0026rsquo;s internal framework Design a state-machine to control the flow of the game Write custom shaders to achieve desired art style Program custom UI components Deploy to Android, IOS and WebGL ","externalUrl":null,"permalink":"/portfolio/beready/","section":"Portfolio","summary":"An online game from the National Crisis Center that teaches primary school students how to act in emergencies like floods and storms, through play.","title":"Be-Ready","type":"portfolio"},{"content":"","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","externalUrl":null,"permalink":"/","section":"Cédric Van Huffelen","summary":"","title":"Cédric Van Huffelen","type":"page"},{"content":"EXR is a cross-platform rowing app where you train, race, and follow structured workouts in a stylized virtual world. As a senior technical artist on a small team, I worked mainly on rendering, UI, and the tooling and pipeline around them.\nRendering # When working on EXR, I was responsible for the rendering of the game.\nEnvironment System # I\u0026rsquo;ve implemented a custom environment system, allowing creating different time of day settings and weather profiles. It\u0026rsquo;s built on Unity\u0026rsquo;s render feature using the RenderGraph system and volume framework. This lets the artists and designer set up overrides while having a smooth transition.\nThis was needed when multiple biomes are set in the same scene.\nYour browser cannot play this video. Download video.\nYour browser cannot play this video. Download video.\nWater # Water is probably the most important thing to look good in a virtual rowing app. As my first task on the game I updated the water rendering to a more modern stylized look. Using render features to implement dynamic wakes to classic depth based coloring I managed to make water feel good while keeping performance within targets. The final shader includes:\nWater Decals for debris or foam effects Depth based colors Depth based refraction Planar reflections Edge foam Dynamic boat wakes Crowds # To make the world of EXR feel more alive we wanted to implement some crowds. Since we could have thousands of instances in the scene, ordinary skeletal animations would be too heavy for performance. We opted for a shader based animation system where the position and rotation of each vertex is baked to a texture, with some randomization added in the clothes and colors. Your browser cannot play this video. Download video.\nUI Implementation # Since the release of UIToolkit in Unity 2021 we started porting the old UI to a newer look. Designs were made in Figma, and I ported them into Unity. Using custom USS themes, I was able to share styles and variables in the project so I could spend less time creating the UXML files, and focus more on the functionality. Custom UI elements were also created for our custom localization solution.\nCustom Tools # Training Editor # As a technical artist I often had to create custom tooling for the game. Because EXR allows users to follow predefined workouts and training plans one of the many pain points was creating them. They are loaded in as JSON data that contain a lot of nested data. Because of that it was very easy to make mistakes in formatting or setting up any data.\nWith the workout and training plan editor, designers can focus only on setting up the needed data and the tool would handle the correct formatting and validate any data.\nYour browser cannot play this video. Download video.\nYour browser cannot play this video. Download video.\nLocalization Importer # Since EXR uses its own custom localization system, with each language table stored in a separate file, keeping everything in sync can be tiresome. Because we also use third-party services for localizing to foreign languages, one big table was kept on the cloud using Google Sheets.\nThen the localization importer in Unity can download those tables and split them up in their correct language files, removing the need to manually add them for every entry.\nBillboard Baker # Your browser cannot play this video. Download video.\nNature is a big part of EXR\u0026rsquo;s world. Because we primarily target tablets and mobile devices, we can\u0026rsquo;t render thousands of grass meshes and trees without tanking performance. Even with LODs, objects in the far distance would be very bad for performance. Instead of rendering low resolution meshes, we baked them into albedo textures and normal maps so they can be displayed into simple quads. ","externalUrl":null,"permalink":"/portfolio/exr/","section":"Portfolio","summary":"EXR is a cross-platform rowing app where you train, race, and follow structured workouts in a stylized virtual world.","title":"EXR","type":"portfolio"},{"content":"","externalUrl":null,"permalink":"/portfolio/","section":"Portfolio","summary":"","title":"Portfolio","type":"portfolio"},{"content":"","externalUrl":null,"permalink":"/projects/","section":"Projects","summary":"","title":"Projects","type":"projects"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"},{"content":"","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"}]