///|
fn webgpu_shader_sources() -> Array[String] {
  [
    webgpu_shader2d_color_source(),
    webgpu_shader2d_texture_source(),
    webgpu_shader3d_color_source(),
    webgpu_shader3d_shadow_solid_source(),
    webgpu_shader3d_shadow_lit_source(),
    webgpu_shader3d_shadow_texture_source(),
    webgpu_shader_light_shadow_structs_source(),
    webgpu_shader_shadow_sampling_source(),
    webgpu_shader3d_lit_color_source(),
    webgpu_shader3d_lit_texture_source(),
    webgpu_shader2d_image_material_source(),
  ]
}

///|
fn webgpu_shader2d_image_material_source() -> String {
  (
    #|struct CanvasUniform { size: vec2, _pad: vec2 };
    #|@group(0) @binding(0) var u_canvas: CanvasUniform;
    #|@group(0) @binding(1) var u_sampler: sampler;
    #|@group(0) @binding(2) var u_texture: texture_2d;
    #|struct VSIn {
    #|  @location(0) pos: vec2,
    #|  @location(1) uv: vec2,
    #|  @location(2) tint: vec4,
    #|  @location(3) effect: vec4,
    #|  @location(4) shadow_color: vec4,
    #|  @location(5) shadow_data: vec4,
    #|  @location(6) uv_bounds: vec4,
    #|};
    #|struct VSOut {
    #|  @builtin(position) pos: vec4,
    #|  @location(0) uv: vec2,
    #|  @location(1) tint: vec4,
    #|  @location(2) effect: vec4,
    #|  @location(3) shadow_color: vec4,
    #|  @location(4) shadow_data: vec4,
    #|  @location(5) uv_bounds: vec4,
    #|};
    #|@vertex fn vs_main(v: VSIn) -> VSOut {
    #|  var o: VSOut;
    #|  o.pos = vec4(
    #|    (v.pos.x / u_canvas.size.x) * 2.0 - 1.0,
    #|    1.0 - (v.pos.y / u_canvas.size.y) * 2.0,
    #|    0.0,
    #|    1.0,
    #|  );
    #|  o.uv = v.uv;
    #|  o.tint = v.tint;
    #|  o.effect = v.effect;
    #|  o.shadow_color = v.shadow_color;
    #|  o.shadow_data = v.shadow_data;
    #|  o.uv_bounds = v.uv_bounds;
    #|  return o;
    #|}
    #|fn inside(uv: vec2, bounds: vec4) -> bool {
    #|  return all(uv >= bounds.xy) && all(uv <= bounds.zw);
    #|}
    #|fn source_alpha(uv: vec2, bounds: vec4) -> f32 {
    #|  if (!inside(uv, bounds)) { return 0.0; }
    #|  return textureSampleLevel(u_texture, u_sampler, uv, 0.0).a;
    #|}
    #|@fragment fn fs_main(v: VSOut) -> @location(0) vec4 {
    #|  var shadow_alpha = 0.0;
    #|  if (v.effect.w > 0.5) {
    #|    let center = v.uv - v.shadow_data.xy;
    #|    for (var y = -2; y <= 2; y = y + 1) {
    #|      for (var x = -2; x <= 2; x = x + 1) {
    #|        let delta = vec2(f32(x), f32(y)) * v.shadow_data.zw * 0.5;
    #|        shadow_alpha += source_alpha(center + delta, v.uv_bounds);
    #|      }
    #|    }
    #|    shadow_alpha = shadow_alpha * 0.04 * v.shadow_color.a * v.effect.x * v.tint.a;
    #|  }
    #|  var source = vec4(0.0);
    #|  if (inside(v.uv, v.uv_bounds)) {
    #|    source = textureSampleLevel(u_texture, u_sampler, v.uv, 0.0) * v.tint;
    #|    let luma = dot(source.rgb, vec3(0.2126, 0.7152, 0.0722));
    #|    source = vec4(
    #|      mix(vec3(luma), source.rgb, v.effect.z) * v.effect.y,
    #|      source.a * v.effect.x,
    #|    );
    #|  }
    #|  let shadow = vec4(v.shadow_color.rgb, shadow_alpha);
    #|  let out_alpha = source.a + shadow.a * (1.0 - source.a);
    #|  var out_rgb = vec3(0.0);
    #|  if (out_alpha > 0.000001) {
    #|    out_rgb = (
    #|      source.rgb * source.a +
    #|      shadow.rgb * shadow.a * (1.0 - source.a)
    #|    ) / out_alpha;
    #|  }
    #|  return vec4(out_rgb, out_alpha);
    #|}
    #|
  )
}

///|
fn webgpu_shader2d_color_source() -> String {
  (
    #|struct CanvasUniform { size: vec2, _pad: vec2 };
    #|@group(0) @binding(0) var u_canvas: CanvasUniform;
    #|struct VSIn {
    #|  @location(0) pos: vec2,
    #|  @location(1) color: vec4,
    #|};
    #|struct VSOut {
    #|  @builtin(position) pos: vec4,
    #|  @location(0) color: vec4,
    #|};
    #|@vertex fn vs_main(v: VSIn) -> VSOut {
    #|  var o: VSOut;
    #|  let nx = (v.pos.x / u_canvas.size.x) * 2.0 - 1.0;
    #|  let ny = 1.0 - (v.pos.y / u_canvas.size.y) * 2.0;
    #|  o.pos = vec4(nx, ny, 0.0, 1.0);
    #|  o.color = v.color;
    #|  return o;
    #|}
    #|@fragment fn fs_main(v: VSOut) -> @location(0) vec4 {
    #|  return v.color;
    #|}
    #|
  )
}

