ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C# obj目录与OBJ模型文件解析:MSBuild中间产物及C#读取指南

C# obj目录与OBJ模型文件解析:MSBuild中间产物及C#读取指南 简介一套基于C#与Visual Studio 2019的OBJ模型加载工程示例面向需要学习WPF三维图形展示或OBJ解析的初中级开发者。示例采用WavefrontObjLoader读取OBJ文件并通过WPF窗口呈现精细人体、海底世界等模型清晰演示了模型数据从文件解析到界面绑定的完整流程。压缩包共43个文件大小约16.49MB包含cs源码、xaml界面、obj模型数据以及编译生成的exe、dll、pdb等目录结构完整便于直接运行调试和对比学习。目前已有411人学习下载。通过阅读工程可掌握OBJ格式中顶点、法线、面索引的解析要点理解C#中加载外部模型并进行渲染展示的实现思路同时可作为后续扩展纹理贴图、模型交互控制等功能的起点。1. 当 VS2019 构建 C# 项目时obj 目录里到底发生了什么如果你用 Visual Studio 2019 写过 C# 代码大概率见过解决方案里每个 .csproj 下面都有一个 obj 目录。它没有 bin 那么显眼但文件数量不少名字也怪很多人会当作临时垃圾直接删掉。删完再编译系统往往会从头构建一遍一个中型 WPF 项目可能要等一分多钟。另一个容易混淆的场景是做 3D 编辑器时想用 C# 读取 .obj 模型文件结果发现网上的示例全是 C 老代码。这两个问题都指向同一个关键词“obj”但一个是 MSBuild 的中间输出目录一个是 Wavefront 的三维模型文本格式。搞清楚这两者你才知道在 VS2019 里哪些文件能删、哪些不能删以及解析 .obj 模型时需要注意哪些细节。这篇内容主要面向正在维护 C# 工程、想优化构建速度或顺手做图形工具的人。2. 在 VS2019 中解析 C# obj 目录的结构与文件角色2.1 obj 和 bin两个输出目录的分工在 VS2019 里新建一个 C# 项目并编译后解决方案资源管理器中默认不显示 obj 和 bin但在文件资源管理器里它们都在。bin 目录放最终可以运行或被引用的程序集Debug/Release、x86/x64 都会在这里分成子目录。obj 目录存的是编译过程中的中间产物包括临时生成的 C# 源码、AssemblyInfo、核心编译缓存、FileList 等。两者的关系可以简单理解为obj 是半成品bin 是成品。MSBuild 会先把编译器生成的中间文件写入 obj再经过后续任务复制或打包到 bin。下面这张表可以快速建立印象目录典型内容生命周期删除后影响bindll、exe、pdb、config、依赖项每次构建重建触发完整输出复制不影响源码obj临时源码、缓存、FileList、中间 dll每次构建维护丢失增量编译状态下次全量编译很多开发者以为 bin 删掉就足够其实真正决定增量编译精度的是 obj 中的缓存文件。如果不小心把 obj 删了bin 可能还在但 MSBuild 无法判断哪些输入文件变化过只能强制重新编译所有任务。2.2 obj 目录里的主要文件与作用不同项目类型和 SDK 版本会让 obj 里的文件有差异但 VS2019 下最常见的几个角色是固定的。首先是{项目名}.csproj.CoreCompileInputs.cache。它记录编译器输入文件的路径和内容哈希MSBuild 在后续构建时用它判断是否真的需要重新调用 C# 编译器。这个文件一旦缺失项目会被当作“从未编译过”所有 .cs 文件都会重新过一遍 Roslyn构建时间明显增加。其次是{项目名}.AssemblyInfo.cs和{项目名}.AssemblyAttributes.cs。这是 MSBuild 在编译前根据 .csproj 里的版本、语言区域、程序集名称等属性自动生成的 C# 源码会和手写代码一起参与编译。你不应该手动修改它们因为下次构建又会被重新生成。还有{项目名}.csproj.FileListAbsolute.txt它列出当前配置将会生成的文件绝对路径。安装项目、ClickOnce 或部署任务经常读取这个列表用来判断哪些文件需要拷贝。删除它一般不影响正常构建但某些第三方打包工具可能会报找不到文件列表。如果是 WPF 或 WinForms 项目obj 里还会出现*.g.cs、*.g.i.cs。这些是 XAML 编译器为每个 Window、Page 生成的分部类代码把 InitializeComponent 和字段定义塞进去。类似地资源文件会生成临时.resources。最终这些中间 dll 会参与二次复制到 bin。如果你用的是 .NET Core 或 .NET Standard 格式的 SDK 风格项目obj 下还会有project.assets.json。它不是编译产物而是 NuGet 还原结果描述项目引用的包关系。删除之后下一次构建会触发 restore过程稍慢但不会造成数据丢失。2.3 MSBuild 是如何决定 obj 路径的在 VS2019 里路径不是写死在程序里的而是由 MSBuild 属性控制。常见做法是在 .csproj 中覆盖BaseIntermediateOutputPath和IntermediateOutputPath但大多数项目不需要自己写因为 SDK 默认已经给了一套比较合理的规则。看一个典型的 SDK 风格项目片段PropertyGroup BaseIntermediateOutputPathobj\/BaseIntermediateOutputPath IntermediateOutputPath$(BaseIntermediateOutputPath)$(Configuration)\/IntermediateOutputPath OutputPathbin\$(Configuration)\/OutputPath /PropertyGroupBaseIntermediateOutputPath是 obj 的根目录默认是项目目录下的obj\。IntermediateOutputPath通常在其基础上拼接Configuration所以 Debug 编译的中间文件在obj\Debug\Release 在obj\Release\。OutputPath控制最终输出默认是bin\Configuration\不要和IntermediateOutputPath混在一起改。还有一个需要留意的属性是PlatformName。旧式非 SDK 风格项目在切换 x86/x64 时IntermediateOutputPath不一定包含平台名导致两个平台的中间文件写进同一个目录。我一般会在多平台支持的项目里手动加上平台条件后续第 3 章会给出具体写法。2.4 为什么 obj 目录可以直接删除因为它是构建缓存而不是源文件或第三方包的最终落点。删除它不会让代码丢失但会让下一次构建从增量变为全量。原理在于CoreCompileInputs.cache中的哈希比较MSBuild 将编译任务的输入文件路径和内容哈希写进缓存后续构建时重新计算当前文件哈希并与之比较。如果一致就跳过编译如果不一致或者缓存文件不存在Roslyn 会重新编译整个项目。所以日常手动清理磁盘空间时删除 obj 是安全的但要有一个心理预期下次打开 VS2019 构建会明显变慢。某些团队在 CI 流水线里每次构建前清理全部 obj这会牺牲增量编译的好处。换来的好处是减少文件残留避免本机与 CI 环境之间的路径状态不一致。3. 在 VS2019 里配置和管理 C# 的 obj 输出路径3.1 修改 BaseIntermediateOutputPath 将 obj 重定向到统一目录大型解决方案中多个项目各自生成 obj 会显得很乱。为了避免开发者误提交或打包工具扫到无关文件我一般会把 obj 重定向到解决方案级别的Intermediate目录。在 .csproj 中加入PropertyGroup BaseIntermediateOutputPath$(SolutionDir)Intermediate\$(MSBuildProjectName)\/BaseIntermediateOutputPath /PropertyGroup这里$(SolutionDir)指向解决方案文件所在目录末尾自带反斜杠。$(MSBuildProjectName)是当前项目文件名不含扩展名用它避免不同项目重定向到同一个文件夹。改完后要关闭并重新加载 VS2019不然路径缓存不会刷新。注意一点这个属性改动会同时影响所有使用该项目的构建环境。如果 CI 用的是不同目录结构$(SolutionDir)不可用时路径解析会失败。此时可以改成基于$(MSBuildProjectDirectory)的相对路径例如..\build\obj\$(MSBuildProjectName)\但要注意父目录权限。3.2 按平台和配置区分中间目录避免 x86/x64 互相覆盖如果 C# 项目引用了 C 原生库或者使用 AnyCPU 之外的目标平台建议在 csproj 中显式区分平台。在 VS2019 中常见的错误是先在 x86 下构建再切到 x64结果 obj 里的旧缓存没有失效链接时引用了错误的原生依赖。一个简单的条件属性方案如下PropertyGroup Condition$(Platform) x86 IntermediateOutputPathobj\x86\$(Configuration)\/IntermediateOutputPath /PropertyGroup PropertyGroup Condition$(Platform) x64 IntermediateOutputPathobj\x64\$(Configuration)\/IntermediateOutputPath /PropertyGroupCondition里的$(Platform)来自解决方案配置管理器中选择的目标平台。两个属性组会分别命中最终当前构建只会使用其中一个IntermediateOutputPath。这样 x86 和 x64 的中间文件彼此隔离清理其中一个平台的缓存不会影响另一个平台。如果是 SDK 风格项目默认的IntermediateOutputPath已经包含目标框架名例如obj\Debug\net6.0-windows\这是为了避免不同目标框架互相干扰。如果你又加入平台名建议按顺序先配置、再框架、再平台保持路径结构统一。3.3 清理 obj 的三种方式VS2019 的“生成”菜单里有一个“清理解决方案”选项。这个动作会运行MSBuild /t:Clean清掉当前配置生成的输出和中间文件但不会删除目录本身也不会处理其他配置的产物。命令行下更可预期msbuild YourProject.csproj /t:Clean /p:ConfigurationRelease/t:Clean指定执行 Clean 目标/p:ConfigurationRelease决定清理哪一套配置。如果需要彻底删除残留我一般先关闭 VS2019再手动删掉整个 objrmdir /s /q obj手动删除的逻辑很简单但是有效。如果是 PowerShell 环境可以用Remove-Item -Recurse -Force obj。这里要注意VS2019 没有完全退出时某些中间文件会被后台编译器进程锁定删除可能报访问被拒绝具体排错方式见第 4 章。3.4 让 Git 忽略 obj 和 bin大多数 .NET 项目的 .gitignore 至少应该包含bin/和obj/。如果第 3.1 节把 obj 重定向到了解决方案目录下的Intermediate则还要加一条bin/ obj/ Intermediate/注意 Git 的路径规则区分斜杠。obj/会匹配所有目录下名为 obj 的文件夹适合没有重定向的项目。如果修改过BaseIntermediateOutputPath标准规则就不够用了。判断方式很简单用git status看看有没有生成文件被标记为 Untracked如果有说明忽略规则没覆盖到实际输出目录。还有一点.gitignore只影响未跟踪文件。如果之前已经误将 obj 提交到仓库需要在仓库里执行git rm -r --cached obj把它从版本控制中撤掉但保留本地文件。4. 排查 VS2019 下 obj 目录的典型错误和边界4.1 文件被占用无法删除先处理编译器服务器进程最常见的报错是弹窗提示“操作无法完成因为文件已在另一个程序中打开”。obj 目录里的文件被锁一般不是代码编辑器而是 Visual Studio 的 Roslyn 编译器服务器VBCSCompiler.exe。它会在后台驻留为多个项目提供增量编译服务有时也会把上一次编译的临时 dll 留在内存映射中。先尝试关闭 VS2019。如果仍然无法删除检查任务管理器是否还有VBSCompiler.exe或类似进程然后强制结束再删taskkill /f /im VBCSCompiler.exe rd /s /q obj/f是强制终止/im指定映像名称。杀进程后MSBuild 下次启动会自动拉起新的编译器服务器不影响后续编译。对于排障这个操作比重启机器快很多。另外杀毒软件或 OneDrive 这类同步软件也可能锁住 obj。如果你把解决方案放在 OneDrive 目录下建议关闭这些目录的实时同步至少排除obj/和bin/。4.2 路径过长或权限导致的“failed to create directory”错误某些项目路径很长比如D:\Users\name\source\repos\CompanyName\ProductName\trunk\src\Module\SubModule\obj\Debug\net472\。Windows 默认有 260 个字符的路径长度限制MSBuild 在创建中间输出目录时会报出类似error MSB3491: 未能向文件...写入 error : failed to create directory .\obj\...这类错误不一定是权限问题而是路径长度超限。最直接的办法是在项目根目录多开一层目录但不把解决方案放太深或者通过 3.1 节的方式把 obj 重定向到短路径例如C:\Temp\obj\$(MSBuildProjectName)\。如果是 CI 环境目录名由流水线编排工具生成也要检查工作目录是否带时间戳或随机串。遇到权限问题时Windows 事件管理器里的详细错误比较有用但大多数情况下手动用资源管理器进入目标目录试建文件夹就能确认权限是否真的缺失。4.3 obj 与 .obj 模型文件的混淆先从扩展名判断搜索“C# obj 文件”时会出现两种完全不同的东西一个是编译中间目录另一个是 Wavefront OBJ 3D 模型。前者二进制且由 MSBuild 管理后者是纯文本可以在 Blender、Maya 或 3ds Max 中导出。判断方法很简单用记事本打开如果看到v 0.0 1.0 0.0、f 1 2 3这样的内容就是 3D 模型如果是一堆.cache、.resources那就是构建产物。工程项目里的obj目录和模型文件的.obj没有任何关系。如果在 VS2019 里想读取模型文件不要把它放在项目 obj 目录中也不要试图用 MSBuild 的加载逻辑去解析而是当作普通文本文件处理。下一章直接给出一个最小实现。5. 当“obj 文件”指 3D 模型时用 C# 读取 Wavefront OBJ 的最小实现5.1 Wavefront OBJ 格式的关键行Wavefront OBJ 是一种简单、可读的 3D 几何表示方式。每行以关键字开头空格分隔参数v x y z顶点坐标。vn x y z法线。vt u v纹理坐标。f 顶点索引/纹理索引/法线索引面定义。o 对象名对象名称。# 注释以#开头。顶点索引从 1 开始而不是 C# 中习惯的 0。如果需要立即编写一个能用的解析器可以忽略表面、材质和法线只读取顶点坐标用于显示或是导出到其他几何处理库。5.2 一个轻量级 C# OBJ 顶点解析代码下面这段代码在 VS2019 中可以直接编译只提取 OBJ 文件中的顶点坐标using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Numerics; public static class ObjMiniParser { public static ListVector3 ReadVertices(string objPath) { var vertices new ListVector3(); using (var reader new StreamReader(objPath)) { string line; while ((line reader.ReadLine()) ! null) { if (string.IsNullOrWhiteSpace(line) || line[0] #) { continue; } var parts line.Split(new[] { , \t }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length 4 || parts[0] ! v) { continue; } var x float.Parse(parts[1], CultureInfo.InvariantCulture); var y float.Parse(parts[2], CultureInfo.InvariantCulture); var z float.Parse(parts[3], CultureInfo.InvariantCulture); vertices.Add(new Vector3(x, y, z)); } } return vertices; } }StreamReader逐行读取避免把整个模型一次性加载到内存。line[0] #直接跳过注释比先StartsWith更快。Split同时支持空格和 Tab 分隔兼容常见导出工具输出。float.Parse显式指定InvariantCulture防止在中文系统上小数点被解析为逗号。Vector3来自System.Numerics命名空间。如果你的目标框架是 .NET Framework 4.5 以下需要单独引用System.Numerics.VectorNuGet 包如果是在 .NET Core 3.1 或 .NET 5 上直接可用。5.3 在 VS2019 项目中引用的三个注意点第一不要用File.ReadAllLines读取超大 OBJ 文件。几百万顶点时ReadAllLines会同时分配大量短生命周期字符串引发 GC 压力。按行读取是更可靠的做法也可以配合Span和stackalloc优化字符串拆分但可读性会下降。第二解析时关注坐标系。OBJ 文件常用右手坐标Y 轴向上而 WinForms 和 WPF 的屏幕坐标通常 Y 轴向下。显示前需要反转 Y 或做一次矩阵变换否则模型会颠倒。第三文件编码大多数是 ASCII 或 UTF-8 无 BOM。如果碰到带 BOM 的 UTF-8 文件StreamReader默认能正确处理。遇到 ANSI 编码的中文注释时建议把文件名和路径单独处理不要混在解析逻辑里。如果需要加载纹理坐标、法线、材质组建议引入AssimpNet或SharpGL等成熟库。自己做全量解析的成本远高于预期尤其是在处理三角扇、多边形和负索引时。6. 收尾用自定义脚本验证并清理 C# 项目 obj 目录6.1 按最后写入时间清理旧 obj 的 PowerShell 脚本与其一键把所有 obj 删掉不如设置一个保留期只清理超过 7 天没有写入的中间目录。这样既释放空间又不影响活跃项目的增量编译。$root C:\src\MySolution $days 7 $threshold (Get-Date).AddDays(-$days) Get-ChildItem -Path $root -Directory -Recurse -Force | Where-Object { $_.Name -eq obj -and $_.LastWriteTime -lt $threshold } | ForEach-Object { Write-Output $_.FullName }这段逻辑先列目录不执行删除。$root换成你的解决方案根目录$days控制保留天数。Get-ChildItem -Directory -Recurse -Force递归查找所有目录-Force让隐藏目录标签页可见。-lt $threshold比较最后写入时间避免误删最近还在使用的缓存。确认列出的目录无误后把Write-Output换成Remove-Item -Recurse -Force $_.FullName即变成真正清理。6.2 执行清理前的安全性检查第一次运行可能会让人不放心我会先做两步验证。第一在 VS2019 外打开目录确认 obj 内没有自己放进去的源码或备份文件。第二用git status检查是否所有 obj 内容都被忽略如果没有被忽略说明项目的 .gitignore 配置不完整先补规则再清理。另一个有用的技巧是把脚本放在 CI 流程的第一步。在msbuild /t:Rebuild之前运行清理可以避免上一轮构建残留导致的目标文件时间戳误判。相比每次全量 Clean按时间清理能保留最近正在使用的缓存只清除长期未触发的目录节省约一半的首次构建时间。本文还有配套的精品资源点击获取
返回列表