Skip to main content

forge-3d-picking

Add GPU-based 3D object picking to an SDL GPU project. Implement color-ID picking with offscreen render targets, stencil-ID picking with per-object reference values, GPU-to-CPU readback with transfer buffers, and stencil outline selection highlighting.

Ir a la instalación

Datos de origen

Repositorio
Nebulavenus/forge-gpu
Última actividad en el origen
11 de marzo de 2026 a las 05:24
Idioma detectado de SKILL.md
inglés
Estrellas
38
Forks
7

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
forge-3d-picking
description
Add GPU-based 3D object picking to an SDL GPU project. Implement color-ID picking with offscreen render targets, stencil-ID picking with per-object reference values, GPU-to-CPU readback with transfer buffers, and stencil outline selection highlighting.
triggers
["3d picking","object picking","object selection","color picking","click to select","GPU readback","mouse picking","ray picking","select object"]
# 3D Picking Add GPU-based object picking to an SDL GPU project. This skill covers two picking methods (color-ID and stencil-ID), GPU-to-CPU data transfer with `SDL_DownloadFromGPUTexture`, transfer buffer readback, and stencil outline selection highlighting. Based on [GPU Lesson 37](../../../lessons/gpu/37-3d-picking/). ## When to use - You need to identify which object the user clicked on in a 3D scene - You want pixel-perfect selection without CPU-side ray casting - You need to highlight selected objects with a visible outline - You want to read data back from the GPU to the CPU ## Color-ID picking pipeline Color-ID picking renders each object with a unique flat color to an offscreen texture, then reads back the single pixel under the cursor to identify the object. This is the recommended approach — it is portable, supports up to 65,535 objects, and the RGBA readback has a consistent byte layout across all GPU backends. ### Index-to-color encoding ```c /* Convert object index to a unique RGB color. * Index 0 maps to ID 1 (background is (0,0,0) = no object). */ static void index_to_color(int index, float *r, float *g, float *b) { int id = index + 1; *r = (float)((id >> 0) & 0xFF) / 255.0f; *g = (float)((id >> 8) & 0xFF) / 255.0f; *b = 0.0f; } /* Decode a read-back pixel back to an object index. Returns -1 for background. */ static int color_to_index(Uint8 r, Uint8 g, Uint8 b) { (void)b; int id = (int)r | ((int)g << 8); if (id == 0) return -1; return id - 1; } ``` ### Offscreen render target Create an `R8G8B8A8_UNORM` texture as the color-ID render target. Do **not** use the swapchain format — you need a known pixel layout for reliable CPU readback. Also create a separate depth buffer for occlusion: ```c /* Color-ID offscreen target */ SDL_GPUTextureCreateInfo ci; SDL_zero(ci); ci.type = SDL_GPU_TEXTURETYPE_2D; ci.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM; ci.width = window_w; ci.height = window_h; ci.layer_count_or_depth = 1; ci.num_levels = 1; ci.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET; SDL_GPUTexture *id_texture = SDL_CreateGPUTexture(device, &ci); /* ID pass depth buffer — separate from the main scene depth */ SDL_GPUTextureCreateInfo depth_ci; SDL_zero(depth_ci); depth_ci.type = SDL_GPU_TEXTURETYPE_2D; depth_ci.format = depth_stencil_fmt; /* D24_UNORM_S8_UINT or D32_FLOAT_S8_UINT */ depth_ci.width = window_w; depth_ci.height = window_h; depth_ci.layer_count_or_depth = 1; depth_ci.num_levels = 1; depth_ci.usage = SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET; SDL_GPUTexture *id_depth = SDL_CreateGPUTexture(device, &depth_ci); ``` ### ID pass shaders The vertex shader only needs MVP — no lighting outputs: ```hlsl cbuffer IdVertUniforms : register(b0, space1) { column_major float4x4 mvp; }; struct VSInput { float3 position : TEXCOORD0; float3 normal : TEXCOORD1; /* unused — must match vertex layout */ }; float4 main(VSInput input) : SV_Position { return mul(mvp, float4(input.position, 1.0)); } ``` The fragment shader outputs the flat ID color: ```hlsl cbuffer IdFragUniforms : register(b0, space3) { float4 id_color; }; float4 main(float4 pos : SV_Position) : SV_Target { return id_color; } ``` ### ID pipeline configuration ```c SDL_GPUColorTargetDescription ct; SDL_zero(ct); ct.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM; SDL_GPUGraphicsPipelineCreateInfo pi; SDL_zero(pi); pi.vertex_shader = id_pass_vert; pi.fragment_shader = id_pass_frag; pi.vertex_input_state = scene_vis; /* same vertex layout as scene */ pi.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; pi.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL; pi.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_BACK; pi.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE; pi.depth_stencil_state.compare_op = SDL_GPU_COMPAREOP_LESS; pi.depth_stencil_state.enable_depth_test = true; pi.depth_stencil_state.enable_depth_write = true; pi.depth_stencil_state.enable_stencil_test = false; pi.target_info.color_target_descriptions = &ct; pi.target_info.num_color_targets = 1; pi.target_info.depth_stencil_format = depth_stencil_fmt; pi.target_info.has_depth_stencil_target = true; ``` ### ID pass execution Only run the ID pass on click frames to avoid wasting GPU time: ```c if (pick_pending && picking_method == PICK_COLOR_ID) { SDL_GPUColorTargetInfo id_color_target; SDL_zero(id_color_target); id_color_target.texture = id_texture; id_color_target.load_op = SDL_GPU_LOADOP_CLEAR; id_color_target.store_op = SDL_GPU_STOREOP_STORE; /* Clear to (0,0,0,0) — decodes to "no object" */ SDL_GPUDepthStencilTargetInfo id_ds; SDL_zero(id_ds); id_ds.texture = id_depth; id_ds.clear_depth = 1.0f; id_ds.load_op = SDL_GPU_LOADOP_CLEAR; id_ds.store_op = SDL_GPU_STOREOP_DONT_CARE; id_ds.cycle = true; SDL_GPURenderPass *pass = SDL_BeginGPURenderPass( cmd, &id_color_target, 1, &id_ds); SDL_BindGPUGraphicsPipeline(pass, id_pipeline); for (int i = 0; i < object_count; i++) { mat4 mvp = mat4_multiply(cam_vp, object_models[i]); IdVertUniforms vu = { .mvp = mvp }; SDL_PushGPUVertexUniformData(cmd, 0, &vu, sizeof(vu)); float id_r, id_g, id_b; index_to_color(i, &id_r, &id_g, &id_b); IdFragUniforms fu = {{ id_r, id_g, id_b, 1.0f }}; SDL_PushGPUFragmentUniformData(cmd, 0, &fu, sizeof(fu)); /* Bind and draw object geometry */ SDL_DrawGPUIndexedPrimitives(pass, idx_count, 1, 0, 0, 0); } SDL_EndGPURenderPass(pass); } ``` ## GPU readback with SDL_DownloadFromGPUTexture ### Transfer buffer for readback Create a `DOWNLOAD` transfer buffer once at init. For color-ID picking (RGBA8), 4 bytes suffices. For stencil-ID picking, `D32_FLOAT_S8_UINT` needs 8 bytes per pixel. Use `SDL_CalculateGPUTextureFormatSize` to size correctly for either: ```c SDL_GPUTransferBufferCreateInfo xfer_ci; SDL_zero(xfer_ci); xfer_ci.usage = SDL_GPU_TRANSFERBUFFERUSAGE_DOWNLOAD; /* Size for the larger format to support both color-ID and stencil-ID */ Uint32 color_bpp = SDL_CalculateGPUTextureFormatSize(SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM, 1, 1, 1); Uint32 ds_bpp = SDL_CalculateGPUTextureFormatSize(ds_format, 1, 1, 1); xfer_ci.size = (color_bpp > ds_bpp) ? color_bpp : ds_bpp; SDL_GPUTransferBuffer *pick_readback = SDL_CreateGPUTransferBuffer(device, &xfer_ci); ``` ### Copy pass Download a single pixel from the source texture into the transfer buffer: ```c SDL_GPUCopyPass *copy = SDL_BeginGPUCopyPass(cmd); SDL_GPUTextureRegion src_region; SDL_zero(src_region); src_region.texture = id_texture; /* or depth-stencil for stencil-ID */ src_region.x = (Uint32)pick_x; src_region.y = (Uint32)pick_y; src_region.w = 1; src_region.h = 1; src_region.d = 1; SDL_GPUTextureTransferInfo dst_info; SDL_zero(dst_info); dst_info.transfer_buffer = pick_readback; dst_info.offset = 0; SDL_DownloadFromGPUTexture(copy, &src_region, &dst_info); SDL_EndGPUCopyPass(copy); ``` ### Wait, map, and decode After submitting the command buffer, wait for GPU completion before reading: ```c if (!SDL_SubmitGPUCommandBuffer(cmd)) { SDL_Log("SDL_SubmitGPUCommandBuffer failed: %s", SDL_GetError()); return; } if (!SDL_WaitForGPUIdle(device)) { SDL_Log("SDL_WaitForGPUIdle failed: %s", SDL_GetError()); return; } void *pixel_data = SDL_MapGPUTransferBuffer(device, pick_readback, false); if (pixel_data) { Uint8 *bytes = (Uint8 *)pixel_data; int picked = color_to_index(bytes[0], bytes[1], bytes[2]); selected_object = (picked >= 0 && picked < object_count) ? picked : -1; SDL_UnmapGPUTransferBuffer(device, pick_readback); } ``` ## Stencil-ID picking An alternative that reuses the scene stencil buffer. Limited to 255 objects and has backend-dependent byte layout, but requires no extra render pass. ### Pipeline setup ```c pi.depth_stencil_state.enable_stencil_test = true; pi.depth_stencil_state.front_stencil_state = (SDL_GPUStencilOpState){ .fail_op = SDL_GPU_STENCILOP_KEEP, .pass_op = SDL_GPU_STENCILOP_REPLACE, .depth_fail_op = SDL_GPU_STENCILOP_KEEP, .compare_op = SDL_GPU_COMPAREOP_ALWAYS, }; pi.depth_stencil_state.back_stencil_state = pi.depth_stencil_state.front_stencil_state; pi.depth_stencil_state.write_mask = 0xFF; pi.depth_stencil_state.compare_mask = 0xFF; ``` ### Per-object stencil reference ```c for (int i = 0; i < object_count; i++) { SDL_SetGPUStencilReference(pass, (Uint8)(i + 1)); /* ... draw object i ... */ } ``` ### D24S8 readback Download from the depth-stencil texture instead of the ID texture. The stencil byte position varies by GPU vendor: ```c Uint8 stencil = bytes[3]; int picked = (int)stencil - 1; if (picked < 0 || picked >= object_count) { stencil = bytes[0]; /* fallback for reversed byte order */ picked = (int)stencil - 1; } ``` ## Selection outline with stencil Highlight the selected object using the two-pass stencil outline technique from [Lesson 34](../../../lessons/gpu/34-stencil-testing/): ### Outline write pipeline ```c /* Pass 1: draw selected object, write stencil marker */ pi.depth_stencil_state.enable_stencil_test = true; pi.depth_stencil_state.front_stencil_state = (SDL_GPUStencilOpState){ .fail_op = SDL_GPU_STENCILOP_KEEP, .pass_op = SDL_GPU_STENCILOP_REPLACE, .depth_fail_op = SDL_GPU_STENCILOP_KEEP, .compare_op = SDL_GPU_COMPAREOP_ALWAYS, }; pi.depth_stencil_state.back_stencil_state = pi.depth_stencil_state.front_stencil_state; pi.depth_stencil_state.write_mask = 0xFF; pi.depth_stencil_state.compare_mask = 0xFF; ``` ### Outline draw pipeline ```c /* Pass 2: draw scaled-up object where stencil != marker */ pi.depth_stencil_state.enable_depth_test = false; pi.depth_stencil_state.enable_depth_write = false; pi.depth_stencil_state.enable_stencil_test = true; pi.depth_stencil_state.front_stencil_state = (SDL_GPUStencilOpState){ .fail_op = SDL_GPU_STENCILOP_KEEP, .pass_op = SDL_GPU_STENCILOP_KEEP, .depth_fail_op = SDL_GPU_STENCILOP_KEEP, .compare_op = SDL_GPU_COMPAREOP_NOT_EQUAL, }; pi.depth_stencil_state.back_stencil_state = pi.depth_stencil_state.front_stencil_state; pi.depth_stencil_state.write_mask = 0x00; /* don't modify stencil */ pi.depth_stencil_state.compare_mask = 0xFF; pi.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE; ``` ### Outline rendering ```c #define STENCIL_OUTLINE 200 /* high value to avoid picking collisions */ #define OUTLINE_SCALE 1.04f /* Outline pass: LOAD color + depth, CLEAR stencil */ SDL_GPUColorTargetInfo outline_color = { .texture = swapchain_tex, .load_op = SDL_GPU_LOADOP_LOAD, .store_op = SDL_GPU_STOREOP_STORE, }; SDL_GPUDepthStencilTargetInfo outline_ds = { .texture = main_depth, .load_op = SDL_GPU_LOADOP_LOAD, .store_op = SDL_GPU_STOREOP_DONT_CARE, .stencil_load_op = SDL_GPU_LOADOP_CLEAR, .stencil_store_op = SDL_GPU_STOREOP_DONT_CARE, .clear_stencil = 0, }; SDL_GPURenderPass *pass = SDL_BeginGPURenderPass( cmd, &outline_color, 1, &outline_ds); /* Step 1: draw object normally, write stencil marker */ SDL_BindGPUGraphicsPipeline(pass, outline_write_pipeline); SDL_SetGPUStencilReference(pass, STENCIL_OUTLINE); /* ... push uniforms, draw selected object ... */
Ver en GitHub
Este SKILL.md es muy grande, por eso SkillsMP muestra aqui solo la primera seccion. Ver en GitHub