网站建设需要审批吗网站开发及app开发都需要多少钱
网站建设需要审批吗,网站开发及app开发都需要多少钱,适合小白的室内设计软件,哈尔滨一恒建设上一篇#xff1a;图像/纹理描述符 | 下一篇#xff1a;纹理系统 | 返回目录 #x1f4da; 快速导航 #x1f4cb; 目录
引言学习目标stb_image库简介纹理加载架构实现纹理创建函数实现纹理加载函数透明度检测默认纹理回退机制纹理热重载支持调试事件系统测试纹理切换内存…上一篇图像/纹理描述符 | 下一篇纹理系统 | 返回目录 快速导航 目录引言学习目标stb_image库简介纹理加载架构实现纹理创建函数实现纹理加载函数透明度检测默认纹理回退机制纹理热重载支持调试事件系统测试纹理切换内存管理改进常见问题练习与挑战下一步 引言在前面的教程中我们实现了完整的纹理系统基础设施创建纹理、上传到GPU、在着色器中采样。但是所有的纹理数据都是在代码中硬编码的如默认的棋盘格纹理。真实的游戏需要从磁盘加载各种格式的图像文件PNG、JPG、TGA等。手动实现这些格式的解码器非常复杂幸运的是有一个优秀的单头文件库可以帮助我们stb_image。本教程将展示如何集成stb_image库从磁盘加载PNG等格式的图像检测纹理的透明度支持纹理热重载实现默认纹理回退机制 学习目标目标描述 集成stb_image使用单头文件库加载常见图像格式 实现load_texture从文件路径加载纹理到GPU 透明度检测自动检测纹理是否包含透明通道 热重载支持允许运行时更新纹理 默认纹理回退未加载时使用默认纹理stb_image库简介stb_image 是Sean Barrett编写的一系列单头文件库的一部分。它的优点特性✅单头文件- 只需包含一个.h文件✅公共域许可- 无需担心授权问题✅多种格式- 支持PNG, JPG, TGA, BMP, PSD, GIF, HDR, PIC等✅易于使用- API简洁明了✅零依赖- 不需要外部库支持的格式格式描述常见用途PNG便携式网络图形游戏UI、需要透明度的纹理JPG/JPEG联合图像专家组照片、不需要透明度的纹理TGATruevision TGA游戏纹理、支持Alpha通道BMP位图简单图像PSDPhotoshop文档直接加载PSD文件GIF图形交换格式动画只加载第一帧HDR高动态范围HDR环境贴图添加stb_image到项目在引擎中创建vendor目录并下载stb_image.hengine/ src/ vendor/ stb_image.h ← 将stb_image.h放在这里纹理加载架构让我们先理解纹理加载的完整流程否是游戏请求加载纹理load_texture函数纹理已加载?使用stb_image读取文件销毁旧纹理检测透明度创建暂存缓冲区上传到GPU更新generation释放CPU内存返回成功纹理状态管理纹理生命周期 ┌──────────────────────────────────────┐ │ 状态1: 未初始化 │ │ generation INVALID_ID │ │ internal_data NULL │ ├──────────────────────────────────────┤ │ 状态2: 首次加载 │ │ load_texture() → generation 0 │ │ internal_data vulkan_texture_data│ ├──────────────────────────────────────┤ │ 状态3: 热重载 │ │ load_texture() → generation │ │ (旧纹理被销毁新纹理替换) │ ├──────────────────────────────────────┤ │ 状态4: 使用默认纹理 │ │ generation INVALID_ID │ │ 使用 default_diffuse 回退 │ └──────────────────────────────────────┘实现纹理创建函数首先我们需要一个辅助函数来初始化纹理结构engine/src/renderer/renderer_frontend.c在文件开头添加stb_image#includeresources/resource_types.h// TODO: temporary#includecore/kstring.h#includecore/event.h#defineSTB_IMAGE_IMPLEMENTATION#includevendor/stb_image.h// TODO: end temporary重要STB_IMAGE_IMPLEMENTATION宏必须在包含stb_image.h之前定义且只能在一个.c文件中定义。纹理创建函数/** * brief 创建一个新的纹理结构初始化为未加载状态 * param t 要初始化的纹理指针 */voidcreate_texture(texture*t){kzero_memory(t,sizeof(texture));t-generationINVALID_ID;// 标记为未加载}更新渲染器状态在renderer_system_state中添加测试纹理字段typedefstructrenderer_system_state{renderer_backend backend;mat4 projection;mat4 view;f32 near_clip;f32 far_clip;texture default_texture;// TODO: temporary - 用于测试纹理加载texture test_diffuse;// TODO: end temporary}renderer_system_state;实现纹理加载函数现在实现核心的纹理加载函数engine/src/renderer/renderer_frontend.c/** * brief 从磁盘加载纹理文件 * param texture_name 纹理名称不含扩展名 * param t 要加载到的纹理指针 * return 成功返回true失败返回false */b8load_texture(constchar*texture_name,texture*t){// TODO: 应该能够从任何位置加载char*format_strassets/textures/%s.%s;consti32 required_channel_count4;// 强制转换为RGBA// stb_image默认从上到下加载但OpenGL/Vulkan期望从下到上stbi_set_flip_vertically_on_load(true);charfull_file_path[512];// TODO: 尝试不同的扩展名png, jpg, tga等string_format(full_file_path,format_str,texture_name,png);// 使用临时纹理结构加载texture temp_texture;// 使用stb_image加载图像u8*datastbi_load(full_file_path,(i32*)temp_texture.width,(i32*)temp_texture.height,(i32*)temp_texture.channel_count,required_channel_count);// 强制4通道temp_texture.channel_countrequired_channel_count;if(data){// 保存当前generationu32 current_generationt-generation;t-generationINVALID_ID;// 暂时标记为无效u64 total_sizetemp_texture.width*temp_texture.height*required_channel_count;// 检测透明度b32 has_transparencyfalse;for(u64 i0;itotal_size;irequired_channel_count){u8 adata[i3];// Alpha通道if(a255){has_transparencytrue;break;}}// 获取内部纹理资源并上传到GPUrenderer_create_texture(texture_name,true,temp_texture.width,temp_texture.height,temp_texture.channel_count,data,has_transparency,temp_texture);// 备份旧纹理texture old*t;// 将临时纹理赋值给目标指针*ttemp_texture;// 销毁旧纹理如果存在renderer_destroy_texture(old);// 更新generationif(current_generationINVALID_ID){t-generation0;// 首次加载}else{t-generationcurrent_generation1;// 热重载}// 清理stb_image分配的内存stbi_image_free(data);returntrue;}else{// 加载失败输出错误信息if(stbi_failure_reason()){KWARN(load_texture() failed to load file %s: %s,full_file_path,stbi_failure_reason());}returnfalse;}}函数流程图否是是否开始 load_texture构建文件路径stbi_load 加载文件加载成功?输出错误信息返回 false检测透明度renderer_create_texture备份旧纹理赋值新纹理销毁旧纹理首次加载?generation 0generationstbi_image_free返回 true代码详解1. 路径构建char*format_strassets/textures/%s.%s;string_format(full_file_path,format_str,texture_name,png);// 例如texture_name cobblestone// 结果full_file_path assets/textures/cobblestone.png2. 垂直翻转stbi_set_flip_vertically_on_load(true);为什么需要翻转stb_image默认加载顺序: Vulkan/OpenGL期望顺序: ┌─────────────┐ ┌─────────────┐ │ 顶部 (0,0) │ │ 底部 (0,0) │ │ │ │ │ │ │ flip垂直 │ │ │ │ ═══════ │ │ │ │ │ │ │ 底部 │ │ 顶部 │ └─────────────┘ └─────────────┘3. stbi_load函数u8*datastbi_load(full_file_path,// 文件路径(i32*)temp_texture.width,// [输出] 宽度(i32*)temp_texture.height,// [输出] 高度(i32*)temp_texture.channel_count,// [输出] 实际通道数required_channel_count);// [输入] 强制通道数0保持原样参数说明参数说明full_file_path要加载的图像文件路径width/height输出图像尺寸channel_count输出实际通道数如果forced则是强制值required_channel_count强制转换的通道数0表示保持原样返回值成功返回像素数据指针失败返回NULL 透明度检测自动检测纹理是否包含透明信息这对于后续的渲染优化很重要实现原理b32 has_transparencyfalse;for(u64 i0;itotal_size;irequired_channel_count){u8 adata[i3];// Alpha通道是第4个字节if(a255){has_transparencytrue;break;}}像素布局RGBA像素数据布局 ┌────┬────┬────┬────┬────┬────┬────┬────┐ │ R │ G │ B │ A │ R │ G │ B │ A │ ... ├────┼────┼────┼────┼────┼────┼────┼────┤ │ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ ... └────┴────┴────┴────┴────┴────┴────┴────┘ Pixel 0 Pixel 1 检查透明度 i 0, 4, 8, 12, ... 检查 data[i 3] (Alpha通道) 如果 255则有透明度透明度的用途透明度信息可用于渲染顺序优化- 不透明物体先渲染透明物体后渲染混合模式选择- 透明物体需要启用Alpha混合深度写入控制- 透明物体通常不写入深度剔除优化- 不透明物体可以使用背面剔除默认纹理回退机制当纹理尚未加载时我们应该使用默认纹理而不是渲染黑色或导致崩溃。更新渲染器后端接口engine/src/renderer/renderer_types.inltypedefstructrenderer_backend{u64 frame_number;// 指向默认纹理的指针texture*default_diffuse;// ... 其他字段 ...}renderer_backend;在初始化时设置默认纹理engine/src/renderer/renderer_frontend.cb8renderer_system_initialize(u64*memory_requirement,void*state,constchar*application_name){// ... 现有代码 ...state_ptrstate;// 让后端可以访问默认纹理state_ptr-backend.default_diffusestate_ptr-default_texture;// 初始化后端renderer_backend_create(RENDERER_BACKEND_TYPE_VULKAN,state_ptr-backend);// ... 创建默认棋盘格纹理 ...// 手动设置默认纹理的generation为INVALID_IDstate_ptr-default_texture.generationINVALID_ID;// 创建测试纹理初始未加载create_texture(state_ptr-test_diffuse);returntrue;}Vulkan着色器中的回退逻辑engine/src/renderer/vulkan/vulkan_types.inl在vulkan_object_shader结构中添加typedefstructvulkan_object_shader{// ... 现有字段 ...// 指向默认纹理的指针texture*default_diffuse;// ... 其他字段 ...}vulkan_object_shader;engine/src/renderer/vulkan/shaders/vulkan_object_shader.h更新函数签名b8vulkan_object_shader_create(vulkan_context*context,texture*default_diffuse,// 添加默认纹理参数vulkan_object_shader*out_shader);engine/src/renderer/vulkan/shaders/vulkan_object_shader.c在创建函数中保存默认纹理指针b8vulkan_object_shader_create(vulkan_context*context,texture*default_diffuse,vulkan_object_shader*out_shader){// 保存默认纹理指针out_shader-default_diffusedefault_diffuse;// ... 其余创建代码 ...}在vulkan_object_shader_update_object函数中添加回退逻辑// 采样器描述符部分constu32 sampler_count1;VkDescriptorImageInfo image_infos[1];for(u32 sampler_index0;sampler_indexsampler_count;sampler_index){texture*tdata.textures[sampler_index];u32*descriptor_generationobject_state-descriptor_states[descriptor_index].generations[image_index];// 如果纹理还未加载使用默认纹理// TODO: 根据纹理用途选择合适的默认纹理漫反射/法线/金属度等if(t-generationINVALID_ID){tshader-default_diffuse;// 重置描述符generation以使用默认纹理*descriptor_generationINVALID_ID;}// 检查描述符是否需要更新if(t(*descriptor_generation!t-generation||*descriptor_generationINVALID_ID)){// ... 更新描述符代码 ...}}回退机制流程是否是否获取对象纹理纹理generation INVALID_ID?使用 default_diffuse使用对象纹理重置描述符generation需要更新描述符?更新VkDescriptorSet跳过更新绑定描述符默认纹理的特殊处理默认纹理标记 generation INVALID_ID 检测逻辑 if (t-generation INVALID_ID) { // 未加载 → 使用默认纹理 } 默认纹理本身 default_texture.generation INVALID_ID // 永不触发更新纹理热重载支持热重载允许在游戏运行时替换纹理这对于开发和调试非常有用。Generation机制回顾// 首次加载if(current_generationINVALID_ID){t-generation0;}// 热重载else{t-generationcurrent_generation1;}热重载流程初始状态 texture.generation INVALID_ID descriptor.generation INVALID_ID 第一次加载 cobblestone.png texture.generation 0 描述符更新descriptor.generation 0 热重载 paving.png 1. 备份旧纹理generation0 2. 加载新纹理数据 3. texture.generation 1 4. 销毁旧纹理GPU资源 5. 下一帧渲染时检测到 descriptor.generation(0) ! texture.generation(1) 6. 更新描述符descriptor.generation 1旧纹理销毁处理// 备份旧纹理texture old*t;// 将新纹理赋值给指针*ttemp_texture;// 销毁旧纹理的GPU资源renderer_destroy_texture(old);改进纹理销毁函数以安全处理空指针engine/src/renderer/vulkan/vulkan_backend.cvoidvulkan_renderer_destroy_texture(structtexture*texture){vkDeviceWaitIdle(context.device.logical_device);vulkan_texture_data*data(vulkan_texture_data*)texture-internal_data;// 检查internal_data是否有效if(data){vulkan_image_destroy(context,data-image);kzero_memory(data-image,sizeof(vulkan_image));vkDestroySampler(context.device.logical_device,data-sampler,context.allocator);data-sampler0;kfree(texture-internal_data,sizeof(vulkan_texture_data),MEMORY_TAG_TEXTURE);}kzero_memory(texture,sizeof(structtexture));}调试事件系统为了测试纹理加载和热重载我们添加调试事件。添加调试事件码engine/src/core/event.htypedefenumsystem_event_code{// ... 现有事件码 ...EVENT_CODE_RESIZED0x08,// 调试事件EVENT_CODE_DEBUG00x10,EVENT_CODE_DEBUG10x11,EVENT_CODE_DEBUG20x12,EVENT_CODE_DEBUG30x13,EVENT_CODE_DEBUG40x14,MAX_EVENT_CODE0xFF}system_event_code;实现纹理切换事件处理器engine/src/renderer/renderer_frontend.c/** * brief 调试事件处理器循环切换测试纹理 */b8event_on_debug_event(u16 code,void*sender,void*listener_inst,event_context data){constchar*names[3]{cobblestone,paving,paving2};statici8 choice2;// 循环选择下一个纹理choice;choice%3;// 加载新纹理load_texture(names[choice],state_ptr-test_diffuse);returntrue;}注册和注销事件在renderer_system_initialize中b8renderer_system_initialize(...){// ... 现有代码 ...state_ptrstate;// TODO: temp - 注册调试事件event_register(EVENT_CODE_DEBUG0,state_ptr,event_on_debug_event);// TODO: end temp// ... 其余初始化代码 ...}在renderer_system_shutdown中voidrenderer_system_shutdown(void*state){if(state_ptr){// TODO: temp - 注销调试事件event_unregister(EVENT_CODE_DEBUG0,state_ptr,event_on_debug_event);// TODO: end temp// 销毁纹理renderer_destroy_texture(state_ptr-default_texture);renderer_destroy_texture(state_ptr-test_diffuse);state_ptr-backend.shutdown(state_ptr-backend);}state_ptr0;}测试纹理切换在testbed中添加按键触发纹理切换。testbed/src/game.c#includecore/input.h#includecore/event.hb8game_update(game*game_inst,f32 delta_time){// ... 现有更新代码 ...// TODO: temp - 按T键切换纹理if(input_is_key_up(T)input_was_key_down(T)){KDEBUG(Swapping texture!);event_context context{};event_fire(EVENT_CODE_DEBUG0,game_inst,context);}// TODO: end temp// ... 其余游戏逻辑 ...}使用测试纹理渲染engine/src/renderer/renderer_frontend.c在renderer_draw_frame中b8renderer_draw_frame(render_packet*packet){if(renderer_begin_frame(packet-delta_time)){state_ptr-backend.update_global_state(state_ptr-projection,state_ptr-view,vec3_zero(),vec4_one(),0);mat4 modelmat4_translation((vec3){0,0,0});geometry_render_data data{};data.object_id0;data.modelmodel;data.textures[0]state_ptr-test_diffuse;// 使用测试纹理state_ptr-backend.update_object(data);// ... 结束帧 ...}}测试流程用户输入系统事件系统渲染器GPU按下 T 键fire(EVENT_CODE_DEBUG0)event_on_debug_event()load_texture(paving)上传新纹理数据generation下一帧检测generation变化更新描述符显示新纹理用户输入系统事件系统渲染器GPU内存管理改进暂存缓冲区销毁时机之前的代码在传输数据之前就销毁了暂存缓冲区这是错误的。修正engine/src/renderer/vulkan/vulkan_backend.cvoidvulkan_renderer_create_texture(...){// ... 创建暂存缓冲区和图像 ...// 转换布局vulkan_image_transition_layout(...);// 从暂存缓冲区复制数据vulkan_image_copy_from_buffer(context,data-image,staging.handle,temp_buffer);// ❌ 错误不应该在这里销毁// vulkan_buffer_destroy(context, staging);// 转换到shader-read-only布局vulkan_image_transition_layout(...);// 结束并提交命令缓冲区vulkan_command_buffer_end_single_use(context,pool,temp_buffer,queue);// ✅ 正确在命令提交后销毁暂存缓冲区vulkan_buffer_destroy(context,staging);// ... 创建采样器 ...}为什么顺序重要正确顺序 1. vkCmdCopyBufferToImage ← 记录复制命令 2. vkQueueSubmit ← 提交命令队列 3. 命令执行 ← GPU读取staging buffer 4. vulkan_buffer_destroy ← 销毁staging buffer 错误顺序 1. vkCmdCopyBufferToImage 2. vulkan_buffer_destroy ← ❌ 过早销毁 3. vkQueueSubmit 4. GPU尝试读取已销毁的buffer ← 崩溃或数据损坏完整的纹理加载示例让我们看一个完整的使用示例准备纹理资源项目结构 assets/ textures/ cobblestone.png ← 512x512 石头纹理 paving.png ← 1024x1024 铺路纹理 paving2.png ← 1024x1024 铺路纹理变体加载和使用纹理// 在游戏初始化中texture my_texture;create_texture(my_texture);// 在合适的时机加载if(load_texture(cobblestone,my_texture)){KINFO(Texture loaded successfully!);KINFO( Size: %dx%d,my_texture.width,my_texture.height);KINFO( Channels: %d,my_texture.channel_count);KINFO( Transparency: %s,my_texture.has_transparency?Yes:No);KINFO( Generation: %u,my_texture.generation);}else{KERROR(Failed to load texture!);}// 在渲染时使用geometry_render_data data;data.textures[0]my_texture;renderer_update_object(data);// 热重载纹理if(should_reload){load_texture(paving,my_texture);// generation会自动递增}// 在关闭时清理renderer_destroy_texture(my_texture);❓ 常见问题❓ 为什么要强制转换为4通道RGBA虽然很多图像格式支持1通道灰度或3通道RGB但强制转换为4通道有几个好处对齐要求- GPU更喜欢4字节对齐的数据统一处理- 简化着色器代码不需要处理不同格式性能- 4通道访问通常更快未来扩展- 预留Alpha通道用于混合或遮罩如果原图像是RGB3通道stb_image会自动添加A255的Alpha通道。❓ stb_image分配的内存需要手动释放吗是的stb_image使用malloc分配内存必须使用stbi_image_free释放u8*datastbi_load(...);if(data){// 使用datarenderer_create_texture(...,data,...);// 必须释放stbi_image_free(data);}不要使用free(data)或kfree(data)因为stb_image可能使用自定义的内存分配器。❓ 为什么默认纹理的generation是INVALID_ID默认纹理是特殊的永不改变- 默认纹理在运行时不会被替换回退标记- generationINVALID_ID 作为未加载的标记避免更新- 保持INVALID_ID可以避免不必要的描述符更新当对象纹理未加载时if(object_texture-generationINVALID_ID){// 使用默认纹理texture*tshader-default_diffuse;}❓ 如何支持多种图像格式PNG、JPG、TGA当前实现只尝试PNG。可以改进为尝试多个扩展名b8load_texture(constchar*texture_name,texture*t){constchar*extensions[]{png,jpg,jpeg,tga,bmp};constu32 ext_count5;for(u32 i0;iext_count;i){charfull_file_path[512];string_format(full_file_path,assets/textures/%s.%s,texture_name,extensions[i]);// 检查文件是否存在if(filesystem_exists(full_file_path)){u8*datastbi_load(full_file_path,...);if(data){// 成功加载// ... 处理纹理 ...returntrue;}}}KWARN(Texture %s not found with any supported extension,texture_name);returnfalse;}❓ 透明度检测会不会很慢对于大纹理如4K4096x4096x4检测所有像素可能需要一些时间4096 * 4096 * 4 67,108,864 字节 64 MB优化策略早退出- 一旦发现透明像素就停止代码中已实现采样检测- 只检查部分像素预计算- 在纹理导出时存储透明度标志异步加载- 在后台线程中加载和检测当前实现的早退出策略对于大多数纹理已经足够快。 练习与挑战练习1添加多格式支持修改load_texture函数以尝试多个文件扩展名。查看提示创建扩展名数组并循环尝试constchar*extensions[]{png,jpg,tga};for(u32 i0;i3;i){string_format(full_file_path,format_str,texture_name,extensions[i]);u8*datastbi_load(full_file_path,...);if(data){// 成功break;}}练习2实现纹理资源管理器创建一个纹理管理器来缓存已加载的纹理typedefstructtexture_manager{texture textures[256];u32 count;}texture_manager;texture*texture_manager_get(texture_manager*mgr,constchar*name);查看提示思路在renderer_system_state中添加texture_managertexture_manager_get首先查找是否已加载如果未找到调用load_texture并缓存使用哈希表或简单的线性搜索练习3异步纹理加载实现后台线程加载纹理避免阻塞主线程。查看提示架构typedefstructtexture_load_request{charname[256];texture*target;b8 complete;}texture_load_request;voidtexture_loader_thread(void*arg){while(running){if(has_pending_request){// 后台加载u8*datastbi_load(...);// 标记完成主线程会检测request-completetrue;}}}主线程检测complete标志调用renderer_create_texture必须在渲染线程。 下一步在本教程中我们完成了纹理系统的重要一环✅ 集成stb_image库✅ 从磁盘加载PNG等格式✅ 自动检测透明度✅ 支持纹理热重载✅ 实现默认纹理回退但是当前的实现还比较简单每次加载纹理都需要手动调用load_texture。在下一篇教程中我们将实现完整的纹理系统包括纹理资源管理器和缓存按需自动加载引用计数和自动释放纹理资源池更高效的资源管理结语恭喜你完成了本教程现在你的引擎可以从磁盘加载真实的纹理图像了。从硬编码的棋盘格纹理到加载PNG图像我们的引擎变得越来越实用。虽然还有很多可以改进的地方异步加载、纹理流式加载、压缩格式支持等但现在的基础已经足够开始创建简单的3D场景了。关键要点回顾stb_image提供了简单易用的图像加载功能Generation机制支持纹理热重载默认纹理回退避免了渲染错误透明度检测为后续渲染优化提供信息调试事件系统方便功能测试关注公众号获取更多引擎开发资讯觉得有帮助请作者喝杯咖啡 作者: 上手实验室 项目地址: https://github.com/travisvroman/kohi上一篇图像/纹理描述符 | 下一篇纹理系统