For the final, we intend to do the “Rendering Style Competition” by recreating this scene from the anime Jujutsu Kaisen:
Figure 1: This image showcases Sukuna using his Divine Flame technique to defeat Mahoraga.
In order to recreate this scene, we have to figure out how to create volumetric fire (or more specifically in our case, fire in the shape of an arrow). We would also have to find a model of Sukuna and rig it. As a nice add-on (if we have time), we would also like to implement the glow around the fire and the embers flying off from the arrow.
Currently, we have implemented a very rough ‘fire’ effect that takes the shape of a sphere (see Figure 2). We developed a procedural field using Perlin noise to simulate volume representation. We used delta tracking to attempt sampling distance through a heterogeneous volume. We used equiangular sampling to implement NEE in a volumetric case. We also handled cases where the light would scatter or absorb.
We have also implemented a simple cube map, texture mapping, and support for OBJ. We have implemented these features to ease the workflow of visually creating a scene. We plan to rig the models and place them in the correct poses in the 3d software Blender. Afterwards, we export only the model positions, normals, and uv positions and export them to our custom scene loader.
Figure 2: Our current render of a volumetric fire contained in the sphere.
Figure 3: Our current render of a volumetric fire bounded by a box with density functions.
On a high level, our next steps are: rig the Sukuna model so that it matches the pose from the scene; further improve the environment mapping with cube mapping for a good contribution to background/lighting; make the volumetric fire look better (should be shaped like an arrow); add the glow caused from the fire; and add sparks. At the end of the project, we aim to focus on making the fire feel like a fluid similar to how it is depicted in the scene.
Some articles and research papers we have looked into so far:
https://dl.acm.org/doi/pdf/10.1145/3587421.3595433
https://cg.informatik.uni-freiburg.de/intern/seminar/volumeRenderingProductionFundamentals2011.pdf
https://cs.dartmouth.edu/~wjarosz/publications/miller19null.pdf
https://jannovak.info/publications/SDTracking/SDTracking.pdf
https://blogs.autodesk.com/media-and-entertainment/wp-content/uploads/sites/162/egsr2012_volume.pdf
We found a 3d model of Sukuna from the 3d model library Sketchfab, which included the 3d object, diffuse, normal, and specular textures. Luckily the model also included a pre-made rig, which we used to pose the object. We additionally modeled Sukuna's arrow ourselves which would be used as a container for the fire particles.
Figure 4: Posing Sukuna using the 3d software Blender
Figure 5: The modeling of the arrow using Blender. We modeled a fourth of the arrow, and then applied the Mirror modifier to complete the shape. We also applied a subdivision modifier to make it look better.
Figure 6: Adding tiny spheres in the scene to represent embers
We exported each model as an OBJ file. This implies that our path tracer integration must support parsing OBJ files, as well as support for diffuse texutres, normal textures, and specular textures. We chose OBJ files because it was the simplest export form that did not include materials. We wanted to be in full control of our materials because of our custom implementation. By using OBJ files, we also save time as we do not have to manually type in every triangle and vertex. By using Blender, we are able to easily make changes to our scene visually instead of re-rendering each time tediously with the path tracer. We converted our coordinates from Blender to our path tracer integration; if a camera's position or rotation was in the form (x,y,z), the coordinate in our path tracer integration would be (x,z,-y).
For Sukuna specifically, we had to apply the armature modifier to retain his pose, allowing us to export the model as an OBJ with the pose baked on the model.
When the scene loads the texture in SceneLoader (commands diffuseTexture, normalTexture, specularTexture), the pngs are decoded using the library lodepng, converted to sRGB, and gamma is applied in loadTexturePNG (SceneLoader.cpp::26). Note that normal maps are set in linear color space, we do not apply sRGB to normal maps specifically.
These textures are stored in an array; materials must store their own id to fetch the correct texture.
Figure 7: Sukuna object in pose, with a simple area light.
We sampled the textures using the triangle's uvs. We used a simple nearest-neighbor algorithm to filter the texture.
Our implementation does not support alpha pngs, which resulted in some of sukuna's detail in clothing being taken out.
First, we added three point lights to simulate firelight: a key light at the arrow tip for hand and face glow, a softer amber fill, and a deep red-orange rim light. The white directional was simultaneously dimmed to warm orange so it would stop washing out the fire colors. Point lights were implemented as delta emitters using one shadow ray per light and quadratic attenuation, with no MIS weight since Dirac delta lights cannot be importance-sampled. These were also wired into volumetric in-scatter so fire volumes could respond to them.
Later, we decided to switch the BRDF from Phong to GGX for physically-based specular on Sukuna's skin and introduced ACES filmic tonemapping. With fire volumes now self-illuminating via NEE volume lighting, the three point lights were stripped back. This is because their role could be taken over by six OBJ ember mesh emitters with blackbody-ish colors ranging from white-hot to deep red, which we ended up liking much better.
Finally, we implemented a custom hemisphere light with separate upper and lower sky color terms. Surface hits blend the two colors based on their normal's Y component, so downward-facing surfaces catch a warm orange fill as a cheap ambient term without additional rays. Miss rays additionally receive a warm orange glow near the horizon via two chained smoothstep calls, peaking just below horizontal and fading to black above and below, creating the illusion of firelit ground behind Sukuna and ensuring every light source traces back to the flaming arrow itself.
We wanted to implement area lights for specifically obj files, so that Sukuna's eyes are more distinguished in the final render. Luckily, because we already implemented the quadLight command over the course of the previous homeworks, the implementation was much easier.
Both implement similar functions to get direct lighting. They both pick a point on the light surface, check for occlusion by raycast, call a brdf function (ggx or phong), and implement MIS.
In contrast to quadLight where the command excepts the exact vertices and triangles, objLight parses an OBJ file, and samples differently. For objLight, we do hierarchical area sampling so that we can accurately pick a triangle that represents the mesh (more area = higher chance of it being chosen). We implement this by using a CDF; once we pick the triangle, we find it using binary search.
In a general overview of our volumetric fire pipeline, we implement the steps:
fireMesh parsing command
SDF baking
Single scattering
Volume Transmittance
Volumetric NEE
Stylization
The fireMesh command takes in the following parameters:
path -> file path to the .obj file
resX, resY, resZ -> SDF grid resolution
band -> drives the rim shell of the model, ramping the density of the volume
density -> a scale parameter that controls how solid/transcluscent the volume is.
Tmin, Tmax -> temperature parameters to create realistic fire colors in Kelvin
albedo -> controls how light is scattered/absorbed
hg_g -> Henyey Greenstein anisotropy that scatters the light direction
noise_freq -> A multiplier on the world position before noise is sampled.
flow_y -> vertical offset added to the noise based on normalized height in AABB
reach -> stretches the fire outside the model
stylized -> boolean that makes density a stepped function, custom color palette, controlled noise
Signed Distance Fields (SDF) are scalar fields where each point stores the signed distance to the nearest surface of some shape; we define negative inside the shape, zero on the surface, positive on the outside. They are used to trace spheres and bound volumes, which is good for our case where we need to contain fire in a container.
In the function bakeSdfFromTriangles: We create an AABB bounding box by scanning all triangles in the model. We allocate the grid according to the parameters, assigning one float number per voxel.
For each voxel pixel p, we compute the unsigned distance between the voxel center and the closest point in the triangles of the model. We are able to give a sign to the distance by shooting a ray in the +X direction from the voxel pixel. Counting an odd number of triangles implies the voxel point is inside and even numbers implies the point is outside (Moller Tumbore even-odd test). We must assume that the mesh is fully closed; otherwise the sign would be random.
We normalize the grid by finding the largest interior depth, allowing us to scale the temperature by the mesh, and not the world.
This process is very inefficient as the runtime is O(voxels * triangles) with no acceleration structure.
From a given camera ray or ray that resulted from a bounce, we get a segment from the origin to the hit surface. We split this into N segments and sample each point. We evaluate the volume at each sample point by reading the baked SDF grid, modifying it using perlin noise. We calculate the emission and inscattering by calculating the scattering and absorption properties, acccumulating the terms per step.
For each light in the scene, we sample a point on the light which differs between area lights and point lights, computing either geometric factor or attenuation for area and point lights respectively. After occlusion checking, we calculate the volume transmittance in the VolumeFire class, which is similar to the Russian Roullete implementation. We then call the phase function weight to control the forward-scattering.
If the stylization parameter is enabled, the density profile is stepped instead of continuous, so the edges are more defined and the fire is more colorful.
Stylized fire arrow
Non-stylized fire arrow
We added black letterboxes at the top and bototm of the image to capture the cinematic moment of the scene.
To improve on the fire effect, we wanted to make the lighting more extreme. We figured that tone mapping and HDR would help us bring the effect of the embers scattered across the scene. We have tried ACES and Reinhard tone mapping, each producing different results to the final image. We decided to go with ACES because it looked more colorful; Reinhard tonemapping made the colors feel washed out.
Additionally, we add a bloom pass to the final image to further bring out the lighting, running before the tone mapping.
We wanted to replicate the embers from the Jujutsu Kaisen scene
128 spp, Reinhard tone mapping
2048 spp, ACES tone mapping