///|
fn webgpu_shader3d_color_source() -> String {
  (
    #|struct CameraUniform {
    #|  view_proj: mat4x4,
    #|};
    #|@group(0) @binding(0) var u_camera: CameraUniform;
    #|struct VSIn {
    #|  @location(0) pos: vec3,
    #|  @location(1) color: vec4,
    #|};
    #|struct VSOut {
    #|  @builtin(position) pos: vec4,
    #|  @location(0) color: vec4,
    #|};
    #|@vertex fn vs_main(v: VSIn) -> VSOut {
    #|  var o: VSOut;
    #|  o.pos = u_camera.view_proj * vec4(v.pos, 1.0);
    #|  o.color = v.color;
    #|  return o;
    #|}
    #|@fragment fn fs_main(v: VSOut) -> @location(0) vec4 {
    #|  return v.color;
    #|}
    #|
  )
}

///|
fn webgpu_shader3d_shadow_solid_source() -> String {
  (
    #|struct ShadowCameraUniform { light_view_proj: mat4x4, };
    #|@group(0) @binding(0) var u_shadow_camera: ShadowCameraUniform;
    #|struct VSIn { @location(0) pos: vec3, @location(1) color: vec4, };
    #|struct VSOut { @builtin(position) pos: vec4, };
    #|@vertex fn vs_main(v: VSIn) -> VSOut {
    #|  var o: VSOut;
    #|  o.pos = u_shadow_camera.light_view_proj * vec4(v.pos, 1.0);
    #|  return o;
    #|}
    #|
  )
}

///|
fn webgpu_shader3d_shadow_lit_source() -> String {
  (
    #|struct ShadowCameraUniform { light_view_proj: mat4x4, };
    #|@group(0) @binding(0) var u_shadow_camera: ShadowCameraUniform;
    #|struct VSIn {
    #|  @location(0) pos: vec3,
    #|  @location(2) color: vec4,
    #|  @location(4) material: vec4,
    #|};
    #|struct VSInInstanced {
    #|  @location(0) pos: vec3,
    #|  @location(2) color: vec4,
    #|  @location(4) material: vec4,
    #|  @location(5) instance_translation: vec3,
    #|  @location(6) instance_rotation: vec4,
    #|  @location(7) instance_scale: vec3,
    #|};
    #|struct VSOut {
    #|  @builtin(position) pos: vec4,
    #|  @location(0) color: vec4,
    #|  @location(1) material: vec4,
    #|};
    #|@vertex fn vs_main(v: VSIn) -> VSOut {
    #|  var o: VSOut;
    #|  o.pos = u_shadow_camera.light_view_proj * vec4(v.pos, 1.0);
    #|  o.color = v.color;
    #|  o.material = v.material;
    #|  return o;
    #|}
    #|
  ) +
  webgpu_shader_rotate_quat_source() +
  (
    #|@vertex fn vs_instanced(v: VSInInstanced) -> VSOut {
    #|  var o: VSOut;
    #|  let world_pos = rotate_quat(v.pos * v.instance_scale, v.instance_rotation) + v.instance_translation;
    #|  o.pos = u_shadow_camera.light_view_proj * vec4(world_pos, 1.0);
    #|  o.color = v.color;
    #|  o.material = v.material;
    #|  return o;
    #|}
    #|@fragment fn fs_main(v: VSOut) {
    #|  let alpha_mode = i32(round(v.material.x));
    #|  let alpha_cutoff = v.material.y;
    #|  if (alpha_mode == 2) { discard; }
    #|  if (alpha_mode == 1 && v.color.a < alpha_cutoff) { discard; }
    #|}
    #|
  )
}

///|
fn webgpu_shader3d_shadow_texture_source() -> String {
  (
    #|struct ShadowCameraUniform { light_view_proj: mat4x4, };
    #|@group(0) @binding(0) var u_shadow_camera: ShadowCameraUniform;
    #|@group(0) @binding(1) var u_shadow_base_sampler: sampler;
    #|@group(0) @binding(2) var u_shadow_base_texture: texture_2d;
    #|struct VSIn {
    #|  @location(0) pos: vec3,
    #|  @location(2) base_uv: vec2,
    #|  @location(7) color: vec4,
    #|  @location(10) material: vec4,
    #|};
    #|struct VSOut {
    #|  @builtin(position) pos: vec4,
    #|  @location(0) base_uv: vec2,
    #|  @location(1) color: vec4,
    #|  @location(2) material: vec4,
    #|};
    #|@vertex fn vs_main(v: VSIn) -> VSOut {
    #|  var o: VSOut;
    #|  o.pos = u_shadow_camera.light_view_proj * vec4(v.pos, 1.0);
    #|  o.base_uv = v.base_uv;
    #|  o.color = v.color;
    #|  o.material = v.material;
    #|  return o;
    #|}
    #|@fragment fn fs_main(v: VSOut) {
    #|  let alpha_mode = i32(round(v.material.x));
    #|  let alpha_cutoff = v.material.y;
    #|  if (alpha_mode == 2) { discard; }
    #|  let albedo = textureSample(u_shadow_base_texture, u_shadow_base_sampler, v.base_uv) * v.color;
    #|  if (alpha_mode == 1 && albedo.a < alpha_cutoff) { discard; }
    #|}
    #|
  )
}

///|
fn webgpu_shader_rotate_quat_source() -> String {
  (
    #|fn rotate_quat(value: vec3, quat: vec4) -> vec3 {
    #|  let len = length(quat);
    #|  var q = quat;
    #|  if (len <= 0.00001) {
    #|    q = vec4(0.0, 0.0, 0.0, 1.0);
    #|  } else {
    #|    q = quat / len;
    #|  }
    #|  let u = q.xyz;
    #|  let s = q.w;
    #|  return 2.0 * dot(u, value) * u + (s * s - dot(u, u)) * value + 2.0 * s * cross(u, value);
    #|}
    #|
  )
}

