Better_Software_Header_MobileBetter_Software_Header_Web

Find what you need - explore our website and developer resources

Shader Variants

Explosions of the Combinatorial Kind

layout(location = 0) in vec3 vertexPosition;
layout(location = 1) in vec3 vertexNormal;
#ifdef TEXCOORD_0_ENABLED
layout(location = 2) in vec2 vertexTexCoord;
#endif

layout(location = 0) out vec3 normal;
#ifdef TEXCOORD_0_ENABLED
layout(location = 1) out vec2 texCoord;
#endif
void main()
{
#ifdef TEXCOORD_0_ENABLED
    texCoord = vertexTexCoord;
#endif
    normal = normalize((camera.view * entity.model[gl_InstanceIndex] * vec4(vertexNormal, 0.0)).xyz);
    gl_Position = camera.projection * camera.view * entity.model[gl_InstanceIndex] * vec4(vertexPosition, 1.0);
}
glslangValidator -o material-with-uvs.vert.spirv -DTEXCOORD_0_ENABLED material.vert    # With texture coords
glslangValidator -o material-without-uvs.vert.spirv material.vert                      # Without texture coords
void main()
{
    vec4 baseColor = ...;
#ifdef ALPHA_CUTOFF_ENABLED
    if (baseColor.a < material.alphaCutoff)
        discard;
#endif
    ...
    fragColor = baseColor;
}
Tex Coord OffTex Coord On
Alpha Cut-off Off-DTEXCOORD_0_ENABLED
Alpha Cut-off On-DALPHA_CUTOFF_ENABLED-DTEXCOORD_0_ENABLED -DALPHA_CUTOFF_ENABLED
[TexCoords Off, Alpha Cut-off Off, blur taps = 3]
[0, 0, 0]
[1, 0, 2]
[0, 0, 0]
[0, 0, 1]
[0, 0, 2]
[0, 0, 3]
[0, 1, 0]
[0, 1, 1]
[0, 1, 2]
[0, 1, 3]
[1, 0, 0]
[1, 0, 1]
[1, 0, 2]
[1, 0, 3]
[1, 1, 0]
[1, 1, 1]
[1, 1, 2]
[1, 1, 3]
for i = 0 to combination_count
   option_vector = calculate_option_vector(i)
   output_compiler_options(option_vector)
next i
{
    "options": [
        {
            "name": "hasTexCoords",
            "define": "TEXCOORD_0_ENABLED"
        },
        {
            "name": "enableAlphaCutoff",
            "define": "ALPHA_CUTOFF_ENABLED"
        },
        {
            "name": "taps",
            "define": "BLUR_TAPS",
            "values": [3, 5, 7, 9]
        }
    ],
    "shaders": [
        {
            "filename": "materials.vert",
            "options": [0, 1]
        },
        {
            "filename": "materials.frag",
            "options": [0, 1, 2]
        }
    ]
}
def calculate_digits(bases, index)
  digits = Array.new(bases.size, 0)
  base_index = digits.size - 1
  current_value = index
  while current_value != 0
    quotient, remainder = current_value.divmod(bases[base_index])
    digits[base_index] = remainder
    current_value = quotient
    base_index -= 1
  end
  return digits
