Skip to content

Lightning effect

Romain Milbert edited this page Aug 28, 2023 · 17 revisions

⚠️ Due to legacy features (or rather non-features), the output of the PBR lighting is currently necessarily tone mapped; this means that no color can be above 1, thus the bloom, which uses a thresholding operation, will never have an effect on the lightning. To fix this, you must remove or comment out the tone mapping operation at the end of the cook-torrance.frag shader: color = color / (color + vec3(1.0));. This is obviously not the wanted behavior and will change in the near future; this message will then be removed accordingly.

To be precise, the bloom actually has a threshold of 0.75 to actually have an effect until this tone mapping gets removed. However, even very high blue emissive values on the lightning actually get ignored on thresholding. This does not happen (so much) without the tone mapping, hence this manual change required.


As the time of writing (2023-01-17, commit 656398d), the following files give the result shown in this video:

RaZ Playground - Lightning effect

Using this code, you can perform several actions:

  • Add a line with Page Up, up to 64;
  • Remove a line with Page Down, down to 1;
  • Toggle between slow & fast mode with R.

main.cpp

#include <RaZ/Application.hpp>
#include <RaZ/Data/Color.hpp>
#include <RaZ/Data/Mesh.hpp>
#include <RaZ/Data/ObjFormat.hpp>
#include <RaZ/Math/Transform.hpp>
#include <RaZ/Render/BloomRenderProcess.hpp>
#include <RaZ/Render/BoxBlurRenderProcess.hpp>
#include <RaZ/Render/Camera.hpp>
#include <RaZ/Render/Light.hpp>
#include <RaZ/Render/MeshRenderer.hpp>
#include <RaZ/Render/RenderSystem.hpp>
#include <RaZ/Render/SsrRenderProcess.hpp>

#include <random>

using namespace Raz::Literals;

constexpr unsigned int textureSize = 512;