///|
fn webgpu_shader_light_shadow_structs_source() -> String {
  (
    #|struct LightUniform {
    #|  ambient_directional_count: vec4,
    #|  point_spot_count: vec4,
    #|  view_position: vec4,
    #|  view_forward: vec4,
    #|  directional_data: array, 8>,
    #|  point_data: array, 16>,
    #|  spot_data: array, 16>,
    #|};
    #|struct ShadowUniform {
    #|  directional_spot_texel: vec4,
    #|  point_texel: vec4,
    #|  directional_meta: array, 4>,
    #|  directional_bounds: array, 16>,
    #|  directional_rects: array, 16>,
    #|  directional_matrices: array, 16>,
    #|  spot_meta: array, 4>,
    #|  spot_rects: array, 4>,
    #|  spot_matrices: array, 4>,
    #|  point_meta: array, 8>,
    #|  point_rects: array, 48>,
    #|  point_matrices: array, 48>,
    #|};
    #|
    #|
  )
}

///|
fn webgpu_shader_shadow_sampling_source() -> String {
  (
    #|fn saturate(value: f32) -> f32 { return clamp(value, 0.0, 1.0); }
    #|fn point_light_attenuation(distance: f32, range: f32) -> f32 {
    #|  let safe_range = max(range, 0.0001);
    #|  return saturate(1.0 - distance / safe_range);
    #|}
    #|fn safe_normalize(value: vec3, fallback: vec3) -> vec3 {
    #|  let len = length(value);
    #|  if (len <= 0.0001) { return fallback; }
    #|  return value / len;
    #|}
    #|fn shadow_local_uv(ndc: vec3) -> vec2 {
    #|  return vec2(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
    #|}
    #|fn sample_directional_shadow_compare(index: i32, uv: vec2, depth_ref: f32) -> f32 {
    #|  if (index == 0) { return textureSampleCompareLevel(u_directional_shadow0, u_shadow_sampler, uv, depth_ref); }
    #|  if (index == 1) { return textureSampleCompareLevel(u_directional_shadow1, u_shadow_sampler, uv, depth_ref); }
    #|  if (index == 2) { return textureSampleCompareLevel(u_directional_shadow2, u_shadow_sampler, uv, depth_ref); }
    #|  if (index == 3) { return textureSampleCompareLevel(u_directional_shadow3, u_shadow_sampler, uv, depth_ref); }
    #|  return 1.0;
    #|}
    #|fn sample_directional_shadow(light_index: i32, world_pos: vec3, normal: vec3, light_dir: vec3, receive_shadows: bool) -> f32 {
    #|  if (!receive_shadows || light_index < 0 || light_index >= 4) { return 1.0; }
    #|  let shadow_meta = u_shadow.directional_meta[light_index];
    #|  if (shadow_meta.x < 0.5) { return 1.0; }
    #|  let cascade_count = clamp(i32(round(shadow_meta.y)), 0, 4);
    #|  if (cascade_count <= 0) { return 1.0; }
    #|  let view_depth = dot(world_pos - u_light.view_position.xyz, u_light.view_forward.xyz);
    #|  let base_index = light_index * 4;
    #|  let first_near = u_shadow.directional_bounds[base_index].x;
    #|  let last_far = u_shadow.directional_bounds[base_index + cascade_count - 1].y;
    #|  if (view_depth < first_near || view_depth > last_far) { return 1.0; }
    #|  var selected = cascade_count - 1;
    #|  for (var cascade_index = 0; cascade_index < 4; cascade_index = cascade_index + 1) {
    #|    if (cascade_index >= cascade_count) { continue; }
    #|    if (view_depth <= u_shadow.directional_bounds[base_index + cascade_index].y) {
    #|      selected = cascade_index;
    #|      break;
    #|    }
    #|  }
    #|  let texel = u_shadow.directional_spot_texel.xy;
    #|  let ndotl = max(dot(normal, light_dir), 0.0);
    #|  let bias = max(0.0, shadow_meta.z) + max(0.0, shadow_meta.w) * max(texel.x, texel.y) * (1.0 - ndotl);
    #|  let filter_radius = 1.25;
    #|  let matrix_index = base_index + selected;
    #|  let clip = u_shadow.directional_matrices[matrix_index] * vec4(world_pos, 1.0);
    #|  if (abs(clip.w) <= 0.00001) { return 1.0; }
    #|  let ndc = clip.xyz / clip.w;
    #|  let local_uv = shadow_local_uv(ndc);
    #|  let depth = ndc.z;
    #|  if (depth <= 0.0 || depth >= 1.0 || any(local_uv < vec2(0.0)) || any(local_uv > vec2(1.0))) { return 1.0; }
    #|  let rect = u_shadow.directional_rects[matrix_index];
    #|  var lit = 0.0;
    #|  var total = 0.0;
    #|  for (var x = -1; x <= 1; x = x + 1) {
    #|    for (var y = -1; y <= 1; y = y + 1) {
    #|      let weight = 1.0 / (1.0 + abs(f32(x)) + abs(f32(y)));
    #|      let sample_uv = clamp(local_uv + vec2(f32(x), f32(y)) * texel * filter_radius, vec2(0.0), vec2(1.0));
    #|      let atlas_uv = rect.xy + sample_uv * rect.zw;
    #|      lit = lit + sample_directional_shadow_compare(light_index, atlas_uv, depth - bias) * weight;
    #|      total = total + weight;
    #|    }
    #|  }
    #|  var shadow = select(1.0, lit / total, total > 0.00001);
    #|  if (selected > 0) {
    #|    let previous = selected - 1;
    #|    let overlap_near = u_shadow.directional_bounds[base_index + selected].x;
    #|    let overlap_far = u_shadow.directional_bounds[base_index + previous].y;
    #|    if (overlap_near < overlap_far && view_depth >= overlap_near && view_depth <= overlap_far) {
    #|      let prev_index = base_index + previous;
    #|      let prev_clip = u_shadow.directional_matrices[prev_index] * vec4(world_pos, 1.0);
    #|      if (abs(prev_clip.w) > 0.00001) {
    #|        let prev_ndc = prev_clip.xyz / prev_clip.w;
    #|        let prev_local_uv = shadow_local_uv(prev_ndc);
    #|        let prev_depth = prev_ndc.z;
    #|        if (!(prev_depth <= 0.0 || prev_depth >= 1.0 || any(prev_local_uv < vec2(0.0)) || any(prev_local_uv > vec2(1.0)))) {
    #|          let prev_rect = u_shadow.directional_rects[prev_index];
    #|          var prev_lit = 0.0;
    #|          var prev_total = 0.0;
    #|          for (var px = -1; px <= 1; px = px + 1) {
    #|            for (var py = -1; py <= 1; py = py + 1) {
    #|              let prev_weight = 1.0 / (1.0 + abs(f32(px)) + abs(f32(py)));
    #|              let prev_sample_uv = clamp(prev_local_uv + vec2(f32(px), f32(py)) * texel * filter_radius, vec2(0.0), vec2(1.0));
    #|              let prev_atlas_uv = prev_rect.xy + prev_sample_uv * prev_rect.zw;
    #|              prev_lit = prev_lit + sample_directional_shadow_compare(light_index, prev_atlas_uv, prev_depth - bias) * prev_weight;
    #|              prev_total = prev_total + prev_weight;
    #|            }
    #|          }
    #|          let previous_shadow = select(1.0, prev_lit / prev_total, prev_total > 0.00001);
    #|          let blend = clamp((view_depth - overlap_near) / (overlap_far - overlap_near), 0.0, 1.0);
    #|          shadow = mix(previous_shadow, shadow, blend);
    #|        }
    #|      }
    #|    }
    #|  }
    #|  return shadow;
    #|}
    #|fn sample_spot_shadow(index: i32, world_pos: vec3, normal: vec3, light_dir: vec3, receive_shadows: bool) -> f32 {
    #|  if (!receive_shadows || index < 0 || index >= 4) { return 1.0; }
    #|  let shadow_meta = u_shadow.spot_meta[index];
    #|  if (shadow_meta.x < 0.5) { return 1.0; }
    #|  let clip = u_shadow.spot_matrices[index] * vec4(world_pos, 1.0);
    #|  if (abs(clip.w) <= 0.00001) { return 1.0; }
    #|  let ndc = clip.xyz / clip.w;
    #|  let local_uv = shadow_local_uv(ndc);
    #|  let depth = ndc.z;
    #|  if (depth <= 0.0 || depth >= 1.0 || any(local_uv < vec2(0.0)) || any(local_uv > vec2(1.0))) { return 1.0; }
    #|  let rect = u_shadow.spot_rects[index];
    #|  let texel = u_shadow.directional_spot_texel.zw;
    #|  let ndotl = max(dot(normal, light_dir), 0.0);
    #|  let bias = max(0.0, shadow_meta.y) + max(0.0, shadow_meta.z) * max(texel.x, texel.y) * (1.0 - ndotl);
    #|  var lit = 0.0;
    #|  var total = 0.0;
    #|  for (var x = -1; x <= 1; x = x + 1) {
    #|    for (var y = -1; y <= 1; y = y + 1) {
    #|      let weight = 1.0 / (1.0 + abs(f32(x)) + abs(f32(y)));
    #|      let sample_uv = clamp(local_uv + vec2(f32(x), f32(y)) * texel * 1.25, vec2(0.0), vec2(1.0));
    #|      let atlas_uv = rect.xy + sample_uv * rect.zw;
    #|      lit = lit + textureSampleCompareLevel(u_spot_shadow_atlas, u_shadow_sampler, atlas_uv, depth - bias) * weight;
    #|      total = total + weight;
    #|    }
    #|  }
    #|  return select(1.0, lit / total, total > 0.00001);
    #|}
    #|fn select_point_shadow_face(dir: vec3) -> i32 {
    #|  let abs_dir = abs(dir);
    #|  if (abs_dir.x >= abs_dir.y && abs_dir.x >= abs_dir.z) { return select(1, 0, dir.x >= 0.0); }
    #|  if (abs_dir.y >= abs_dir.x && abs_dir.y >= abs_dir.z) { return select(3, 2, dir.y >= 0.0); }
    #|  return select(5, 4, dir.z >= 0.0);
    #|}
    #|fn sample_point_shadow(index: i32, world_pos: vec3, normal: vec3, light_dir: vec3, light_to_fragment: vec3, receive_shadows: bool) -> f32 {
    #|  if (!receive_shadows || index < 0 || index >= 8) { return 1.0; }
    #|  let shadow_meta = u_shadow.point_meta[index];
    #|  if (shadow_meta.x < 0.5) { return 1.0; }
    #|  let face = select_point_shadow_face(light_to_fragment);
    #|  let matrix_index = index * 6 + face;
    #|  let clip = u_shadow.point_matrices[matrix_index] * vec4(world_pos, 1.0);
    #|  if (abs(clip.w) <= 0.00001) { return 1.0; }
    #|  let ndc = clip.xyz / clip.w;
    #|  let local_uv = shadow_local_uv(ndc);
    #|  let depth = ndc.z;
    #|  if (depth <= 0.0 || depth >= 1.0 || any(local_uv < vec2(0.0)) || any(local_uv > vec2(1.0))) { return 1.0; }
    #|  let rect = u_shadow.point_rects[matrix_index];
    #|  let texel = u_shadow.point_texel.xy;
    #|  let ndotl = max(dot(normal, light_dir), 0.0);
    #|  let bias = max(0.0, shadow_meta.y) + max(0.0, shadow_meta.z) * max(texel.x, texel.y) * (1.0 - ndotl);
    #|  var lit = 0.0;
    #|  var total = 0.0;
    #|  for (var x = -1; x <= 1; x = x + 1) {
    #|    for (var y = -1; y <= 1; y = y + 1) {
    #|      let weight = 1.0 / (1.0 + abs(f32(x)) + abs(f32(y)));
    #|      let sample_uv = clamp(local_uv + vec2(f32(x), f32(y)) * texel, vec2(0.0), vec2(1.0));
    #|      let atlas_uv = rect.xy + sample_uv * rect.zw;
    #|      lit = lit + textureSampleCompareLevel(u_point_shadow_atlas, u_shadow_sampler, atlas_uv, depth - bias) * weight;
    #|      total = total + weight;
    #|    }
    #|  }
    #|  return select(1.0, lit / total, total > 0.00001);
    #|}
    #|
    #|
  )
}

