From 750d13779d881fe5794dcd04cca2baffbeb51032 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Tue, 15 Sep 2026 20:20:42 +0300 Subject: [PATCH] feat(render3d): mesh-shader grass The chunked grass path draws each chunk as one mesh-shader dispatch when the setting asks and the card has VK_EXT_mesh_shader: work group y is a tile, x a batch of 16 of its blades, and a blade the placement, density, frustum, water or slope tests reject emits nothing - the instanced path still runs its eight vertices to a degenerate position. grass.mesh generates the same blades as grass.vert (grass_blade_mesh(4)'s rows, the same hashes, sway and lighting normal), capped at the instanced path's 65535 a tile. Vulkan: VK_EXT_mesh_shader with meshShader, and maintenance4 (glslang's mesh stages declare LocalSizeId); vkCmdDrawMeshTasksEXT looked up per device, as the Streamline interposer exports none; a *.mesh program's pipeline takes the mesh stage and no vertex input, its bindings the mesh stage bit. gpu_has_mesh, gpu_draw_mesh_tasks; r3d_mesh_grass and R3D_MESH_GRASS / R3D_NO_MESH. bin/ludic-dev rebuilt: the committed binary predated the shader tool's mesh support and compiled grass.mesh as a vertex stage. PC (RTX 3070 Ti): the camp matches the chunked path; validation only the no-window present-id message. Co-Authored-By: Claude Opus 5 --- packages/ludic.render3d/gpu.ludic | 5 +- packages/ludic.render3d/gpu_vk.ludic | 32 +++- packages/ludic.render3d/gpu_vk_draw.ludic | 41 ++++- packages/ludic.render3d/grass.ludic | 31 +++- packages/ludic.render3d/shaders/grass.mesh | 167 ++++++++++++++++++ .../shaders/spv/d48d6a48.frag.spv | Bin 0 -> 45536 bytes .../shaders/spv/d48d6a48.vert.spv | Bin 0 -> 27260 bytes .../ludic.render3d/shaders/spv/manifest.txt | 67 +++++++ packages/ludic.render3d/shaders/variants.list | 1 + runtime/native/vk.ludic | 3 + 10 files changed, 334 insertions(+), 13 deletions(-) create mode 100644 packages/ludic.render3d/shaders/grass.mesh create mode 100644 packages/ludic.render3d/shaders/spv/d48d6a48.frag.spv create mode 100644 packages/ludic.render3d/shaders/spv/d48d6a48.vert.spv diff --git a/packages/ludic.render3d/gpu.ludic b/packages/ludic.render3d/gpu.ludic index 224c32bb..aef9ed0c 100644 --- a/packages/ludic.render3d/gpu.ludic +++ b/packages/ludic.render3d/gpu.ludic @@ -394,7 +394,7 @@ function gpu_caps_probe() -> void { # DLSS super resolution and Reflex run through NVIDIA Streamline (streamline.ludic), and HDR output is an # HDR10 swapchain (gpu_vk_draw.ludic); whether this # machine can use one is the caps' question, not this one. -function gpu_feature_implemented(f: int) -> bool { return f == GF_VULKAN or f == GF_DLSS or f == GF_REFLEX or f == GF_HDR } +function gpu_feature_implemented(f: int) -> bool { return f == GF_VULKAN or f == GF_DLSS or f == GF_REFLEX or f == GF_HDR or f == GF_MESH_GRASS } # ---- vertex data -------------------------------------------------------------------- # A Mesh is built through these and records what it is made of - which buffer feeds which @@ -524,6 +524,9 @@ function gpu_buffer_free(buf: int) -> void { function gpu_has_compute() -> bool { return gpu_kind == GPU_VK } # several records in one indirect draw, each with its own firstInstance function gpu_has_mdi() -> bool { return gpu_kind == GPU_VK and gvk_has_mdi } +# mesh shaders (VK_EXT_mesh_shader): a *.mesh program drawn with gpu_draw_mesh_tasks +function gpu_has_mesh() -> bool { return gpu_kind == GPU_VK and gvk_has_mesh } +function gpu_draw_mesh_tasks(x: int, y: int, z: int) -> void { if gpu_kind == GPU_VK { gvk_draw_mesh_tasks_now(x, y, z) } } function gpu_compute(name: string, n_bufs: int) -> int { if gpu_kind != GPU_VK { return 0 } return gvk_compute_new(name, n_bufs) diff --git a/packages/ludic.render3d/gpu_vk.ludic b/packages/ludic.render3d/gpu_vk.ludic index e7dd3788..30a56421 100644 --- a/packages/ludic.render3d/gpu_vk.ludic +++ b/packages/ludic.render3d/gpu_vk.ludic @@ -58,6 +58,8 @@ function gvk_ext_in(props: bytes, n: int, want: string) -> bool { # the caller stays on OpenGL. var gvk_has_mdi: bool = false # multiDrawIndirect + drawIndirectFirstInstance var gvk_has_dic: bool = false # drawIndirectCount +var gvk_has_mesh: bool = false # VK_EXT_mesh_shader with its meshShader feature on +var gvk_mesh_fn: pointer = null # vkCmdDrawMeshTasksEXT: the device's own, the interposer exports none function gvk_init() -> bool { if gvk_ready { return true } @@ -161,6 +163,8 @@ function gvk_init() -> bool { Vk.put_i32(want13, VkPhysicalDeviceVulkan13Features_sType, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES) Vk.put_i32(want13, VkPhysicalDeviceVulkan13Features_dynamicRendering, 1) Vk.put_i32(want13, VkPhysicalDeviceVulkan13Features_synchronization2, 1) + # maintenance4: glslang's mesh stages declare their work group size with LocalSizeId, which needs it + if Vk.get_i32(f13, VkPhysicalDeviceVulkan13Features_maintenance4) == 1 { Vk.put_i32(want13, VkPhysicalDeviceVulkan13Features_maintenance4, 1) } # Streamline's hooks keep their own data on our objects (private data slots), and ask the device # to have the feature rather than turning it on themselves if gsl_on and Vk.get_i32(f13, VkPhysicalDeviceVulkan13Features_privateData) == 1 { Vk.put_i32(want13, VkPhysicalDeviceVulkan13Features_privateData, 1) } @@ -198,6 +202,27 @@ function gvk_init() -> bool { let nde = Vk.get_i32(cnt, 0) let dexts = bytes(nde * VkExtensionProperties_sizeof + 8) Vk.enumerate_device_extension_properties(gvk_pd, null, cnt, dexts) + # mesh-shader grass: the extension, and its meshShader feature asked for where the device has it. + # R3D_NO_MESH=1 leaves it off. + gvk_has_mesh = false + if gvk_ext_in(dexts, nde, VK_EXT_MESH_SHADER_EXTENSION_NAME) and not Os.has_env("R3D_NO_MESH") { + let fm = bytes(VkPhysicalDeviceMeshShaderFeaturesEXT_sizeof) + Vk.zero(fm, VkPhysicalDeviceMeshShaderFeaturesEXT_sizeof) + Vk.put_i32(fm, VkPhysicalDeviceMeshShaderFeaturesEXT_sType, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_FEATURES_EXT) + let qm = bytes(VkPhysicalDeviceFeatures2_sizeof) + Vk.zero(qm, VkPhysicalDeviceFeatures2_sizeof) + Vk.put_i32(qm, VkPhysicalDeviceFeatures2_sType, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2) + Vk.put_ptr(qm, VkPhysicalDeviceFeatures2_pNext, fm) + Vk.get_physical_device_features2(gvk_pd, qm) + if Vk.get_i32(fm, VkPhysicalDeviceMeshShaderFeaturesEXT_meshShader) == 1 { + let wm = bytes(VkPhysicalDeviceMeshShaderFeaturesEXT_sizeof) + Vk.zero(wm, VkPhysicalDeviceMeshShaderFeaturesEXT_sizeof) + Vk.put_i32(wm, VkPhysicalDeviceMeshShaderFeaturesEXT_sType, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_FEATURES_EXT) + Vk.put_i32(wm, VkPhysicalDeviceMeshShaderFeaturesEXT_meshShader, 1) + Vk.put_ptr(want13, VkPhysicalDeviceVulkan13Features_pNext, wm) + gvk_has_mesh = true + } + } let prio = bytes(4) Vk.put_i32(prio, 0, 0x3F800000) let qci = bytes(VkDeviceQueueCreateInfo_sizeof) @@ -226,6 +251,7 @@ function gvk_init() -> bool { gvk_has_hdr_meta = gvk_ext_in(dexts, nde, VK_EXT_HDR_METADATA_EXTENSION_NAME) and Vk.has("vkSetHdrMetadataEXT") == 1 if gvk_has_hdr_meta { Vk.put_ptr(dext_names, n_dext * 8, VK_EXT_HDR_METADATA_EXTENSION_NAME); n_dext += 1 } } + if gvk_has_mesh { Vk.put_ptr(dext_names, n_dext * 8, VK_EXT_MESH_SHADER_EXTENSION_NAME); n_dext += 1 } if n_dext > 0 { Vk.put_i32(dci, VkDeviceCreateInfo_enabledExtensionCount, n_dext) Vk.put_ptr(dci, VkDeviceCreateInfo_ppEnabledExtensionNames, dext_names) @@ -236,12 +262,16 @@ function gvk_init() -> bool { Vk.get_device_queue(gvk_dev, gvk_family, 0, out) gvk_queue = Vk.get_ptr(out, 0) gsl_probe_device(gvk_pd) + if gvk_has_mesh { + gvk_mesh_fn = Vk.get_device_proc_addr(gvk_dev, "vkCmdDrawMeshTasksEXT") + if gvk_mesh_fn == null { gvk_has_mesh = false } + } gvk_mp = bytes(VkPhysicalDeviceMemoryProperties_sizeof) Vk.get_physical_device_memory_properties(gvk_pd, gvk_mp) if not gvk_cmd_init() { return false } gvk_ready = true - print(`r3d: vulkan on {gvk_device_name}, anisotropy up to {gvk_aniso_x(gvk_max_aniso)}x, multi-draw indirect {gvk_has_mdi}, indirect count {gvk_has_dic}`) + print(`r3d: vulkan on {gvk_device_name}, anisotropy up to {gvk_aniso_x(gvk_max_aniso)}x, multi-draw indirect {gvk_has_mdi}, indirect count {gvk_has_dic}, mesh shaders {gvk_has_mesh}`) return true } diff --git a/packages/ludic.render3d/gpu_vk_draw.ludic b/packages/ludic.render3d/gpu_vk_draw.ludic index f668d565..fdbe74dc 100644 --- a/packages/ludic.render3d/gpu_vk_draw.ludic +++ b/packages/ludic.render3d/gpu_vk_draw.ludic @@ -50,14 +50,21 @@ function gvk_module(path: string) -> long { # The Vulkan side of a program handle the renderer already made (gpu_program): the handle's # manifest key finds the variant. Returns false when there is no such variant. +# a program whose first stage is a mesh shader (its first file is *.mesh): its pipeline has no vertex +# input, and its first stage's bindings are the mesh stage's +var gvk_prog_mesh: []int = null +function gvk_stage_first(p: int) -> int { + if gvk_prog_mesh != null and p < len(gvk_prog_mesh) and gvk_prog_mesh[p] == 1 { return VK_SHADER_STAGE_MESH_BIT_EXT } + return VK_SHADER_STAGE_VERTEX_BIT +} function gvk_program(p: int, key: string, spv_dir: string) -> bool { let zero: long = 0 if gvk_prog_var == null { gvk_prog_var = new []GpuVariant; gvk_prog_vs = new []long; gvk_prog_fs = new []long - gvk_prog_dsl = new []long; gvk_prog_layout = new []long + gvk_prog_dsl = new []long; gvk_prog_layout = new []long; gvk_prog_mesh = new []int } while len(gvk_prog_var) <= p { - push(gvk_prog_var, null); push(gvk_prog_vs, zero); push(gvk_prog_fs, zero); push(gvk_prog_dsl, zero); push(gvk_prog_layout, zero) + push(gvk_prog_var, null); push(gvk_prog_vs, zero); push(gvk_prog_fs, zero); push(gvk_prog_dsl, zero); push(gvk_prog_layout, zero); push(gvk_prog_mesh, 0) } let parts = Text.split(key, "|") var defs = "" @@ -65,6 +72,7 @@ function gvk_program(p: int, key: string, spv_dir: string) -> bool { let v = gpu_variant_find_key(parts[0], parts[1], defs) if v == null { print(`r3d: vulkan: no SPIR-V variant for {key}`); return false } let vs = gvk_module(`{spv_dir}/{v.id}.vert.spv`) + if Text.ends_with(v.vs, ".mesh") { gvk_prog_mesh[p] = 1 } else { gvk_prog_mesh[p] = 0 } let fs = gvk_module(`{spv_dir}/{v.id}.frag.spv`) if vs == 0 or fs == 0 { return false } @@ -80,7 +88,7 @@ function gvk_program(p: int, key: string, spv_dir: string) -> bool { Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_binding, 0) Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_descriptorType, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC) Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_descriptorCount, 1) - Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_stageFlags, VK_SHADER_STAGE_VERTEX_BIT) + Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_stageFlags, gvk_stage_first(p)) k += 1 } if v.fblock >= 0 { @@ -94,7 +102,7 @@ function gvk_program(p: int, key: string, spv_dir: string) -> bool { Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_binding, v.t_bind[t]) Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_descriptorType, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER) Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_descriptorCount, 1) - Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_stageFlags, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT) + Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_stageFlags, gvk_stage_first(p) | VK_SHADER_STAGE_FRAGMENT_BIT) k += 1 } let dslci = bytes(VkDescriptorSetLayoutCreateInfo_sizeof) @@ -371,7 +379,7 @@ function gvk_pipeline(p: int, m: Mesh, st: GvkState, n_color: int, color_fmt: in let stages = bytes(ss * 2) Vk.zero(stages, ss * 2) Vk.put_i32(stages, VkPipelineShaderStageCreateInfo_sType, VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO) - Vk.put_i32(stages, VkPipelineShaderStageCreateInfo_stage, VK_SHADER_STAGE_VERTEX_BIT) + Vk.put_i32(stages, VkPipelineShaderStageCreateInfo_stage, gvk_stage_first(p)) Vk.put_i64(stages, VkPipelineShaderStageCreateInfo_module, gvk_prog_vs[p]) Vk.put_ptr(stages, VkPipelineShaderStageCreateInfo_pName, "main") Vk.put_i32(stages, ss + VkPipelineShaderStageCreateInfo_sType, VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO) @@ -524,8 +532,11 @@ function gvk_pipeline(p: int, m: Mesh, st: GvkState, n_color: int, color_fmt: in Vk.put_ptr(gpci, VkGraphicsPipelineCreateInfo_pNext, prci) Vk.put_i32(gpci, VkGraphicsPipelineCreateInfo_stageCount, 2) Vk.put_ptr(gpci, VkGraphicsPipelineCreateInfo_pStages, stages) - Vk.put_ptr(gpci, VkGraphicsPipelineCreateInfo_pVertexInputState, vin) - Vk.put_ptr(gpci, VkGraphicsPipelineCreateInfo_pInputAssemblyState, ias) + # a mesh-shader pipeline has neither: the mesh stage makes its own vertices + if gvk_stage_first(p) == VK_SHADER_STAGE_VERTEX_BIT { + Vk.put_ptr(gpci, VkGraphicsPipelineCreateInfo_pVertexInputState, vin) + Vk.put_ptr(gpci, VkGraphicsPipelineCreateInfo_pInputAssemblyState, ias) + } Vk.put_ptr(gpci, VkGraphicsPipelineCreateInfo_pViewportState, vps) Vk.put_ptr(gpci, VkGraphicsPipelineCreateInfo_pRasterizationState, rs) Vk.put_ptr(gpci, VkGraphicsPipelineCreateInfo_pMultisampleState, ms) @@ -1273,6 +1284,12 @@ function gvk_draw(p: int, m: Mesh, st: GvkState, first: int, count: int, instanc } var n = count if n == 0 and m != null { n = m.count } + if gvk_mesh_x > 0 { + # a mesh-shader dispatch (gpu_draw_mesh_tasks): the device's command, through a pointer + Vk.sl_call_piii(gvk_mesh_fn, cb, gvk_mesh_x, gvk_mesh_y, gvk_mesh_z) + if prof { gvk_n_draws += 1; gvk_us_draw = gvk_us_draw + (gl_now_us() - t0) } + return + } if m != null and m.ebo != 0 { let zero: long = 0 var itype = VK_INDEX_TYPE_UINT32 @@ -1461,6 +1478,16 @@ var gvk_ind_off: int = 0 var gvk_ind_n: int = 0 var gvk_ind_cbuf: int = 0 var gvk_ind_coff: int = 0 +# x * y * z mesh-shader invocations with the current program (a *.mesh one) and state +var gvk_mesh_x: int = 0 +var gvk_mesh_y: int = 0 +var gvk_mesh_z: int = 0 +function gvk_draw_mesh_tasks_now(x: int, y: int, z: int) -> void { + if not gvk_has_mesh or gvk_mesh_fn == null or x <= 0 or y <= 0 or z <= 0 { return } + gvk_mesh_x = x; gvk_mesh_y = y; gvk_mesh_z = z + gvk_draw_now(null, 0, 0, 1) + gvk_mesh_x = 0 +} function gvk_draw_indirect_now(m: Mesh, cmds: int, offset: int, n: int, count_buf: int, count_off: int) -> void { if m == null or m.ebo == 0 or cmds <= 0 or n <= 0 { return } gvk_ind_buf = cmds; gvk_ind_off = offset; gvk_ind_n = n; gvk_ind_cbuf = count_buf; gvk_ind_coff = count_off diff --git a/packages/ludic.render3d/grass.ludic b/packages/ludic.render3d/grass.ludic index d4104447..fb5952f0 100644 --- a/packages/ludic.render3d/grass.ludic +++ b/packages/ludic.render3d/grass.ludic @@ -35,6 +35,17 @@ var grass_cmds: int = 0 var grass_band_start: words = null # per band: its first record var grass_band_cells: words = null # ... and its 16 m cells per tile side var grass_band_n: int = 0 +# Mesh-shader grass (Settings, Video, Advanced): the chunked path's records, but each chunk is one +# mesh dispatch - work group y a tile, x a batch of GRASS_MESH_BLADES of its blades - so a blade the +# tests reject emits nothing instead of eight degenerate vertices. R3D_MESH_GRASS=1 / 0 overrides. +const GRASS_MESH_BLADES: int = 16 +var grass_mesh_on: bool = false +var grass_mesh_prog: int = 0 +function r3d_mesh_grass(on: bool) -> void { + grass_mesh_on = on + if Os.has_env("R3D_MESH_GRASS") { grass_mesh_on = Text.to_int(Os.env("R3D_MESH_GRASS")) != 0 } +} +function grass_mesh_live() -> bool { return grass_mesh_on and grass_merge and grass_mesh_prog != 0 and gpu_has_mesh() } # a blade: `rows` rows of 2 vertices (x across, y along, z bend), attribute 2 = uv function grass_blade_mesh(rows: int) -> Mesh { @@ -81,6 +92,7 @@ function grass_init() -> void { grass_cmds = gpu_buffer_new() } grass_prog = r3d_program("grass.vert", "model.frag", defs) + if grass_merge and gpu_has_mesh() { grass_mesh_prog = r3d_program("grass.mesh", "model.frag", "#define FOLIAGE\n#define BLADE\n#define MESH\n") } grass_mesh = grass_blade_mesh(4) grass_wind = fl(2.4) grass_s0 = fl(0.11) @@ -155,7 +167,8 @@ function grass_tiles(size: int, d_min: int, d_max: int) -> void { function grass_draw() -> void { if not grass_on or ter_reflect or grass_prog == 0 { return } - let p = grass_prog + var p = grass_prog + if grass_mesh_live() { p = grass_mesh_prog } gpu_use_program(p) u_mat4(gpu_uniform(p, "u_view"), cam_view) u_mat4(gpu_uniform(p, "u_proj"), cam_proj) @@ -203,8 +216,9 @@ function grass_draw() -> void { # the records gathered this frame: uploaded once, then each band GRASS_CHUNK records a draw function grass_flush() -> void { if grass_n == 0 { return } - let p = grass_prog - gpu_buffer_upload(grass_cmds, grass_n * 20, grass_rec, GPU_DYNAMIC) + var p = grass_prog + let mesh = grass_mesh_live() + if mesh { p = grass_mesh_prog } else { gpu_buffer_upload(grass_cmds, grass_n * 20, grass_rec, GPU_DYNAMIC) } for b in 0 .. grass_band_n { let s = grass_band_start[b] var e = grass_n @@ -216,7 +230,16 @@ function grass_flush() -> void { if m > GRASS_CHUNK { m = GRASS_CHUNK } for q in 0 .. m * 4 { grass_chunk_tv[q] = grass_tv[k * 4 + q] } u_f4v(gpu_uniform(p, "u_tiles"), m, grass_chunk_tv) - gpu_draw_mesh_indirect(grass_mesh, grass_cmds, k * 20, m, 0, 0) + if mesh { + # enough blade batches for the chunk's largest tile, capped as the instanced path caps a tile + let cells = grass_band_cells[b] + var most = 0 + for q in 0 .. m { let t = f_to_int(grass_tv[(k + q) * 4 + 2]) * cells * cells; if t > most { most = t } } + if most > 65535 { most = 65535 } + gpu_draw_mesh_tasks((most + GRASS_MESH_BLADES - 1) / GRASS_MESH_BLADES, m, 1) + } else { + gpu_draw_mesh_indirect(grass_mesh, grass_cmds, k * 20, m, 0, 0) + } grass_draws += 1 k += m } diff --git a/packages/ludic.render3d/shaders/grass.mesh b/packages/ludic.render3d/shaders/grass.mesh new file mode 100644 index 00000000..f7c3de5f --- /dev/null +++ b/packages/ludic.render3d/shaders/grass.mesh @@ -0,0 +1,167 @@ +// Procedural ground-cover blades as a MESH shader (Vulkan, VK_EXT_mesh_shader): the same blades +// grass.vert draws - the same places, density, thinning, culling, size, sway and lighting normal - +// but a blade the tests reject emits nothing, where the instanced path still runs all eight of its +// vertices to a degenerate position. +// +// One dispatch covers a chunk of tiles (grass.ludic): work group y is the tile's place in u_tiles, +// work group x a batch of BLADES blade indices within that tile. Everything past the tile's own +// count is skipped. +layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; +const int BLADES = 16; // blades an invocation may emit +const int ROWS = 4; // grass_blade_mesh(4): two vertices a row, three quads +layout(triangles, max_vertices = 128, max_primitives = 96) out; + +uniform mat4 u_view; +uniform mat4 u_proj; +uniform mat4 u_vp; +uniform vec3 u_cam_pos; +uniform float u_time; +uniform sampler2D u_ts_height; +uniform vec2 u_ts_origin; +uniform float u_ts_half; +uniform sampler2D u_ortho; +uniform float u_ortho_on; +uniform float u_lake_level; +uniform float u_sea_level; +uniform vec4 u_lake; +uniform float u_snow_line; +uniform float u_wind; +uniform int u_tile_cells; // 16 m cells per tile side +uniform vec4 u_tiles[256]; // per tile of the dispatch: corner x, corner z, indices per cell +uniform float u_s0; +uniform float u_d0; +uniform float u_radius; +uniform int u_dbg; +out vec3 v_wpos[]; +out vec3 v_nrm[]; +out vec2 v_uv[]; +out float v_seed[]; +out vec2 v_rot[]; +out float v_hull[]; + +const float CELL = 16.0; +float hash1(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); } +uint pcg(uint v) { uint s = v * 747796405u + 2891336453u; uint w = ((s >> ((s >> 28u) + 4u)) ^ s) * 277803737u; return (w >> 22u) ^ w; } +float bladeHash(ivec2 cell, int j, int k) { + uint h = pcg(uint(cell.x + 32768) * 73856093u ^ uint(cell.y + 32768) * 19349663u ^ uint(j) * 83492791u ^ uint(k) * 2654435761u); + return float(h) * (1.0 / 4294967295.0); +} +// no implicit level of detail outside a fragment shader: every lookup names level 0 +float heightSmooth(sampler2D tex, vec2 uv) { + vec2 res = vec2(textureSize(tex, 0)); + vec2 t = uv * res - 0.5; + vec2 f = fract(t); + vec2 i = floor(t); + vec2 w0 = (1.0 - f) * (1.0 - f) * (1.0 - f) / 6.0; + vec2 w1 = (4.0 - 6.0 * f * f + 3.0 * f * f * f) / 6.0; + vec2 w3 = f * f * f / 6.0; + vec2 w2 = 1.0 - w0 - w1 - w3; + vec2 s0 = w0 + w1, s1 = w2 + w3; + vec2 o0 = (i - 1.0 + w1 / s0 + 0.5) / res; + vec2 o1 = (i + 1.0 + w3 / s1 + 0.5) / res; + return (textureLod(tex, vec2(o0.x, o0.y), 0.0).r * s0.x + textureLod(tex, vec2(o1.x, o0.y), 0.0).r * s1.x) * s0.y + + (textureLod(tex, vec2(o0.x, o1.y), 0.0).r * s0.x + textureLod(tex, vec2(o1.x, o1.y), 0.0).r * s1.x) * s1.y; +} + +void main() { + int ti = int(gl_WorkGroupID.y); + vec2 tile = u_tiles[ti].xy; + int per_cell = int(u_tiles[ti].z + 0.5); + int total = min(per_cell * u_tile_cells * u_tile_cells, 65535); // the instanced path's cap on a tile + int first = int(gl_WorkGroupID.x) * BLADES; + int nv = 0; + int np = 0; + for (int b = 0; b < BLADES; b++) { + int i = first + b; + if (per_cell <= 0 || i >= total) break; + int c = i / per_cell; + int j = i - c * per_cell; + vec2 cell = tile + vec2(float(c % u_tile_cells), float(c / u_tile_cells)) * CELL; + vec2 cid = floor(cell / CELL + 0.5); + ivec2 ci = ivec2(cid); + float fj = float(j); + vec2 hv = vec2(bladeHash(ci, j, 0), bladeHash(ci, j, 1)); + vec2 xz = cell + hv * CELL; + float dist = length(xz - u_cam_pos.xz); + if (dist >= u_radius) continue; + float spacing = u_s0 * (1.0 + dist / u_d0); + float count = CELL * CELL / (spacing * spacing) * (1.0 - smoothstep(u_radius * 0.7, u_radius, dist)); + if (fj >= count) continue; + float life = 1.0 - smoothstep(0.8, 1.0, fj / max(count, 1.0)); + vec2 huv = (xz - u_ts_origin) / (2.0 * u_ts_half) + 0.5; + if (huv.x < 0.0 || huv.x > 1.0 || huv.y < 0.0 || huv.y > 1.0) continue; + vec4 ht = textureLod(u_ts_height, huv, 0.0); + vec4 croot = u_vp * vec4(xz.x, ht.r, xz.y, 1.0); + if (croot.w < -1.0 || abs(croot.x) > croot.w * 1.25 + 1.5 || abs(croot.y) > croot.w * 1.4 + 1.5) continue; + vec3 gn = normalize(ht.gba); + float h3 = bladeHash(ci, j, 2), h4 = bladeHash(ci, j, 3); + float wl = u_sea_level; + if (u_lake.z > 0.0) { vec2 q = (xz - u_lake.xy) / u_lake.zw; if (dot(q, q) < 1.0) wl = max(wl, u_lake_level); } + float ok = (1.0 - smoothstep(0.30, 0.55, 1.0 - gn.y)) * smoothstep(0.0, 0.6, ht.r - wl - 0.15) * smoothstep(u_snow_line - 80.0, u_snow_line - 200.0, ht.r); + if (u_ortho_on > 0.5) { + vec3 oc = textureLod(u_ortho, huv, 1.5).rgb; + ok *= 0.25 + 0.75 * smoothstep(0.0, 0.02, oc.g - max(oc.r, oc.b)); + } + if (h4 > ok) continue; + bool far = dist > 300.0; + float h = far ? ht.r : heightSmooth(u_ts_height, huv); + if (far) h += 0.03; + if ((u_dbg & 1) != 0) h += 0.3; + float seed = hv.x * 0.7 + hv.y * 0.3; + float ang = hv.y * 6.2831853; + float s = sin(ang), c_ = cos(ang); + float grow = spacing / u_s0; + float tall = mix(0.18, 0.42, h3) * mix(0.8, 1.2, hash1(cid * 0.1)) * (1.0 + 0.35 * smoothstep(1.0, 12.0, grow)) * life; + float bw = 0.028 * mix(1.0, 0.45 * grow, smoothstep(1.0, 4.0, grow)); + if (far) { bw = max(bw, spacing * 0.35); tall = min(tall, spacing * 0.3); } + float gust = sin(xz.x * 0.09 + u_time * 1.1) * 0.5 + sin(xz.y * 0.13 - u_time * 0.8 + xz.x * 0.05) * 0.5; + float ph = u_time * 1.7 + seed * 6.2831 + xz.x * 0.05 + xz.y * 0.07; + float sway = (sin(ph) * 0.6 + sin(ph * 2.3 + 1.0) * 0.4 + gust) * u_wind; + // the ground's frame, shared by the blade's vertices + vec3 up = vec3(0.0, 1.0, 0.0); + vec3 k = cross(up, gn); + float sk = length(k), ck = gn.y; + bool tilt = sk > 1e-4; + if (tilt) k /= sk; + vec3 n = vec3(0.0, 0.3, 1.0); + n = normalize(vec3(c_ * n.x + s * n.z, n.y, -s * n.x + c_ * n.z)); + if (tilt) n = normalize(n * ck + cross(k, n) * sk + k * dot(k, n) * (1.0 - ck)); + n = normalize(mix(n, gn, smoothstep(2.0, 12.0, dist))); + float hull = (dist > 2.0 || far || (u_dbg & 2) != 0) ? -1.0 : 1.0; + // the blade: grass_blade_mesh(4)'s vertices, placed as grass.vert places them + for (int r = 0; r < ROWS; r++) { + float t = float(r) / float(ROWS - 1); + float taper = max(1.0 - t * t * sqrt(t), 0.12); + float bend = t * t * 0.28; + for (int sd = 0; sd < 2; sd++) { + vec3 a_pos = vec3((float(sd) - 0.5) * taper, t, bend); + vec2 a_uv = vec2(float(sd), t); + vec3 p = vec3(a_pos.x * bw, a_pos.y * tall, a_pos.z * tall * (0.6 + 0.8 * h4)); + float hgt = max(p.y, 0.0); + p.x += sway * hgt * hgt * 0.35; + p.z += sway * hgt * hgt * 0.15 * cos(ph * 0.7); + p = vec3(c_ * p.x + s * p.z, p.y, -s * p.x + c_ * p.z); + if (tilt) p = p * ck + cross(k, p) * sk + k * dot(k, p) * (1.0 - ck); + vec3 w = vec3(xz.x, h - 0.02, xz.y) + p; + vec4 clip = u_proj * u_view * vec4(w, 1.0); + clip.z = (clip.z + clip.w) * 0.5; // OpenGL's depth range to Vulkan's, as the vertex wrapper does + int o = nv + r * 2 + sd; + gl_MeshVerticesEXT[o].gl_Position = clip; + v_wpos[o] = w; + v_nrm[o] = n; + v_uv[o] = far ? vec2(a_uv.x, 0.45 + 0.2 * a_uv.y) : a_uv; + v_seed[o] = seed; + v_rot[o] = vec2(s, c_); + v_hull[o] = hull; + } + } + for (int q = 0; q < ROWS - 1; q++) { + uint bb = uint(nv + q * 2); + gl_PrimitiveTriangleIndicesEXT[np] = uvec3(bb, bb + 1u, bb + 2u); + gl_PrimitiveTriangleIndicesEXT[np + 1] = uvec3(bb + 1u, bb + 3u, bb + 2u); + np += 2; + } + nv += ROWS * 2; + } + SetMeshOutputsEXT(uint(nv), uint(np)); +} diff --git a/packages/ludic.render3d/shaders/spv/d48d6a48.frag.spv b/packages/ludic.render3d/shaders/spv/d48d6a48.frag.spv new file mode 100644 index 0000000000000000000000000000000000000000..f83e77c00b5279c33bd4288b5d96c2c33c91f03f GIT binary patch literal 45536 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 MM_=#F+PpXaKhQQ-cmMzZ literal 0 HcmV?d00001 diff --git a/packages/ludic.render3d/shaders/spv/d48d6a48.vert.spv b/packages/ludic.render3d/shaders/spv/d48d6a48.vert.spv new file mode 100644 index 0000000000000000000000000000000000000000..5d50274f3b15b2cf82dcb188e4d834186dd39ee5 GIT binary patch literal 27260 zcmZXc2fUVL{l;HB2P7r;p5O{+Db8?!;zq?(G}rL}2RI5GJm&yz5AKyKa~qiEPQ{U< zAz5Y`TG=qu%C?zpEw$4B_xqgZI^MqjSMSe#UDt2i^BM2W3oT1+xO8L4pvJO|p^Zmd z8`WpU#!`)KK-Rac(r7Hx7}7Xo!o;>ck2th#W=GHTww~$jQ#!hd8PXVp-`?X78NXFe z@06{!-R=`6Sgz4xRoRv&%Uk3y_00CpS?bFXv=-$}%9%?w8sjk@LOFtRETxBX66Fla zHI)4a4XNTcA$IAatbmHSu@G$5_|KD&LgRz(| z{Tz()QTX)sp6OeUoI7=!(G8TA#xS(m6#Z8cPo6e%&eo$V+luhHm2GHa9qLIln5un9 zFmi6^HlsVY_J8Cmc1`NZ9W!S1^OkBH$5*zN#@f^;RCs7(BkJiLoztfG9x}75t9Say zo^7@s-QS(6%?+^kcAQk<4Z(Bhfc@MW+(QxXKs~R<`%zD?aBE`<_3ZZU_L;TK&UAP5 zR59J)-daDIdTNbNqwcKnnbh-Gb=$a1OrzqP#iRQ1onkhW`XX>o?f=7<_1Bjefsz9aG!q%;-IA zR_D~N?wPyK=$g!g4#7japE+%FJ3E+^Xv%ZiW_Ne72+`a#__^F@L#kMJ_nfxL?K9hE zcR9cDb??k+>+PILPvSecU+Zb>>h7Gz_K!X!`tAYj z78@dMy7wK=jE*+9sY4y`Uv9wGGoUSBX|C=Eek)Yklv?YbzZEO3yM0RMoDb#nTAI@~ zWzsZi_loxjKVF9;z_UBLtB&+*t&L65db@htoecNEcJQg4-96c}qv5maYhh}(&md0M-?BzqilWYBOaR0T~+BinNxK3xvJEv5+dMn(YcOG~uLAG%UczV@#YvWY# z2iI6@X6HFCxD-64vzmfxP8Ps>X17o7oHecDcfu!k z&6(9dN3D$~;4?a>*4Iu;<7xPGZXk}~pJE)HfA7`og`T$l8&Q?(ZM4bV+>z=z+mwrC z8hh5*Ey2^P>%XIZH?Wwe>~fL$ky7}p^5MGA>Ps$4WGw`un&8J zPptcPB)E&~U;PX*3+HTe_PLir*(IE8?M@Z0nYBNu4Vi3GW>%!S{kpxC*iH_zsAnVy0$dd0?XdTDZ>z=W(HNwcoAccCOHx-wU7A;oYR)_aS%>%$#2Zx7YVh`BN45snzfMES&CF zZM*`WKCQRH8@95xmEPLe5lb^PD(yc{uX!!hLE; zWitl*gsz^>-cD|^-tUZIJNN6Ivw9EdJf*rOVq^NX?Lm{{xV+kDP4?c|A4eA}8`Csk z8;s5Onz|o$qH}UbkKLfZt=0R^+_rhu14Qn-&)l|I-Tlwu*6KZIZd>N0wR$I-+t$D1 z^!NQ}Zd-SK?X_0#N^{%#SIV_H4}13hrQk`EYml2eP*pt@s{eD8Z3Og=TL3zF%mLU+t%bW}9lS6q?U!wO0$x zXSCXH3(e=V+TRMzIahNtrq9D_t+mj6HtRRE(0nGVtx#w_lPisfZt}S%YO53)u+r8j zG@r-%t%K$s@OeA}?k9YMep+##5!xMtc6n*{UaQ?X(=PwO-cntINL4?*9{rfhkA2TI zsz&Mo%^0 z?q8+RSeY#589}u1;#Ki+{|u-2*}Ue?g?(5JPxsAm%IetMFLGmt!`&j2(`yDNOXRzxVJ{J7aKQ3J%d>q{Ihwl&eBX^z- zl=OXIKNxzi->n2!w#KN|kX`|k|c+u@5&-r@7worCEV5`H}SeJ1;fy?S6a z8oPz}Ro1=%eo4g_kBR;Ae)|Bwrs8A9xGpb%U4Pq+zxO#m?w6OSwK+fXVT1bjm;2lJ zkHY!C(nr_aXNa2ig`D%||$vwi(mLo>lTaGyW=tF6Z_^%-N1l*H${ zF;`0ZE_a>G6xF)*SgSj1p+%gGQQ&W{~5UVLdW$i*!EnD#6OS5XH44r9^5t7 z{~uuYp7ZFu52yCo;V1s@z|E=uyES(0wVP)!#_S#YKUC*N-xFznrHU^c=>IAeUp&Bv z!$*yJa5wXh1XuUMI=|!F87SA&u6Qj#Gg~~zJdOnTk$ai?Vkr< z=EYlfcKp|a%~kHVn=1dS2lm6g@XMZjYiIM@{vx>V3u*5u_{+;bGba3p@HY?t9)A>}IdiUE$t;`zY4w=lSe)piWJ_GGP0H;5@dN19ZfjdX8@p`o9dsXg{+AQYry4n}qM{V4GV13oxC;L;A zw$j~qa=-JaX+NmO<~IIdxMNHFr)s;p@p9kO5DP>n|AXZ4fdK$zGG^ey8d$C z>-5jOOoemb+SUGb)i!gO??kxsl6>8@UEO?g-wV^fxivN>?fIUlJ^sEYrhVu0D)@2K zw)Z)(&rvn|b9L>jZoJ(0%Cxtj#>Tk*){mgMjw#!77iKFE-TyK6Z4~d#=JdVIGgbZN z-ordU4c9)RiF?jg+P7=sp0ky9&)Q1vd0WXnb1S*$ZY4jw;P%h+x7g*LzlHlg)?0Aj zW9AiHf6wAdf6wE>9k1tcCHFio-29%$h3kJ#6ZbqWcH8qjF5LcjCRcLLw9Kf2(K$8)*T?%7+-K1Z1vlSH8mE7~Xl6z)X za?kBb?zvsK?Y&%Z+k35vdv+Ip?VjD0-1EC|$M5-F$vwjhH{Nr+l6#gH?tFQk7j8b! z^TLhyOfTH^^Gq*X`@$yf*hg zd)60jdt(Z2{`P`v_v|bFzSDaCB_GRO(C;ChkNDk0Z5Qr=-j`Rb_0!-!k5_`L?aJNP z`=wl8ZR>#5#uDjwfb}STPw=~eHvN3&tq<0A5H0zP84gyn_^~p-_g`(vzX7;E|I*cW zh>hXNug&im`ug121gw2NW0QXjtTq=stM2bexVmk5Z*wI(_&*LePCap-05`{Nji#QsZNSE=4`WWuwJn(ctMB{TM^Wsj&m3)8v+cpIncunX zmut2ITs>>HBUml%eG;tZcPMlE+)@+!%<}uRxcal98vCqLe}`x0u3(>?*8if-G1Oxz zezfmi+pRNKdx$BEm@D6(_oS%Vhj+;#-;H7}bBhzZH`qRg?<1!4)$_hDSRZx!?Q>Ku z{`-Ml%LnVW_Xn#vp7@N18y9{6*tx#1jyVvlkNR!3b`aQH+Wt+u2UGiTJlZBu)Etl4 zxyks{)7GKjvhBm*_V@gHd=tU?s5?I23)B*S1h|awgqjAN8~9HjW0H zN1JW<~$rjt(JM11y)Pm zF0k>I3+f!R!TM>7--%%FJ(jf9ji#@*VO+WPvj@!o)!b;e55Cu_rw?<$YTm~@mz)b$ z)3=vep7}fp>{@1CP6n&#JC9nfuX#=b8?WtDYI*XU0XC1e)2Zd+GpW6fcH+L}diKFR zJ2YND?a6;OIQh>4%ai|HaPpr6mYe@P>iLx9KOby<?IlcgF`}!Dr3bmSTIJXyp zUAyp$!TMxBTmn`r_rqu4#%VL>h16=vaT!?6>);CND=F^R%PIP-O8r@idY*Sz)tb5= z9_@{@>%H?iG;`|Xo^VdD2J?SocZzoVaVfRBZRm3iSj~QHM138_eq2k5&-Jzbb$rHw zZ=e{b?)~c~YCqn;wB1Ni^Zq6F{x!Dd>S^m{ux%}*q^&Q2)x&QAm;JjHZp==F&lkb! zUPq4UHn4H(UNdU9gZpaRxwUo&n(b+GZWd6hrR}@HYT@_PJn>(G>!Y5w?*-pQ(Ux)E z2e!}UoZb&tPp${RYKtg|dl1|l_Yj(T`t&f^`5DG29p{(9{9pZ6$(;J@<37>;2)M8I zOWTj4*>7#m!=2P>Y2z{Qyn;Ut)+hVnD`0)p-49=*_Tzrg_En0S`$1gp2lce`4RG1c zlW^Pd-NwFu6U_gOkrZux)S35h!Hw4^bMX|I|EsxJ4A5V{%*D5BO+Ux|1hty|@w)#G zIM?X2;O|mgk7p?HSyJn{4xfV?r=E5EKG?YLQ8LyafYq}GFMyw?sOP@)L$JCf&xjv^ zZ9`l9ehhAYM*IX#Uu}-+AS*^Ry zuYmoy&gw5y)LdtA+WI-TxvgKIsXtz~^-Hi?ANkxL$z%N%>u=kyQtXp^Mt|E#zkUVQ zZohs*?Zr6-@(;02fqiaIS2MDd92f~KY(r9zUprq>DOyu z?e^<+YCrZ%{f`ti`z21l{seCB*BfZ+nXf;C)%wV1zmmr~{rU^ow(YC_wqZa1M(xLb z=<`>Kn*9)$*QdIB)<0i-3;bryy*~a0wx35ZgML5zH&`EapH0r0d+(ocZI16<>iot{W5eq483Z>^xyDPOsZXf;-2yhZvD*58 zSI}3-)C#w6`Z$)i!D`7r1gz$tm*m=B8myi@^bxSyP)hd2GGKMrV;Hp`*F$|-ikj;o zPFo*Uqpfnh%b}^~nZG<37c%kv4xHE(z|H+!5lub!pq0UD{@Koa#_#p8Dp=h=-^t!v z4QxBw+$-s;b@txsV8`eF(cf`o>}!CvJNC7x{Wx~@H7RP2U7Y@}twyeL9oIoq&)C-m zXYAS%yB@fC?CYbcXY3=uYQrfR`vze3a_k$zZAY79PhYJw_Km=f&-u{baX8jZs54f5 zHm0aKR&jaVsk@)tgPVcf=LfMOt5Ry)Rh9l>eK zb5FTBJoCJ~XzP=3+cI81?fQ9^s?D|D2>)Hc-dlF2whj02u40P*qpc~PHR(SFtp9G* z8ya87jD>5h+tlQoFmVZ)iQ>Iz-r+W zYJOFHk2n~vk9zKTp8~7*(T?+VDAa;%-te&>Iz-qH7iJJ{>jyn-eJ#jr?wQfqr-wSRY{~R>+jAJf1 z=#z2G13MP|T<;^P)zY_Wt|WuzIfRQ^9J!Kf6y)2m5iKYCDah<~|jh$9JJV z@R>FDI-d_N--XVC>!Z&9)iWgb@3RrKTl_dip<_4$pgr?+K3J{XlNZ3%<8xu{Q@$5m zgr**!i))|q9(M_vdak3-fbEOdk@4EIhL?hESD&oUW#Fuj`|Lcpep#Q(#T5PK*Y}Dm zz~@l3WlgRGyLO5DELct7tgD*WZPvm%@mGPjE%@ia=F1vi4c159HNKYGk87;$8j6~0 zEH0nJ>S^aj@TCkmc|Q+VyNO~yZ=hDwe;j@b!D_a73w7F5{{rPUifxLsPPc-auZu6D zsoTcQ)N=c1PUFpEo9@Fq!0tnRZwJeFs?US&1aE|o@9^4^Prb}%-i-M!ux;wQfLfmM z-3`w8z6AE;oT}eLQL}&IiGv;Y(^~8J^T*fSc8?zimn=#K) zt0(4p@B*Cd`$p8?gS++%De?I}ntINGegHO3-JH)+tHuAv;4LY+hy4Uhy2e(NAA)~G z(MNx;r=Nmn;*hi6pTX5$qL}|>YPFr{qwg=Tfc-t9dfNCoIBhHjya?AX^Zg63{m?J{ z{1w=~C&#bB=I|Y(kMb&9pXB%r_?Hy@vKM{})>m8neh0QaZE5TG;3r5PpFhC$Q+JFn zP^-oNPhk5N|2M#|Q`F=0XK>m7zrf4>``cN4GQPjocGtl^o70%I@i*`vDf-!`*QnKO z&+Gf|;2S7j+yA8YBm7u9-EKEz|J^1^S zTu)1(c|G~tz4WsMte&{RV6|3C_SF!u`LyL8ur%1dyZ5yxekfR*?^o&XN8rt8GRvT; zCvF&6&EE~CzaIrR_jfro^~5a?R$EMG^6s(%+#)6I6 zgIe9O_`b3?*tW{|m3`3E_1}wHo^kF6Hb>s2$Ai`UJ+A)yQmg4dj>ZoFt9gGvh}w_$ zcl84)YTn<)X=4J|HZrb*!H#PJ@59c=r^H3^9YXyOG<~&ae$>4GrX6c-hf*I#Nt+YF zzQ<%<4u|VgzW*HoSI-)M8f=_;@*D~F9V&T_f}5xO{&qB6J$a4+8>c?Mejhm&Y<_LY z)dn_KxmL%))zf}E*!It4j=gV9f~zIRWN`Dirl6^(KOJD>)RT8AxSXG9@aAz%M^jIp zPOx$68Q1Y(^J~j`o&a{djzzos*Y%tMrfPT|s!!HU-F~^hW`eW7y1;(iU+S|cYVI$w zG3K2Ow(l9+iD37+?~2;H!TP8trU&dE4DSV-BiHC0uzmF!)mMA+&jq`mlYbsqe|6`@ zHctZEuC}ysGPrE>lqP;E+&<(y?=-MJ>S^nAuz9qlKWBg)Z=SPfg8S&VdgA)P#%fC+ z=Y#F1wye=vV8`tmY0ny|d);OJtuwB(!S*}+9I*GG^zU4_-`Dewc^=$0e20D>@G8Fg zWdEEG)+fa{=k$U)&UaJWv>koZ_X}&geb+aAS5IFq2A5|Cm%!ERhd%DX&%k|$&F{o6 zh3luDKRdY$tnPjA3Ti*@Q}xR!YVK39*Q(!{uLS#?Sya!B-;qCyrk?N2SAo?$-_LjE z&%xDa;qMwGw{`xk<7%+|ab5Jc-8;GNyk}ek);^SW`{2&YMR0ZTwbaRd9oT+b``f@9 zsBfhBvF)2`yS4M>7(Nd+M}8Z)0IW8M;+)8@r6wzHa5R+`T|%j-y?4Ut6f7o z_H`jxE$jJ3uv++SHP1ckcDO$3@wub+$#eBiIMZF7Cu&RVU0~z2=i0g(+=s8Wd|&ty z*m%E-XupSIUvH(>mUiz0mwmmz=4o#cTp#sl4}#SnpqTSsYBljg)LFNO!OpF<>-Grs zqZB``+heue+Wxq1kAuyTb^8if&AsQk$-hi7m$}7>{TkTVtlQVYYMG}ez-qp8XZ(xd z&Pm4h4X}Rd@p%%wu;AYWJLcRQz6CZ;KW)y#SE<$0w{L^XzC8_BOB>$-+eUu3@eEiW zb@M$%t>$&>JJol=wo$%2EP<;h=W}4c!)9#H!}U>5p6`LpQ$Fv%4_8l~AAtS7nmjMS z^-)irAA-$OzJvS-uAV$U2A6q$0@p`9J}-i|E!z4id@*aUKA*9B?|2F9d%U*v>1SZ` zY0J9240g=+SG)80EVX+2{d2HdIsRY3)pITV5}a#ETYkIn3RqkE{wjF$g8vGvPx|<4 zus-UE`3<=A`7K;O^~C)StnOJu`u%&j{Vv!44{-I&(`#Tg;}iEsaN?W~xqj)->)>(> ze}b3&djqbIdSd?EdEt8 zaQWW;KX}@W&wFU<$@4zgIQ5Ky+hD`|+A@YgXyz*S@lt5&$=w1j-`iW^YRNGeY>skV zL(tUIpQXXZsVDDHa5+C8k&~xfhh@;zlV@45aq1b@FtGWx$4?wC8)z3Sd7zE7X^#sQIiA8{>Q1ieUSgF{}jN3!Zn=mEroRCuSA!Zgq?~R)w1* z@8_$*-K*m$`f5-9)xmod`PYE!ukQHGw%#R>&sf$2 zn@3yvvp(4I=GizL-bcUH6E^~EthV%V1F-$nmbFmxd7gQ;&Ys!`+#Ob<{5w!;_W51bVJooj z#QqtPIs6UU$I;Z|^NHHWcb)ibji!Djd5qZxY@B-fG74;a2iMQCZNX}OqHR}eq!`e) zuQjHG@%I0wZU;2u_1E5q?F#bxyOAB?wy%$4^zU`4WnFdxtCiR2C*kVx8D0AfrcwLv zvv+4S_4#%GcL5uxo@;wouzGo}yBj?1*&li3=lqg)EPQn48@@aITGlLozhn=%n(u-6 zJ38aw#%Zh8fVe%u4-priz2N$(+wK@@wfG+ZR+Bq#2ZHUVzX3XkdIDuUML*Zt9BTR{ z$D!cl@H##W?ixFf6R8iU97560c)31_|1`LaKN5Z@V@Ui_@FOVt886o-@onJ5kE8wL z;C@&1emEKI$NQoBB+3km_d{{kdJ5Qg%<|sffu*E|fil6gooqT6uE3cEY;cDLVjMt`S z{KIwpBh>O6Yi^x<=iux0n(OXdxLWdQQ#1a$I{pS~+d2_nFQAyq+~UMu1}^VAm&47K`_2_$wer4mCEPe|{(VUQ ze&lDt>W=49YI)kY7Q8ckk?VUMT%YjkYwp}>zX7h6>rCy&CZC(&YTmzn{tcu4Jlx#I zYjV70Q3Uxb%Fx53r&{4me$ zO+I(P)yi?)2{*s-+B1&3z>dTIIS#qL8OPn=avb--)m%IMwW+y}+*9ra>%@Nv?Am7@ z?ggu54{K91KKs-?mH7L>?xn=v4_BMWwQu{{)QleswtefwFT%D<5&r;OE$wSlGk(`P zeoPVnAU4~`o_q+dmi!NcorBoF3|G5v6|Q$``Tux>-I3zIZQ;GkKeP3-EM*7A;NJoF z-%hdbekU`x_vcM&J@28L)!L?&-hbw|Ihf|FoZ9ou*b=PGe^V^q`?dn>YkS(?qdm3s z&Aqe|#gG4HlYR5=pZo7D*|(Lc{Wmw%JkxcbhVM{w`);0XD8_ls%eSpH|4p;(_w8%l zf7i^lG^bi}>K$L);ax$sMY*;(2{c(u=>uF`0WZdm$u~E4Q%dj*SW@k^;1uu zcL$H5jHTE=$EcPZdw|u_j_1*8<0$dj3#?Dt*&D2%y7Mp3{JS^&&9$F3D9-#%{;m8(+z-R4H>k1K_l6Yr#>Uk7cXDQcvp4kV zZsI35@za|4nN9q%CVq1hzq5%iYT^$!@yD9@Q#E(a)(6j|IL0aP^{BgQZ5ngFDtLCS z&8q7Dd+3R^?ix6Lx%oaq-9vGn!h6B`xCYwiQ1nsHd*(dw+`9PPTECMh`f9Ts=T$9v zPXViWAJ+d=${>nspv@TPUOh3VgUgsR;KsODv>D_2s3&GV*gWa$S>V39q_1bg)%8u> zIp8wxTzFqy5_cY4UEj>l`C#*DORfvRWv&b1eRWB$i{R?|CfCJa^Jz=}KLfs`F8TYI zm%{bc<{Y>u)zbbIVB7b+#kszcqGq1>d={)v+P(^`pL*K<9Qg9Oq~BM=)%8!VYr$oE z*TL1UDSWQ4ebU|yaQ)Pi=f)RG<~%>M_#83DQW8q zVEY_?OU={2TjBbs$LEVpKDWX3Q%_%R2RHZS4m5qW*_WHC)spuvuxpqccZ2)tl6k)e zuC8z5z65q$@wpeQpL%@m1KVDF?g#6qZeQ-CR*U}wVEYpOAlNoC{||xnQ8#`OwOahY z3^r%@BVgN4{G(uf)Qx|bS}p#MgT1CR7heJQ)g@#2DqKHpiTfJZeujS?Y~OOtJOS26 zJ$+dWHkY=v_YLsey2S5E_+pB-K{-xpp51Ee1ZCh6vw_lMW3P6Kcc9|=f_}umZrq#Ct&sT_eHRA>iN6AKLgvIwxuXP zrQ~e$Ww3TXY3CKNd!X#g&*AD`=l1Ov6l2vhreA`~F}(_R+-06$!PV2&uffe@`VE?T z#`IgTaq1b<@4&XFt$9qpN7L>n?fe1k`j>rq4X&Os{Sj=e`uuvFuY(<{w&eN~*!d~f z;SIQYa{U=>ta|3>FW_>1{t9ni^S_~~r>(z(ox`%Nf56ovgDOz*;**ZeS^o0wNE*w|Dma8Oz(k>Q_q;* z2iu;u<}vw#quo#18HCon=1ZZeXG|?%W7X%^Yu*ZWtlE-mFxXsWpNF8SC)d(oW7U&u PDA>KA&3AFR@8bUlvh0q` literal 0 HcmV?d00001 diff --git a/packages/ludic.render3d/shaders/spv/manifest.txt b/packages/ludic.render3d/shaders/spv/manifest.txt index 1738f4c7..fcc70708 100644 --- a/packages/ludic.render3d/shaders/spv/manifest.txt +++ b/packages/ludic.render3d/shaders/spv/manifest.txt @@ -628,6 +628,73 @@ T a4683767 u_prefilter 8 T a4683767 u_shadow 9 T a4683767 u_tershadow 10 T a4683767 u_ts_height 11 +P d48d6a48 grass.mesh model.frag #define FOLIAGE;#define BLADE;#define MESH; +B d48d6a48 vert 0 4384 +U d48d6a48 vert u_view 0 mat4 1 0 +U d48d6a48 vert u_proj 64 mat4 1 0 +U d48d6a48 vert u_vp 128 mat4 1 0 +U d48d6a48 vert u_cam_pos 192 vec3 1 0 +U d48d6a48 vert u_time 204 float 1 0 +U d48d6a48 vert u_ts_origin 208 vec2 1 0 +U d48d6a48 vert u_ts_half 216 float 1 0 +U d48d6a48 vert u_ortho_on 220 float 1 0 +U d48d6a48 vert u_lake_level 224 float 1 0 +U d48d6a48 vert u_sea_level 228 float 1 0 +U d48d6a48 vert u_lake 240 vec4 1 0 +U d48d6a48 vert u_snow_line 256 float 1 0 +U d48d6a48 vert u_wind 260 float 1 0 +U d48d6a48 vert u_tile_cells 264 int 1 0 +U d48d6a48 vert u_tiles 272 vec4 256 16 +U d48d6a48 vert u_s0 4368 float 1 0 +U d48d6a48 vert u_d0 4372 float 1 0 +U d48d6a48 vert u_radius 4376 float 1 0 +U d48d6a48 vert u_dbg 4380 int 1 0 +B d48d6a48 frag 1 892 +U d48d6a48 frag u_cascade_vp 0 mat4 5 64 +U d48d6a48 frag u_cascade_split 320 float 5 16 +U d48d6a48 frag u_cascade_range 400 float 5 16 +U d48d6a48 frag u_cascade_texel 480 float 5 16 +U d48d6a48 frag u_sun_dir 560 vec3 1 0 +U d48d6a48 frag u_sun_color 576 vec3 1 0 +U d48d6a48 frag u_cam_pos 592 vec3 1 0 +U d48d6a48 frag u_prefilter_levels 604 float 1 0 +U d48d6a48 frag u_fog_density 608 float 1 0 +U d48d6a48 frag u_fog_height_falloff 612 float 1 0 +U d48d6a48 frag u_fog_base 616 float 1 0 +U d48d6a48 frag u_clip_y 620 float 1 0 +U d48d6a48 frag u_spec_scale 624 float 1 0 +U d48d6a48 frag u_sky_rot 632 vec2 1 0 +U d48d6a48 frag u_ibl_scale 640 vec3 1 0 +U d48d6a48 frag u_daylight 652 float 1 0 +U d48d6a48 frag u_fire_pos 656 vec3 1 0 +U d48d6a48 frag u_fire_color 672 vec3 1 0 +U d48d6a48 frag u_hand_pos 688 vec3 1 0 +U d48d6a48 frag u_hand_color 704 vec3 1 0 +U d48d6a48 frag u_hand_dir 720 vec3 1 0 +U d48d6a48 frag u_hand_cone 732 float 1 0 +U d48d6a48 frag u_ts_origin 736 vec2 1 0 +U d48d6a48 frag u_ts_half 744 float 1 0 +U d48d6a48 frag u_ts_on 748 float 1 0 +U d48d6a48 frag u_force_cascade 752 int 1 0 +U d48d6a48 frag u_cloud_shadow 756 float 1 0 +U d48d6a48 frag u_time 760 float 1 0 +U d48d6a48 frag u_model_h 764 float 1 0 +U d48d6a48 frag u_view 768 mat4 1 0 +U d48d6a48 frag u_tint 832 vec3 1 0 +U d48d6a48 frag u_rough_scale 844 float 1 0 +U d48d6a48 frag u_emissive 848 float 1 0 +U d48d6a48 frag u_blade_base 864 vec3 1 0 +U d48d6a48 frag u_blade_tip 880 vec3 1 0 +T d48d6a48 u_arm 2 +T d48d6a48 u_brdf 3 +T d48d6a48 u_diff 4 +T d48d6a48 u_irradiance 5 +T d48d6a48 u_nrm 6 +T d48d6a48 u_ortho 7 +T d48d6a48 u_prefilter 8 +T d48d6a48 u_shadow 9 +T d48d6a48 u_tershadow 10 +T d48d6a48 u_ts_height 11 P 7ed7de52 grass.vert model.frag #define FOLIAGE;#define BLADE; B 7ed7de52 vert 0 296 U 7ed7de52 vert u_view 0 mat4 1 0 diff --git a/packages/ludic.render3d/shaders/variants.list b/packages/ludic.render3d/shaders/variants.list index 85bcff7e..8dc8e83e 100644 --- a/packages/ludic.render3d/shaders/variants.list +++ b/packages/ludic.render3d/shaders/variants.list @@ -17,6 +17,7 @@ fullscreen.vert|ternormal.frag| fullscreen.vert|tershadow.frag|#define NOISE_ONLY; fullscreen.vert|tonemap.frag| fullscreen.vert|tonemap.frag|#define HDR10; +grass.mesh|model.frag|#define FOLIAGE;#define BLADE;#define MESH; grass.vert|model.frag|#define FOLIAGE;#define BLADE; grass.vert|model.frag|#define FOLIAGE;#define BLADE;#define TILES; impostor.vert|impostor.frag| diff --git a/runtime/native/vk.ludic b/runtime/native/vk.ludic index cd352a15..7ad3fdfd 100644 --- a/runtime/native/vk.ludic +++ b/runtime/native/vk.ludic @@ -43,3 +43,6 @@ extern function vk_sl_call_p(fn: pointer, a: pointer) -> int = "lsl_call_p" extern function vk_sl_call_pp(fn: pointer, a: pointer, b: pointer) -> int = "lsl_call_pp" extern function vk_sl_call_ppp(fn: pointer, a: pointer, b: pointer, c: pointer) -> int = "lsl_call_ppp" extern function vk_sl_call_ip(fn: pointer, a: int, b: pointer) -> int = "lsl_call_ip" +# a command-buffer command with three counts through a pointer (vkCmdDrawMeshTasksEXT, which the +# Streamline interposer does not export; looked up with vkGetDeviceProcAddr) +extern function vk_sl_call_piii(fn: pointer, a: pointer, b: int, c: int, d: int) = "lsl_call_piii"