int main() {
  Raz::Application app;
  Raz::World& world = app.addWorld(7);

  auto& render = world.addSystem<Raz::RenderSystem>(1280, 720, "RaZ");

  if (!Raz::Renderer::checkVersion(4, 0) && !Raz::Renderer::isExtensionSupported("GL_ARB_tessellation_shader")) {
    throw std::runtime_error("Error: Tessellation is only available with an OpenGL 4.0 context or above, or with the 'GL_ARB_tessellation_shader' extension; "
                             "please update your graphics drivers or try on another computer");
  }

  if (!Raz::Renderer::checkVersion(4, 3) && !Raz::Renderer::isExtensionSupported("GL_ARB_compute_shader")) {
    throw std::runtime_error("Error: Compute is only available with an OpenGL 4.3 context or above, or with the 'GL_ARB_compute_shader' extension; "
                             "please update your graphics drivers or try on another computer");
  }

  Raz::Window& window = render.getWindow();

  window.addKeyCallback(Raz::Keyboard::ESCAPE, [&app] (float) noexcept { app.quit(); });
  window.setCloseCallback([&app] () noexcept { app.quit(); });

  Raz::Entity& camera = world.addEntityWithComponent<Raz::Transform>(Raz::Vec3f(0.f, 0.f, 5.f));
  camera.addComponent<Raz::Camera>(window.getWidth(), window.getHeight());

  const auto noiseTexture = Raz::Texture1D::create(textureSize, Raz::TextureColorspace::GRAY, Raz::TextureDataType::FLOAT16);
  Raz::Renderer::bindImageTexture(0, noiseTexture->getIndex(), 0, false, 0, Raz::ImageAccess::WRITE, Raz::ImageInternalFormat::R16F);

  Raz::ComputeShaderProgram noise(Raz::ComputeShader("perlin_noise_1d.comp"));
  noise.setAttribute(0.015f, "uniNoiseFactor");
  noise.setAttribute(8, "uniOctaveCount");
  noise.sendAttributes();
  noise.execute(textureSize);

  Raz::Entity& mesh = world.addEntityWithComponent<Raz::Transform>(Raz::Vec3f(0.2f, -1.f, 4.7f), Raz::Quaternionf(90_deg, Raz::Axis::Y), Raz::Vec3f(0.0053f));
  mesh.addComponent<Raz::MeshRenderer>(Raz::ObjFormat::load("crytek_sponza.obj").second);

  Raz::Entity& lightning = world.addEntityWithComponent<Raz::Transform>();

  Raz::Mesh lightningPoints;
  lightningPoints.addSubmesh().getVertices() = {
    Raz::Vertex{ Raz::Vec3f(-1.f, 0.f, 0.f) },
    Raz::Vertex{ Raz::Vec3f(1.f, 0.f, 0.f) },
    Raz::Vertex{ Raz::Vec3f(-1.f, 0.f, 10.1f) },
    Raz::Vertex{ Raz::Vec3f(1.f, 0.f, 10.1f) }
  };

  ////////////////////////
  // Lightning material //
  ////////////////////////

  auto& lightningProgram = lightning.addComponent<Raz::MeshRenderer>(lightningPoints, Raz::RenderMode::PATCH).getMaterials().front().getProgram();
  lightningProgram.setAttribute(Raz::ColorPreset::Blue, Raz::MaterialAttribute::BaseColor);
  // We're giving a very high blue value, since the bloom's thresholding pass reduces it so much
  lightningProgram.setAttribute(Raz::Vec3f(0.1f, 0.1f, 50.f), Raz::MaterialAttribute::Emissive);
  Raz::Renderer::setPatchVertexCount(2); // To render points 2 by 2, creating lines

  lightningProgram.setTessellationControlShader(Raz::TessellationControlShader::loadFromSource(R"(
    layout(vertices = 2) out;

    struct MeshInfo {
      vec3 vertPosition;
      vec2 vertTexcoords;
      mat3 vertTBNMatrix;
    };

    in MeshInfo vertMeshInfo[];

    uniform float uniLineCount  = 1.0;
    uniform float uniPointCount = 64.0;

    out MeshInfo tessMeshInfo[];

    void main() {
      gl_out[gl_InvocationID].gl_Position         = gl_in[gl_InvocationID].gl_Position;
      tessMeshInfo[gl_InvocationID].vertPosition  = vertMeshInfo[gl_InvocationID].vertPosition;
      tessMeshInfo[gl_InvocationID].vertTexcoords = vertMeshInfo[gl_InvocationID].vertTexcoords;
      tessMeshInfo[gl_InvocationID].vertTBNMatrix = vertMeshInfo[gl_InvocationID].vertTBNMatrix;

      if (gl_InvocationID == 0) {
        gl_TessLevelOuter[0] = uniLineCount; // How many lines to create
        gl_TessLevelOuter[1] = uniPointCount; // How many points to create for each line
      }
    }
  )"));
  lightningProgram.setTessellationEvaluationShader(Raz::TessellationEvaluationShader::loadFromSource(R"(
    layout(isolines, fractional_odd_spacing, ccw) in;

    struct MeshInfo {
      vec3 vertPosition;
      vec2 vertTexcoords;
      mat3 vertTBNMatrix;
    };

    in MeshInfo tessMeshInfo[];

    uniform sampler1D uniNoiseMap;
    uniform float uniSmoothness = 1.0;
    uniform float uniHeight = 1.0;
    uniform float uniSpeed = 1.0;
    uniform float uniTime;

    layout(std140) uniform uboCameraInfo {
      mat4 uniViewMat;
      mat4 uniInvViewMat;
      mat4 uniProjectionMat;
      mat4 uniInvProjectionMat;
      mat4 uniViewProjectionMat;
      vec3 uniCameraPos;
    };

    out MeshInfo vertMeshInfo;

    float hash(float val) {
      // "Hash without Sine", from https://www.shadertoy.com/view/4djSRW
      val  = fract(val * 0.1031);
      val *= val + 33.33;
      val *= val + val;
      return fract(val);
    }

    void main() {
      vec3 beginPos = tessMeshInfo[0].vertPosition;
      vec3 endPos   = tessMeshInfo[1].vertPosition;
      vec3 vertPos  = mix(beginPos, endPos, gl_TessCoord.x);

      // Leaving the lines' extremities in place
      if (gl_TessCoord.x != 0.0 && gl_TessCoord.x != 1.0) {
        vertPos.y -= gl_TessCoord.y;

        float time = uniTime * uniSpeed * uniSpeed;

        float texcoord = mod(mix(gl_TessCoord.x, hash(time * (gl_TessCoord.y + 1.0)), 1.0 - uniSmoothness) * 10.0 + time * 0.1, 1.0);
        float noise    = texture(uniNoiseMap, texcoord).r * 2.0 - 1.0;

        vertPos.y += noise * uniHeight;
      }

      vertMeshInfo.vertPosition = vertPos;

      // Small hack to "fix" SSR by setting the normal pointing to the camera. This sort of prevents finding a reflection
      //  since nothing can exist between the point and the camera, thus displaying the line as-is
      vertMeshInfo.vertTBNMatrix[2] = normalize(uniCameraPos - vertPos);

      gl_Position = uniViewProjectionMat * vec4(vertPos, 1.0);
    }
  )"));
  lightningProgram.link();

  lightningProgram.setTexture(noiseTexture, "uniNoiseMap");

  // Sending initial values
  lightningProgram.setAttribute(0.15f, "uniHeight");
  lightningProgram.setAttribute(0.035f, "uniSmoothness");
  lightningProgram.sendAttributes();

  ////////////////////
  // Post processes //
  ////////////////////

  const auto depthBuffer        = Raz::Texture2D::create(window.getWidth(), window.getHeight(), Raz::TextureColorspace::DEPTH);
  const auto colorBuffer        = Raz::Texture2D::create(window.getWidth(), window.getHeight(), Raz::TextureColorspace::RGB, Raz::TextureDataType::FLOAT16);
  const auto blurredColorBuffer = Raz::Texture2D::create(window.getWidth(), window.getHeight(), Raz::TextureColorspace::RGB, Raz::TextureDataType::FLOAT16);
  const auto colorBuffer2       = Raz::Texture2D::create(window.getWidth(), window.getHeight(), Raz::TextureColorspace::RGB, Raz::TextureDataType::FLOAT16);
  const auto normalBuffer       = Raz::Texture2D::create(window.getWidth(), window.getHeight(), Raz::TextureColorspace::RGB);
  const auto specularBuffer     = Raz::Texture2D::create(window.getWidth(), window.getHeight(), Raz::TextureColorspace::RGBA);

  Raz::RenderPass& geometryPass = render.getGeometryPass();

  geometryPass.setWriteDepthTexture(depthBuffer);
  geometryPass.addWriteColorTexture(colorBuffer, 0);
  geometryPass.addWriteColorTexture(normalBuffer, 1);
  geometryPass.addWriteColorTexture(specularBuffer, 2);

  auto& blur = render.getRenderGraph().addRenderProcess<Raz::BoxBlurRenderProcess>();
  blur.addParent(geometryPass);
  blur.setInputBuffer(colorBuffer);
  blur.setOutputBuffer(blurredColorBuffer);
  blur.setStrength(8);

  auto& ssr = render.getRenderGraph().addRenderProcess<Raz::SsrRenderProcess>();
  ssr.addParent(blur);
  ssr.setInputDepthBuffer(depthBuffer);
  ssr.setInputColorBuffer(colorBuffer);
  ssr.setInputBlurredColorBuffer(blurredColorBuffer);
  ssr.setInputNormalBuffer(normalBuffer);
  ssr.setInputSpecularBuffer(specularBuffer);
  ssr.setOutputBuffer(colorBuffer2);

  auto& bloom = render.getRenderGraph().addRenderProcess<Raz::BloomRenderProcess>();
  bloom.addParent(ssr);
  bloom.setInputColorBuffer(colorBuffer2);

  ////////////
  // Lights //
  ////////////

  auto& leftLight = world.addEntityWithComponent<Raz::Transform>(Raz::Vec3f(-0.95f, 0.f, 0.f)).addComponent<Raz::Light>(Raz::LightType::POINT,
                                                                                                                        0.1f,
                                                                                                                        Raz::ColorPreset::MediumBlue);
  auto& rightLight = world.addEntityWithComponent<Raz::Transform>(Raz::Vec3f(0.95f, 0.f, 0.f)).addComponent<Raz::Light>(Raz::LightType::POINT,
                                                                                                                        0.1f,
                                                                                                                        Raz::ColorPreset::MediumBlue);
  auto& backLeftLight = world.addEntityWithComponent<Raz::Transform>(Raz::Vec3f(-0.95f, 0.f, 10.1f)).addComponent<Raz::Light>(Raz::LightType::POINT,
                                                                                                                              0.1f,
                                                                                                                              Raz::ColorPreset::MediumBlue);
  auto& backRightLight = world.addEntityWithComponent<Raz::Transform>(Raz::Vec3f(0.95f, 0.f, 10.1f)).addComponent<Raz::Light>(Raz::LightType::POINT,
                                                                                                                              0.1f,
                                                                                                                              Raz::ColorPreset::MediumBlue);

  ///////////////////
  // Key callbacks //
  ///////////////////

  int lineCount = 1;

  window.addKeyCallback(Raz::Keyboard::PAGEUP, [&lineCount, &lightningProgram] (float) {
    lineCount = std::min(64, lineCount + 1);
    lightningProgram.setAttribute(static_cast<float>(lineCount), "uniLineCount");
    lightningProgram.sendAttributes();
  }, Raz::Input::ONCE);

  window.addKeyCallback(Raz::Keyboard::PAGEDOWN, [&lineCount, &lightningProgram] (float) {
    lineCount = std::max(1, lineCount - 1);
    lightningProgram.setAttribute(static_cast<float>(lineCount), "uniLineCount");
    lightningProgram.sendAttributes();
  }, Raz::Input::ONCE);

  bool isSlowedDown = false;

  window.addKeyCallback(Raz::Keyboard::R, [&isSlowedDown, &lightningProgram] (float) {
    isSlowedDown = !isSlowedDown;
    lightningProgram.setAttribute((isSlowedDown ? 0.055f : 1.f), "uniSpeed");
    lightningProgram.sendAttributes();
  }, Raz::Input::ONCE);

  app.run([&] (float deltaTime) {
    static float time = 0.f;
    time += deltaTime;

    lightningProgram.setAttribute(time, "uniTime");
    lightningProgram.sendAttributes();

    // Making the lights flicker
    static std::mt19937 gen(std::random_device{}());
    static std::uniform_real_distribution<float> distrib(0.1f, 0.25f);
    leftLight.setEnergy(distrib(gen));
    rightLight.setEnergy(distrib(gen));
    backLeftLight.setEnergy(distrib(gen));
    backRightLight.setEnergy(distrib(gen));
    render.updateLights();
  });

  return EXIT_SUCCESS;
}
layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;

layout(r16f, binding = 0) uniform writeonly image1D uniNoiseMap;
uniform float uniNoiseFactor = 0.01;
uniform int uniOctaveCount   = 1;

const int permutations[512] = int[](
  151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142,
  8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117,
  35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71,
  134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41,
  55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89,
  18, 169, 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226,
  250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182,
  189, 28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43,
  172, 9, 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97,
  228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239,
  107, 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254,
  138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180,
  151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142,
  8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117,
  35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71,
  134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41,
  55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89,
  18, 169, 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226,
  250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182,
  189, 28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43,
  172, 9, 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97,
  228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239,
  107, 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254,
  138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180
);

float smootherstep(float value) {
  return value * value * value * (value * (value * 6.0 - 15.0) + 10.0);
}

float recoverGradient1D(int x) {
  return (permutations[x] % 2 == 0 ? 1.0 : -1.0);
}

float computePerlin(float coord) {
  // Determining coordinates on the line
  //
  //  x0---------x0+1

  int intX = int(coord);

  int x0 = intX & 255;

  float leftGrad  = recoverGradient1D(x0);
  float rightGrad = recoverGradient1D(x0 + 1);

  // Computing the distance to the coordinate
  //
  //  |------X--|
  //      xWeight

  float xWeight = coord - float(intX);

  float leftDot  = xWeight * leftGrad;
  float rightDot = (xWeight - 1) * rightGrad;

  float smoothX = smootherstep(xWeight);

  return mix(leftDot, rightDot, smoothX);
}

float computeFbm(float coord, int octaveCount) {
  float frequency = 1.0;
  float amplitude = 1.0;
  float total     = 0.0;

  for (int i = 0; i < octaveCount; ++i) {
    total += computePerlin(coord * frequency) * amplitude;

    frequency *= 2.0;
    amplitude *= 0.5;
  }

  return (total + 1.0) / 2.0;
}

void main() {
  float noise = computeFbm(float(gl_GlobalInvocationID.x) * uniNoiseFactor, uniOctaveCount);

  int pixelCoord = int(gl_GlobalInvocationID.x);
  imageStore(uniNoiseMap, pixelCoord, vec4(vec3(noise), 1.0));
}
Clone this wiki locally