///|
fn webgpu_shader3d_lit_color_source() -> String {
  (
    #|struct CameraUniform { view_proj: mat4x4, };
    #|
  ) +
  webgpu_shader_light_shadow_structs_source() +
  (
    #|@group(0) @binding(0) var u_camera: CameraUniform;
    #|@group(0) @binding(1) var u_light: LightUniform;
    #|@group(0) @binding(2) var u_shadow: ShadowUniform;
    #|@group(0) @binding(3) var u_shadow_sampler: sampler_comparison;
    #|@group(0) @binding(4) var u_directional_shadow0: texture_depth_2d;
    #|@group(0) @binding(5) var u_directional_shadow1: texture_depth_2d;
    #|@group(0) @binding(6) var u_directional_shadow2: texture_depth_2d;
    #|@group(0) @binding(7) var u_directional_shadow3: texture_depth_2d;
    #|@group(0) @binding(8) var u_spot_shadow_atlas: texture_depth_2d;
    #|@group(0) @binding(9) var u_point_shadow_atlas: texture_depth_2d;
    #|struct VSIn {
    #|  @location(0) pos: vec3,
    #|  @location(1) normal: vec3,
    #|  @location(2) color: vec4,
    #|  @location(3) emissive: vec3,
    #|  @location(4) material: vec4,
    #|};
    #|struct VSInInstanced {
    #|  @location(0) pos: vec3,
    #|  @location(1) normal: vec3,
    #|  @location(2) color: vec4,
    #|  @location(3) emissive: vec3,
    #|  @location(4) material: vec4,
    #|  @location(5) instance_translation: vec3,
    #|  @location(6) instance_rotation: vec4,
    #|  @location(7) instance_scale: vec3,
    #|  @location(8) instance_receive_shadows: f32,
    #|};
    #|struct VSOut {
    #|  @builtin(position) pos: vec4,
    #|  @location(0) world_pos: vec3,
    #|  @location(1) normal: vec3,
    #|  @location(2) color: vec4,
    #|  @location(3) emissive: vec3,
    #|  @location(4) material: vec4,
    #|};
    #|@vertex fn vs_main(v: VSIn) -> VSOut {
    #|  var o: VSOut;
    #|  o.pos = u_camera.view_proj * vec4(v.pos, 1.0);
    #|  o.world_pos = v.pos;
    #|  o.normal = normalize(v.normal);
    #|  o.color = v.color;
    #|  o.emissive = v.emissive;
    #|  o.material = v.material;
    #|  return o;
    #|}
    #|
  ) +
  webgpu_shader_rotate_quat_source() +
  (
    #|fn safe_scale_component(value: f32) -> f32 {
    #|  return select(value, 1.0, abs(value) <= 0.00001);
    #|}
    #|@vertex fn vs_instanced(v: VSInInstanced) -> VSOut {
    #|  var o: VSOut;
    #|  let scaled_pos = v.pos * v.instance_scale;
    #|  let world_pos = rotate_quat(scaled_pos, v.instance_rotation) + v.instance_translation;
    #|  let inverse_scale = vec3(
    #|    safe_scale_component(v.instance_scale.x),
    #|    safe_scale_component(v.instance_scale.y),
    #|    safe_scale_component(v.instance_scale.z),
    #|  );
    #|  let scaled_normal = v.normal / inverse_scale;
    #|  let world_normal = normalize(rotate_quat(scaled_normal, v.instance_rotation));
    #|  var material = v.material;
    #|  material.w = v.instance_receive_shadows;
    #|  o.pos = u_camera.view_proj * vec4(world_pos, 1.0);
    #|  o.world_pos = world_pos;
    #|  o.normal = world_normal;
    #|  o.color = v.color;
    #|  o.emissive = v.emissive;
    #|  o.material = material;
    #|  return o;
    #|}
    #|
  ) +
  webgpu_shader_shadow_sampling_source() +
  (
    #|fn apply_lighting(world_pos: vec3, normal: vec3, receive_shadows: bool) -> vec3 {
    #|  let n = normalize(normal);
    #|  var lit = u_light.ambient_directional_count.xyz;
    #|  let directional_count = clamp(i32(round(u_light.ambient_directional_count.w)), 0, 4);
    #|  for (var idx = 0; idx < 4; idx = idx + 1) {
    #|    if (idx >= directional_count) { continue; }
    #|    let base = idx * 2;
    #|    let direction_intensity = u_light.directional_data[base];
    #|    let color_data = u_light.directional_data[base + 1];
    #|    let light_dir = normalize(-direction_intensity.xyz);
    #|    let lambert = max(dot(n, light_dir), 0.0);
    #|    let shadow = sample_directional_shadow(idx, world_pos, normal, light_dir, receive_shadows);
    #|    lit = lit + color_data.xyz * direction_intensity.w * lambert * shadow;
    #|  }
    #|  let point_count = clamp(i32(round(u_light.point_spot_count.x)), 0, 8);
    #|  for (var idx = 0; idx < 8; idx = idx + 1) {
    #|    if (idx >= point_count) { continue; }
    #|    let base = idx * 2;
    #|    let position_intensity = u_light.point_data[base];
    #|    let color_range = u_light.point_data[base + 1];
    #|    let offset = position_intensity.xyz - world_pos;
    #|    let distance = length(offset);
    #|    if (distance <= 0.0001) { continue; }
    #|    let light_dir = offset / distance;
    #|    let lambert = max(dot(n, light_dir), 0.0);
    #|    let attenuation = point_light_attenuation(distance, color_range.w) *
    #|      sample_point_shadow(idx, world_pos, normal, light_dir, world_pos - position_intensity.xyz, receive_shadows);
    #|    lit = lit + color_range.xyz * position_intensity.w * lambert * attenuation;
    #|  }
    #|  let spot_count = clamp(i32(round(u_light.point_spot_count.y)), 0, 4);
    #|  for (var idx = 0; idx < 4; idx = idx + 1) {
    #|    if (idx >= spot_count) { continue; }
    #|    let base = idx * 4;
    #|    let position_intensity = u_light.spot_data[base];
    #|    let color_range = u_light.spot_data[base + 1];
    #|    let direction_inner = u_light.spot_data[base + 2];
    #|    let outer_cos = u_light.spot_data[base + 3].x;
    #|    let offset = position_intensity.xyz - world_pos;
    #|    let distance = length(offset);
    #|    if (distance <= 0.0001) { continue; }
    #|    let light_dir = offset / distance;
    #|    let to_fragment = -light_dir;
    #|    let spot_direction = normalize(direction_inner.xyz);
    #|    let spot_cos = dot(spot_direction, to_fragment);
    #|    let inner_cos = direction_inner.w;
    #|    let spot_denom = max(inner_cos - outer_cos, 0.0001);
    #|    let spot_factor = saturate((spot_cos - outer_cos) / spot_denom);
    #|    if (spot_factor <= 0.0) { continue; }
    #|    let lambert = max(dot(n, light_dir), 0.0);
    #|    let attenuation = point_light_attenuation(distance, color_range.w) *
    #|      spot_factor * sample_spot_shadow(idx, world_pos, normal, light_dir, receive_shadows);
    #|    lit = lit + color_range.xyz * position_intensity.w * lambert * attenuation;
    #|  }
    #|  return lit;
    #|}
    #|@fragment fn fs_main(v: VSOut) -> @location(0) vec4 {
    #|  var albedo = v.color;
    #|  let alpha_mode = i32(round(v.material.x));
    #|  let alpha_cutoff = v.material.y;
    #|  let unlit = v.material.z >= 0.5;
    #|  let receive_shadows = v.material.w >= 0.5;
    #|  if (alpha_mode == 0) {
    #|    albedo.a = 1.0;
    #|  } else if (alpha_mode == 1) {
    #|    if (albedo.a < alpha_cutoff) { discard; }
    #|    albedo.a = 1.0;
    #|  } else if (albedo.a <= 0.0001) {
    #|    discard;
    #|  }
    #|  if (unlit) { return albedo; }
    #|  let lit = apply_lighting(v.world_pos, v.normal, receive_shadows);
    #|  return vec4(albedo.rgb * lit + v.emissive, albedo.a);
    #|}
    #|
  )
}

