A few years ago I posted a blog with a wireframe screenshot of the rare hammer Mjolner. A couple months ago I opened the file again to take a look, and then the Delirium league was about to be released; so I opened again. Anyway, let's get that lightning to sing.
For the lightning/electricity itself there’s many considerations. Similar to the neural project, I want the setup to be nice and procedural. As a note of difference in this system it was more about using ribbon particles in Niagara. I had done some procedural lightning in the past, for Chronicles of Elyria —- although it was created using Cascade. In that rendition there was far more blueprint involved, and it was more focused on the primary bolt with one split. Naturally some of operations like setting beam tangents are applicable in the module scripting rather than BP. Not all of the functions necessarily have exact crossover , but there’s a lot of additional vector/matrix functions available in Niag.
Visual components can be broken down to Materials/Shader, particle system, blueprint, additional scene lighting.
The shaders aren’t overly complex, but the main goal is having a parametric band/linear gradient that runs along the polystrip that is our ribbon. Using some functions we’ll compose a gradient/2d line, with the uv coordinates being offset by noise. Keep in mind my shader is leveraging function based noise rather than sampling a texture object, hence higher instruction count. I could also just encode the functional noise to a texture, or author the noise outside of the editor & import for use. We’ll start off with one coordinate offset from the initial noise (simplex), and then another simplex with 1-2 levels, which we can integrate into the main tendril in a few ways. By inverting the gradient we can power it as to create a shell/halo, which can be closed arcing sub-tendrils —- or, keep the initial values to create mini-offshoots. I’m still playing ‘round w/ the shader. Interestingly the effect you get when panning the noise is something like a flame combustion stream.
The emissive output is the float result multiplied by blackbody (lightning temperatures can reach 30,000 C), but the final intensity is a fractional multiplier due to the sensitivity of the post process to bloom & autoexposure. For the presentation that’s fine.
For the electricity on the hammer mesh there’s various ways to go about it. I did a hybrid approach, which is not particularly optimized. Essentially what we’ll do is an offscreen capture of a particle system with the main shader, and then write that simulation to a texture, which gets applied onto a shell of the static mesh via tangent space. Since the coordinates are not contiguous the tendrils hop between uv islands at the boundary, but it’s not a big deal since electricity can appear rather erratic anyway. That said it needs improvements. The more physical approach is something more analytical. which would be querying collision hits on the surface of the static mesh, and then spawning the particles along the spline; or doing a lookup of the barycoords for an array of triangles on the mesh surface. In the latter case you’d optimally deal with a decomposition of the high-poly model, so that you can quickly sort through the series of nearest vectors. Etc. I’ll probably expand into that later on.
Onto the particle system portion, there’s really a lot. The essentials come down to the system, the emitters, and the modules. The system contains all of our emitters, and the emitters leverage modules. The first step is an emitter which starts at a point approximate to the surface of the static mesh (can do a random tri coord, weighted coordinate, or simply a numerical range within the bounding box), and then set a target position. For the purpose of testing I’m setting the target position by way of blueprint vector widget (you can drag the vector gizmo around, and its worldspace position gets set to a linked Niagara variable. The workflow is create a new variable on the emitter of the type you want (currently, at-least in 4.21 those are limited to float data types —- float, vec2, vec3, vec4, non arrays), with the User. namespace. Niagara’s namespace functionality is nice, but takes a bit getting used to.
Between these positions we do some logic to set all things on the ribbon renderer. For example the density of particles across the length; higher counts increasing the smoothness of the emitter spline, but requiring functional noise as a modulator. So on-top of the primary positions, I included an additive —- which is simply the sine of the normalized execution index, with a period over 1, and random coefficient. We can add some randomization onto that position w things like Gaussian random vector, but since the random will get called for each particle index you usually end up with a very erratic, but constant, offset. Other options are to multiply it by a curve, or use a spline handle calculation. We can supply with a 3d/curl noise, or assemble the points+handles on emitter initialization. However, with this approach, the algorithm is doing everything between 2 points, so if the bolt is calculated afterwards it can go right through collision objects. If the goal was to have it wrap, or change path then we’d do a collision query on ParticleUpdate perhaps. Some variation of an lsystem could work nicely for raytracing the vectors, but that’s another layer of complexity that I’m not doing for this iteration.
When the primary bolt is rendered, we step through a couple if statements & then if true, select that point as well as a couple other vectors to dispatch as an event write. The event write is a custom struc that you can simply add from the project UI. Anyway, I ran into an issue with ribbons sporadically returning a particle to the root… I thought hey maybe that would be due to a conflict with the acquire tag; thus I just give each ID a random integer (when an emitter is being used in a forloop method, acquire tag shouldn’t be rand). Next up I created a tracer emitter that reads the event, along with its data; things are done, mainly just a collision query in a direct line from the main bolt position to another position (the start position + direction with magnitude.) That direction is designated as a vector we get by taking the result of a spherical linear interpolation between the forward & right vector of the system owner. The alpha being a random range, and the mirrored direction being one axis * -1, with a random boolean as the selector. Re-write the data through our map.
The collision point will be the end positions of the secondary bolts. Those secondary bolts receive a similar set of algorithms as the primary bolt , but with some extra things to make them pop. Electrical discharges from the hammer mesh also leverage this logic, the only difference between that the start point is a random tri coordinate with a bias for top-down. Random ribbon width etc.
The other material settings don't require a great degree of explanation. The textures & shader setup are pbr; textures were arranged in substance designer with some of the explicit maps being curvature & displacement (although for now the ground texture is just a modified photoscan). The volume fog comes in two flavors; one being simply enabled on the fog component, and the other being the volume fog shader model applied onto a sprite/geometry field. So I used the same method as a workaround; by way of a top-down render target texture for the particle system, & then applying it in the volume fog material. For VXGI propagation I sample that texture, with opacity, on an inverted single-sided plane. Right now I’m not worrying about the z-axis save for a 3d noise to break up lines; Regardless, it adds a proper element.
More to come