end
{
  "variants": [
    {
      "input": "materials.vert",
      "defines": "",
      "output": "materials.vert.spv"
    },
    {
      "input": "materials.vert",
      "defines": "-DALPHA_CUTOFF_ENABLED",
      "output": "materials_alpha_cutoff_enabled.vert.spv"
    },
    {
      "input": "materials.vert",
      "defines": "-DTEXCOORD_0_ENABLED",
      "output": "materials_texcoord_0_enabled.vert.spv"
    },
    {
      "input": "materials.vert",
      "defines": "-DTEXCOORD_0_ENABLED -DALPHA_CUTOFF_ENABLED",
      "output": "materials_texcoord_0_enabled_alpha_cutoff_enabled.vert.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DBLUR_TAPS=3",
      "output": "materials_blur_taps_3.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DBLUR_TAPS=5",
      "output": "materials_blur_taps_5.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DBLUR_TAPS=7",
      "output": "materials_blur_taps_7.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DBLUR_TAPS=9",
      "output": "materials_blur_taps_9.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DALPHA_CUTOFF_ENABLED -DBLUR_TAPS=3",
      "output": "materials_alpha_cutoff_enabled_blur_taps_3.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DALPHA_CUTOFF_ENABLED -DBLUR_TAPS=5",
      "output": "materials_alpha_cutoff_enabled_blur_taps_5.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DALPHA_CUTOFF_ENABLED -DBLUR_TAPS=7",
      "output": "materials_alpha_cutoff_enabled_blur_taps_7.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DALPHA_CUTOFF_ENABLED -DBLUR_TAPS=9",
      "output": "materials_alpha_cutoff_enabled_blur_taps_9.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DTEXCOORD_0_ENABLED -DBLUR_TAPS=3",
      "output": "materials_texcoord_0_enabled_blur_taps_3.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DTEXCOORD_0_ENABLED -DBLUR_TAPS=5",
      "output": "materials_texcoord_0_enabled_blur_taps_5.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DTEXCOORD_0_ENABLED -DBLUR_TAPS=7",
      "output": "materials_texcoord_0_enabled_blur_taps_7.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DTEXCOORD_0_ENABLED -DBLUR_TAPS=9",
      "output": "materials_texcoord_0_enabled_blur_taps_9.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DTEXCOORD_0_ENABLED -DALPHA_CUTOFF_ENABLED -DBLUR_TAPS=3",
      "output": "materials_texcoord_0_enabled_alpha_cutoff_enabled_blur_taps_3.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DTEXCOORD_0_ENABLED -DALPHA_CUTOFF_ENABLED -DBLUR_TAPS=5",
      "output": "materials_texcoord_0_enabled_alpha_cutoff_enabled_blur_taps_5.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DTEXCOORD_0_ENABLED -DALPHA_CUTOFF_ENABLED -DBLUR_TAPS=7",
      "output": "materials_texcoord_0_enabled_alpha_cutoff_enabled_blur_taps_7.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DTEXCOORD_0_ENABLED -DALPHA_CUTOFF_ENABLED -DBLUR_TAPS=9",
      "output": "materials_texcoord_0_enabled_alpha_cutoff_enabled_blur_taps_9.frag.spv"
    }
  ]
}
require 'json'
require 'pp'

def expand_options(data)
  # Expand the options so that if no explicit options are specified we default
  # to options where the #define symbole is defined or not
  data[:options].each do |option|
    if !option.has_key?(:values)
      option[:values] = [:nil, :defined]
    end
    option[:count] = option[:values].size
  end
end

def extract_options(data, shader)
  shader_options = Hash.new
  shader_options[:options] = Array.new
  shader[:options].each do |option_index|
    shader_options[:options].push data[:options][option_index]
  end
  # STDERR.puts "Options for shader:"
  # STDERR.puts shader_options
  return shader_options
end

def find_bases(data)
  bases = Array.new(data[:options].size)
  (0..(data[:options].size - 1)).each do |index|
    bases[index] = data[:options][index][:count]
  end
  return bases
end

def calculate_steps(bases)
  step_count = bases[0]
  (1..(bases.size - 1)).each do |index|
    step_count *= bases[index]
  end
  return step_count
end

# Calculate the number for "index" in our variable-bases counting system
def calculate_digits(bases, index)
  digits = Array.new(bases.size, 0)
  base_index = digits.size - 1
  current_value = index
  while current_value != 0
    quotient, remainder = current_value.divmod(bases[base_index])
    digits[base_index] = remainder
    current_value = quotient
    base_index -= 1
  end
  return digits
end

def build_options_string(data, selected_options)
  str = ""
  selected_options.each_with_index do |selected_option, index|
    # Don't add anything if option is disabled
    next if selected_option == :nil

    # If we have the special :defined option, then we add a -D option
    if selected_option == :defined
      str += " -D#{data[:options][index][:define]}"
    else
      str += " -D#{data[:options][index][:define]}=#{selected_option}"
    end
  end
  return str.strip
end

def build_filename(shader, data, selected_options)
  str = File.basename(shader[:filename], File.extname(shader[:filename]))
  selected_options.each_with_index do |selected_option, index|
    # Don't add anything if option is disabled
    next if selected_option == :nil

    # If we have the special :defined option, then we add a section for that option
    if selected_option == :defined
      str += "_#{data[:options][index][:define].downcase}"
    else
      str += "_#{data[:options][index][:define].downcase}_#{selected_option.to_s}"
    end
  end
  str += File.extname(shader[:filename]) + ".spv"
  return str