///|
fn webgpu_shader3d_lit_texture_source() -> String {
  (
    #|struct CameraUniform { view_proj: mat4x4, };
    #|
  ) +
  webgpu_shader_light_shadow_structs_source() +
  (
    #|@group(0) @binding(0) var u_camera: CameraUniform;
    #|@group(0) @binding(1) var u_light: LightUniform;
    #|@group(0) @binding(2) var u_shadow: ShadowUniform;
    #|@group(0) @binding(3) var u_shadow_sampler: sampler_comparison;
    #|@group(0) @binding(4) var u_directional_shadow0: texture_depth_2d;
    #|@group(0) @binding(5) var u_directional_shadow1: texture_depth_2d;
    #|@group(0) @binding(6) var u_directional_shadow2: texture_depth_2d;
    #|@group(0) @binding(7) var u_directional_shadow3: texture_depth_2d;
    #|@group(0) @binding(8) var u_spot_shadow_atlas: texture_depth_2d;
    #|@group(0) @binding(9) var u_point_shadow_atlas: texture_depth_2d;
    #|@group(0) @binding(10) var u_base_sampler: sampler;
    #|@group(0) @binding(11) var u_texture: texture_2d;
    #|@group(0) @binding(12) var u_emissive_sampler: sampler;
    #|@group(0) @binding(13) var u_emissive_texture: texture_2d;
    #|@group(0) @binding(14) var u_metallic_roughness_sampler: sampler;
    #|@group(0) @binding(15) var u_metallic_roughness_texture: texture_2d;
    #|@group(0) @binding(16) var u_occlusion_sampler: sampler;
    #|@group(0) @binding(17) var u_occlusion_texture: texture_2d;
    #|@group(0) @binding(18) var u_normal_sampler: sampler;
    #|@group(0) @binding(19) var u_normal_texture: texture_2d;
    #|struct VSIn {
    #|  @location(0) pos: vec3,
    #|  @location(1) normal: vec3,
    #|  @location(2) base_uv: vec2,
    #|  @location(3) emissive_uv: vec2,
    #|  @location(4) metallic_roughness_uv: vec2,
    #|  @location(5) occlusion_uv: vec2,
    #|  @location(6) normal_uv: vec2,
    #|  @location(7) color: vec4,
    #|  @location(8) emissive: vec3,
    #|  @location(9) tangent: vec4,
    #|  @location(10) material: vec4,
    #|  @location(11) surface: vec4,
    #|};
    #|struct VSOut {
    #|  @builtin(position) pos: vec4,
    #|  @location(0) world_pos: vec3,
    #|  @location(1) normal: vec3,
    #|  @location(2) base_uv: vec2,
    #|  @location(3) emissive_uv: vec2,
    #|  @location(4) metallic_roughness_uv: vec2,
    #|  @location(5) occlusion_uv: vec2,
    #|  @location(6) normal_uv: vec2,
    #|  @location(7) color: vec4,
    #|  @location(8) emissive: vec3,
    #|  @location(9) tangent: vec4,
    #|  @location(10) material: vec4,
    #|  @location(11) surface: vec4,
    #|};
    #|@vertex fn vs_main(v: VSIn) -> VSOut {
    #|  var o: VSOut;
    #|  o.pos = u_camera.view_proj * vec4(v.pos, 1.0);
    #|  o.world_pos = v.pos;
    #|  o.normal = normalize(v.normal);
    #|  o.base_uv = v.base_uv;
    #|  o.emissive_uv = v.emissive_uv;
    #|  o.metallic_roughness_uv = v.metallic_roughness_uv;
    #|  o.occlusion_uv = v.occlusion_uv;
    #|  o.normal_uv = v.normal_uv;
    #|  o.color = v.color;
    #|  o.emissive = v.emissive;
    #|  o.tangent = v.tangent;
    #|  o.material = v.material;
    #|  o.surface = v.surface;
    #|  return o;
    #|}
    #|
  ) +
  webgpu_shader_shadow_sampling_source() +
  (
    #|fn apply_lighting(world_pos: vec3, shading_normal: vec3, geometric_normal: vec3, view_dir: vec3, roughness: f32, metallic: f32, receive_shadows: bool) -> vec3 {
    #|  let n = normalize(shading_normal);
    #|  var lit = u_light.ambient_directional_count.xyz;
    #|  var specular = vec3(0.0);
    #|  let shininess = mix(96.0, 8.0, clamp(roughness, 0.0, 1.0));
    #|  let specular_strength = mix(0.04, 1.0, clamp(metallic, 0.0, 1.0));
    #|  let directional_count = clamp(i32(round(u_light.ambient_directional_count.w)), 0, 4);
    #|  for (var idx = 0; idx < 4; idx = idx + 1) {
    #|    if (idx >= directional_count) { continue; }
    #|    let base = idx * 2;
    #|    let direction_intensity = u_light.directional_data[base];
    #|    let color_data = u_light.directional_data[base + 1];
    #|    let light_dir = normalize(-direction_intensity.xyz);
    #|    let lambert = max(dot(n, light_dir), 0.0);
    #|    let shadow = sample_directional_shadow(idx, world_pos, geometric_normal, light_dir, receive_shadows);
    #|    lit = lit + color_data.xyz * direction_intensity.w * lambert * shadow;
    #|    if (lambert > 0.0) {
    #|      let halfway = safe_normalize(light_dir + view_dir, view_dir);
    #|      let spec = pow(max(dot(n, halfway), 0.0), shininess) * specular_strength;
    #|      specular = specular + color_data.xyz * direction_intensity.w * spec * shadow;
    #|    }
    #|  }
    #|  let point_count = clamp(i32(round(u_light.point_spot_count.x)), 0, 8);
    #|  for (var idx = 0; idx < 8; idx = idx + 1) {
    #|    if (idx >= point_count) { continue; }
    #|    let base = idx * 2;
    #|    let position_intensity = u_light.point_data[base];
    #|    let color_range = u_light.point_data[base + 1];
    #|    let offset = position_intensity.xyz - world_pos;
    #|    let distance = length(offset);
    #|    if (distance <= 0.0001) { continue; }
    #|    let light_dir = offset / distance;
    #|    let lambert = max(dot(n, light_dir), 0.0);
    #|    let shadow = sample_point_shadow(idx, world_pos, geometric_normal, light_dir, world_pos - position_intensity.xyz, receive_shadows);
    #|    let attenuation = point_light_attenuation(distance, color_range.w) * shadow;
    #|    lit = lit + color_range.xyz * position_intensity.w * lambert * attenuation;
    #|    if (lambert > 0.0 && attenuation > 0.0) {
    #|      let halfway = safe_normalize(light_dir + view_dir, view_dir);
    #|      let spec = pow(max(dot(n, halfway), 0.0), shininess) * specular_strength;
    #|      specular = specular + color_range.xyz * position_intensity.w * spec * attenuation;
    #|    }
    #|  }
    #|  let spot_count = clamp(i32(round(u_light.point_spot_count.y)), 0, 4);
    #|  for (var idx = 0; idx < 4; idx = idx + 1) {
    #|    if (idx >= spot_count) { continue; }
    #|    let base = idx * 4;
    #|    let position_intensity = u_light.spot_data[base];
    #|    let color_range = u_light.spot_data[base + 1];
    #|    let direction_inner = u_light.spot_data[base + 2];
    #|    let outer_cos = u_light.spot_data[base + 3].x;
    #|    let offset = position_intensity.xyz - world_pos;
    #|    let distance = length(offset);
    #|    if (distance <= 0.0001) { continue; }
    #|    let light_dir = offset / distance;
    #|    let to_fragment = -light_dir;
    #|    let spot_direction = normalize(direction_inner.xyz);
    #|    let spot_cos = dot(spot_direction, to_fragment);
    #|    let inner_cos = direction_inner.w;
    #|    let spot_denom = max(inner_cos - outer_cos, 0.0001);
    #|    let spot_factor = saturate((spot_cos - outer_cos) / spot_denom);
    #|    if (spot_factor <= 0.0) { continue; }
    #|    let lambert = max(dot(n, light_dir), 0.0);
    #|    let shadow = sample_spot_shadow(idx, world_pos, geometric_normal, light_dir, receive_shadows);
    #|    let attenuation = point_light_attenuation(distance, color_range.w) * spot_factor * shadow;
    #|    lit = lit + color_range.xyz * position_intensity.w * lambert * attenuation;
    #|    if (lambert > 0.0 && attenuation > 0.0) {
    #|      let halfway = safe_normalize(light_dir + view_dir, view_dir);
    #|      let spec = pow(max(dot(n, halfway), 0.0), shininess) * specular_strength;
    #|      specular = specular + color_range.xyz * position_intensity.w * spec * attenuation;
    #|    }
    #|  }
    #|  return lit + specular;
    #|}
    #|@fragment fn fs_main(v: VSOut) -> @location(0) vec4 {
    #|  var albedo = textureSample(u_texture, u_base_sampler, v.base_uv) * v.color;
    #|  let emissive_sample = textureSample(u_emissive_texture, u_emissive_sampler, v.emissive_uv).rgb;
    #|  let mr_sample = textureSample(u_metallic_roughness_texture, u_metallic_roughness_sampler, v.metallic_roughness_uv);
    #|  let occlusion_sample = textureSample(u_occlusion_texture, u_occlusion_sampler, v.occlusion_uv);
    #|  let normal_sample = textureSample(u_normal_texture, u_normal_sampler, v.normal_uv).xyz * 2.0 - vec3(1.0, 1.0, 1.0);
    #|  let alpha_mode = i32(round(v.material.x));
    #|  let alpha_cutoff = v.material.y;
    #|  let unlit = v.surface.z >= 0.5;
    #|  let receive_shadows = v.surface.w >= 0.5;
    #|  if (alpha_mode == 0) {
    #|    albedo.a = 1.0;
    #|  } else if (alpha_mode == 1) {
    #|    if (albedo.a < alpha_cutoff) { discard; }
    #|    albedo.a = 1.0;
    #|  } else if (albedo.a <= 0.0001) {
    #|    discard;
    #|  }
    #|  if (unlit) { return albedo; }
    #|  let tangent = safe_normalize(v.tangent.xyz - v.normal * dot(v.tangent.xyz, v.normal), vec3(1.0, 0.0, 0.0));
    #|  let bitangent = safe_normalize(cross(v.normal, tangent) * v.tangent.w, vec3(0.0, 1.0, 0.0));
    #|  let tangent_normal = normalize(vec3(normal_sample.xy * v.surface.y, normal_sample.z));
    #|  let geometric_normal = normalize(v.normal);
    #|  let mapped_normal = safe_normalize(
    #|    tangent * tangent_normal.x + bitangent * tangent_normal.y + v.normal * tangent_normal.z,
    #|    geometric_normal,
    #|  );
    #|  let metallic = clamp(v.material.z * mr_sample.b, 0.0, 1.0);
    #|  let roughness = clamp(v.material.w * mr_sample.g, 0.0, 1.0);
    #|  let view_dir = safe_normalize(u_light.view_position.xyz - v.world_pos, vec3(0.0, 0.0, 1.0));
    #|  let lit = apply_lighting(v.world_pos, mapped_normal, geometric_normal, view_dir, roughness, metallic, receive_shadows);
    #|  let occlusion = mix(1.0, occlusion_sample.r, clamp(v.surface.x, 0.0, 1.0));
    #|  let emissive = emissive_sample * v.emissive;
    #|  return vec4(albedo.rgb * lit * occlusion + emissive, albedo.a);
    #|}
    #|
  )
}

///|
fn webgpu_shader2d_texture_source() -> String {
  (
    #|struct CanvasUniform { size: vec2, _pad: vec2 };
    #|@group(0) @binding(0) var u_canvas: CanvasUniform;
    #|@group(0) @binding(1) var u_sampler: sampler;
    #|@group(0) @binding(2) var u_texture: texture_2d;
    #|struct VSIn {
    #|  @location(0) pos: vec2,
    #|  @location(1) uv: vec2,
    #|  @location(2) color: vec4,
    #|};
    #|struct VSOut {
    #|  @builtin(position) pos: vec4,
    #|  @location(0) uv: vec2,
    #|  @location(1) color: vec4,
    #|};
    #|@vertex fn vs_main(v: VSIn) -> VSOut {
    #|  var o: VSOut;
    #|  let nx = (v.pos.x / u_canvas.size.x) * 2.0 - 1.0;
    #|  let ny = 1.0 - (v.pos.y / u_canvas.size.y) * 2.0;
    #|  o.pos = vec4(nx, ny, 0.0, 1.0);
    #|  o.uv = v.uv;
    #|  o.color = v.color;
    #|  return o;
    #|}
    #|@fragment fn fs_main(v: VSOut) -> @location(0) vec4 {
    #|  return textureSample(u_texture, u_sampler, v.uv) * v.color;
    #|}
    #|
  )
}