end

# Load the configuration data and expand default options
if ARGV.size != 1
  puts "No filename specified."
  puts "  Usage: generate_shader_variants.rb "
  exit(1)
end

variants_filename = ARGV[0]
file = File.read(variants_filename)
data = JSON.parse(file, { symbolize_names: true })
expand_options(data)

# Prepare a hash to output as json at the end
output_data = Hash.new
output_data[:variants] = Array.new

data[:shaders].each do |shader|
  # STDERR.puts "Processing #{shader[:filename]}"

  # Copy over the options referenced by this shader to a local hash that we can operate on
  shader_options = extract_options(data, shader)

  # Create a "digits" array we can use for counting. Each element (digit) in the array
  # will correspond to an option in the loaded data configuration. The values each
  # digit can take are those specified in the "values" array for that option.
  #
  # The number of steps we need to take to count from "0" to the maximum value is the
  # product of the number of options for each "digit" (option).
  bases = find_bases(shader_options)
  # STDERR.puts "Bases = #{bases}"
  step_count = calculate_steps(bases)
  # STDERR.puts "There are #{step_count} combinations of options"

  # Count up through out range of options
  (0..(step_count - 1)).each do |index|
    digits = calculate_digits(bases, index)

    selected_options = Array.new(bases.size)
    (0..(bases.size - 1)).each do |digit_index|
      settings = data[:options][digit_index]
      setting_index = digits[digit_index]
      selected_options[digit_index] = settings[:values][setting_index]
    end

    # Construct the options to pass to glslangValidator
    defines = build_options_string(shader_options, selected_options)
    output_filename = build_filename(shader, shader_options, selected_options)

    # STDERR.puts "  Step #{index}: #{digits}, selected_options = #{selected_options}, defines = #{defines}, output_filename = #{output_filename}"

    variant = { input: shader[:filename], defines: defines, output: output_filename }
    output_data[:variants].push variant
  end

  # STDERR.puts ""
end

puts output_data.to_json
function(CompileShaderVariants target variants_filename)
    # Run the helper script to generate json data for all configured shader variants
    execute_process(
        COMMAND ruby ${CMAKE_SOURCE_DIR}/generate_shader_variants.rb ${variants_filename}
        WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
        OUTPUT_VARIABLE SHADER_VARIANTS
        RESULT_VARIABLE SHADER_VARIANT_RESULT
    )

    if(NOT SHADER_VARIANT_RESULT EQUAL "0")
        message(NOTICE ${SHADER_VARIANT_RESULT})
        message(FATAL_ERROR "Failed to generate shader variant build targets for " ${variants_filename})
    endif()

    string(JSON VARIANT_COUNT LENGTH ${SHADER_VARIANTS} variants)
    message(NOTICE "Generating " ${VARIANT_COUNT} " shader variants from " ${variants_filename})

    # Adjust count as loop index goes from 0 to N
    MATH(EXPR VARIANT_COUNT "${VARIANT_COUNT} - 1")

    foreach(VARIANT_INDEX RANGE ${VARIANT_COUNT})
        string(JSON CURRENT_INTPUT_FILENAME GET ${SHADER_VARIANTS} variants ${VARIANT_INDEX} input)
        string(JSON CURRENT_OUTPUT_FILENAME GET ${SHADER_VARIANTS} variants ${VARIANT_INDEX} output)
        string(JSON CURRENT_DEFINES GET ${SHADER_VARIANTS} variants ${VARIANT_INDEX} defines)

        set(SHADER_TARGET_NAME "${target}_${CURRENT_OUTPUT_FILENAME}")
        CompileShader(${SHADER_TARGET_NAME} ${CURRENT_INTPUT_FILENAME} ${CURRENT_OUTPUT_FILENAME} ${CURRENT_DEFINES})
    endforeach(VARIANT_INDEX RANGE ${VARIANT_COUNT})
endfunction()
# Re-run cmake configure step if the variants file changes
set_property(
    DIRECTORY
    APPEND
    PROPERTY CMAKE_CONFIGURE_DEPENDS ${variants_filename}
)

1 Comment

27 - Apr - 2023

Andy Maloney

SeanHarmer

Sean Harmer

Managing Director KDAB UK

Learn Modern C++

Learn more