[{"data":1,"prerenderedAt":623},["ShallowReactive",2],{"notes-list":3},[4,83,160,320,474],{"id":5,"title":6,"body":7,"date":69,"description":63,"extension":70,"meta":71,"navigation":72,"path":73,"seo":74,"stem":75,"summary":76,"tags":77,"__hash__":82},"notes\u002Fnotes\u002Fagent-kb-sharing-boundary.md","知识库访问边界：授予只读权限，不授予凭据",{"type":8,"value":9,"toc":62},"minimark",[10,14,18,21,56,59],[11,12,13],"h2",{"id":13},"问题",[15,16,17],"p",{},"多 Agent 业务平台里，知识库分属不同属主，但平台侧的身份（登录用户、Agent 调用方）需要读取其中的条目。最直接的实现是把库的访问凭据下发给每个调用方——这能跑通，但边界就丢了：任何拿到凭据的调用方都等价拥有库的全部权限，越多调用方持有凭据，访问边界越难控制。",[11,19,20],{"id":20},"取舍",[22,23,24,32,38,44],"ul",{},[25,26,27,31],"li",{},[28,29,30],"strong",{},"为指定平台身份授予知识库资源的只读访问权限，但不授予凭据资源权限","：检索由服务端按真实库属主解析后返回，调用方不直接持有库属主凭据。",[25,33,34,37],{},[28,35,36],{},"排除项显式声明","：合成施工记录库明确排除在共享范围外，排除写在边界设计里而不是靠\"不调用\"的默契。",[25,39,40,43],{},[28,41,42],{},"权限收口在服务端一处","：调用方看到的是平台身份的只读接口，属主凭据不离开服务端，吊销与审计都集中在一个边界上。",[25,45,46,49,50,55],{},[28,47,48],{},"写操作另行约束","：读共享之外，幂等键与高危操作审批作为独立设计对齐，不与读共享混为一谈（仍处于设计对齐阶段，见 ",[51,52,54],"a",{"href":53},"\u002Fworks\u002Fsitepulse-agent","智慧工地案例","）。",[11,57,58],{"id":58},"边界",[15,60,61],{},"这套做法的动机是收缩凭据的分发范围、集中审计面，而不是性能优化。不同平台的属主模型不同，但\"访问权限与凭据分开授予\"这一层判断可以复用。",{"title":63,"searchDepth":64,"depth":64,"links":65},"",2,[66,67,68],{"id":13,"depth":64,"text":13},{"id":20,"depth":64,"text":20},{"id":58,"depth":64,"text":58},"2026-10-17","md",{},true,"\u002Fnotes\u002Fagent-kb-sharing-boundary",{"title":6,"description":63},"notes\u002Fagent-kb-sharing-boundary","平台登录用户需要跨属主读取知识库时，边界设计的选择是把访问权限与凭据分开：为指定平台身份授予只读权限，凭据仍由服务端持有。",[78,79,80,81],"Agent","知识库","权限设计","AgentScope","5az2QE96wTEU7gw07N7qsYZeHTjvY9ObPuVxtOW_FuI",{"id":84,"title":85,"body":86,"date":69,"description":63,"extension":70,"meta":148,"navigation":72,"path":149,"seo":150,"stem":151,"summary":152,"tags":153,"__hash__":159},"notes\u002Fnotes\u002Fasset-to-interaction-conventions.md","从资产到交互的接入规范",{"type":8,"value":87,"toc":143},[88,91,94,97,138,140],[11,89,90],{"id":90},"为什么需要接入规范",[15,92,93],{},"三维展示页不少问题出在资产接入：模型朝向不一致、压缩参数随手给、交互命中靠运气。把接入环节固化成规范后，新资产接入从\"每次调一遍\"变成\"按清单过一遍\"。",[11,95,96],{"id":96},"我实际遵守的规则",[22,98,99,105,115,121,132],{},[25,100,101,104],{},[28,102,103],{},"格式只留一种主路","：Blender 导出统一走 glTF，几何用 Draco 压缩、纹理用 KTX2，不把原始 .blend \u002F 高模直接丢给前端。",[25,106,107,110,111,55],{},[28,108,109],{},"坐标约定写进数据而不是靠记忆","：在 Scene 工具链里，发布态显式声明坐标约定，下游不再猜测文件是否已含旋转（见 ",[51,112,114],{"href":113},"\u002Fworks\u002Fscene-toolchain","Scene 工具链",[25,116,117,120],{},[28,118,119],{},"压缩参数按展示目标定","：首屏要快的页面优先减体积，细节页才放精度；两者是不同产物，不是同一个文件调参。",[25,122,123,126,127,131],{},[28,124,125],{},"交互命中独立于渲染网格","：可点区域用 Hitbox 代理而不是直接拾取高精度模型（见 ",[51,128,130],{"href":129},"\u002Fworks\u002Fphotography-interaction","摄影器材交互展示","），命中稳定且与 LOD 解耦。",[25,133,134,137],{},[28,135,136],{},"新资产接入是流程不是手活","：接入周期以\"按清单走一遍\"为目标，清单包括格式检查、朝向验证、压缩产物与命中层四步。",[11,139,58],{"id":58},[15,141,142],{},"这些规则来自摄影器材展示页与 Scene 工具链两类项目的实际做法，不同项目的数据形态不同，清单内容需要按项目调整，但\"约定显式化、接入流程化\"这一层是通用的。",{"title":63,"searchDepth":64,"depth":64,"links":144},[145,146,147],{"id":90,"depth":64,"text":90},{"id":96,"depth":64,"text":96},{"id":58,"depth":64,"text":58},{},"\u002Fnotes\u002Fasset-to-interaction-conventions",{"title":85,"description":63},"notes\u002Fasset-to-interaction-conventions","三维展示页最容易在资产接入环节失控：格式、坐标、压缩与交互命中需要一套固定约定，本文整理我在摄影器材展示与 Scene 工具链中实际遵守的规则。",[154,155,156,157,158],"glTF","Draco","KTX2","3DGS","资产管线","KpYqqv9X9weBCcQzTeBxycgmgwJhIbKMQHk-vdjuYX0",{"id":161,"title":162,"body":163,"date":309,"description":63,"extension":70,"meta":310,"navigation":72,"path":311,"seo":312,"stem":313,"summary":314,"tags":315,"__hash__":319},"notes\u002Fnotes\u002F3dgs-coordinate-system.md","3DGS 坐标系与 180°Z 约定：从 PLY 到引擎空间",{"type":8,"value":164,"toc":300},[165,168,171,174,178,190,193,196,199,220,223,230,233,248,252,263,270,273,276,279,294,297],[11,166,167],{"id":167},"为什么坐标会成为问题",[15,169,170],{},"3DGS 训练工具通常输出 COLMAP 风格的数据：Z 轴向上（Z-up），相机位姿以 world-to-camera 形式给出。浏览器中的渲染引擎则按 Y 轴向上（Y-up）组织场景。两者之间没有统一的“正确”方向，只要某一步少做了一次变换，场景就会表现为倒立、侧躺或镜像，而且很难从画面上一眼看出是哪一步出错。",[15,172,173],{},"我们遇到的问题大多属于这一类：同一份数据在 scene-studio 里看起来正常，经过离线管线后翻转，或者在查看器里碰撞体与模型对不上。",[11,175,177],{"id":176},"ply-文件里隐含的约定","PLY 文件里隐含的约定",[15,179,180,181,185,186,189],{},"PLY 本身不记录坐标系。在 ",[182,183,184],"code",{},"@playcanvas\u002Fsplat-transform"," v3 中，PLY 数据被标记为绕 Z 轴旋转 180° 的变换空间（文档中称为 ",[182,187,188],{},"Transform.PLY","）。也就是说，读入的高斯数据已经包含一层隐含的 180°Z 变换，写出时必须按相同约定还原，否则会整体镜像。",[15,191,192],{},"这个约定是库内部的惯例，并不写在文件头里，因此管线中每一处读写 PLY 的地方都需要明确处理它。",[11,194,195],{"id":195},"我们落实的不变量",[15,197,198],{},"scene-pipeline 的 AGENTS.md 中把坐标规则写成了明确的不变量，核心有三条：",[200,201,202,208,214],"ol",{},[25,203,204,207],{},[28,205,206],{},"document TRS 是引擎空间","：场景文档中记录的平移、旋转与缩放，已经处于引擎坐标系。",[25,209,210,213],{},[28,211,212],{},"源数据的 meta.transform 含 PLY 的 180°Z 惯例","：读入时需要把这层惯例与文档变换合并。",[25,215,216,219],{},[28,217,218],{},"先左乘再烘焙","：TRS 以左乘方式作用于 pending 变换，之后整体烘到 IDENTITY，再交给碰撞与 LOD 消费。",[15,221,222],{},"烘焙之后，下游模块不再关心原始坐标约定。这样碰撞、LOD 与打包三个分支看到的是同一份数据，不会出现某个分支多翻一次的情况。",[15,224,225,226,229],{},"此外还有一条纪律：",[28,227,228],{},"不要在任何地方手工翻转坐标","。每次看到“这里看起来反了，加个负号”的修改，都应当回到变换链路里找到漏掉的那一步。",[11,231,232],{"id":232},"四元数的数组顺序",[15,234,235,236,239,240,243,244,247],{},"管线内部统一使用 ",[182,237,238],{},"[x, y, z, w]"," 的四元数顺序，这与 PlayCanvas 的 ",[182,241,242],{},"Quat"," 原生顺序一致。很多第三方工具使用 ",[182,245,246],{},"[w, x, y, z]","，如果混用，旋转会表现为随机的轴向错误，而且不会报错。我们的做法是在边界处显式转换，并在测试中覆盖一个非对称的三维尖塔场景，用朝向回归来守住这一点。",[11,249,251],{"id":250},"z-up-到-y-up-的轴向校正","Z-up 到 Y-up 的轴向校正",[15,253,254,255,258,259,262],{},"对于 COLMAP 产出的 PLY，常见做法是绕 X 轴旋转 +90°，把 Z-up 变为 Y-up。轴向校正 CLI 把这个角度暴露为 ",[182,256,257],{},"--base-rot-x","（默认 90）与 ",[182,260,261],{},"--flip-updown-x"," 两个参数，实际旋转为两者之和。",[15,264,265,266,269],{},"在 v3 的实现中，库内部会对 PLY 数据做一次坐标空间的共轭补偿，因此用户看到的角度参数与旧版保持同一语义。如果你在日志中看到 ",[182,267,268],{},"Axis rotation (deg)","，那是最终生效的角度，遇到方向不对时以它为准调整。",[15,271,272],{},"同时，v3 的输出列顺序是 canonical 的（位置、旋转、缩放、不透明度、球谐系数），与输入列顺序可能不同。下游应当按属性名读取列，不要硬编码列下标。",[11,274,275],{"id":275},"怎么验证",[15,277,278],{},"我们的做法是用一个非对称的三维模型（比如带尖顶的塔）做回归：",[22,280,281,284,291],{},[25,282,283],{},"它的前后左右与上下都能一眼分辨，任何轴向错误都会被画面暴露。",[25,285,286,287,290],{},"每次修改变换链路后，先跑管线生成产物，再用查看器的 ",[182,288,289],{},"?scene="," 方式加载，比较尖顶的朝向与地面的位置。",[25,292,293],{},"碰撞体与可视模型需要分别检查，因为两者经过的变换分支不同。",[11,295,296],{"id":296},"小结",[15,298,299],{},"3DGS 的坐标问题本质上是“约定没有写在数据里”。解决办法是把约定写成显式的不变量，放在管线的固定位置处理，并用非对称场景做回归。不要依赖肉眼调整角度来掩盖缺失的变换步骤。",{"title":63,"searchDepth":64,"depth":64,"links":301},[302,303,304,305,306,307,308],{"id":167,"depth":64,"text":167},{"id":176,"depth":64,"text":177},{"id":195,"depth":64,"text":195},{"id":232,"depth":64,"text":232},{"id":250,"depth":64,"text":251},{"id":275,"depth":64,"text":275},{"id":296,"depth":64,"text":296},"2026-10-16",{},"\u002Fnotes\u002F3dgs-coordinate-system",{"title":162,"description":63},"notes\u002F3dgs-coordinate-system","同一份高斯点云在重建工具、离线管线与浏览器引擎之间坐标定义不同，本文整理我们在管线中落实的不变量与踩过的坑。",[157,316,317,318],"坐标系","PlayCanvas","管线","ijO0xzACRWxKjQub1Y-8Fmt7NVYowp02s9LOQQLi4cE",{"id":321,"title":322,"body":323,"date":309,"description":63,"extension":70,"meta":463,"navigation":72,"path":464,"seo":465,"stem":466,"summary":467,"tags":468,"__hash__":473},"notes\u002Fnotes\u002Fpkg-url-offline-bridge.md","不 Fork 官方 Viewer：pkg:\u002F\u002F 四层 URL 重映射离线桥",{"type":8,"value":324,"toc":455},[325,328,339,357,361,368,390,404,407,416,420,423,426,429,432,450,452],[11,326,327],{"id":327},"场景",[15,329,330,331,334,335,338],{},"我们希望用户把一个产物目录拖进页面就能直接查看，不需要启动服务器。目录里有 ",[182,332,333],{},"settings.json","、",[182,336,337],{},"lod\u002Flod-meta.json","，以及成百上千个分块文件和体素碰撞数据。",[15,340,341,342,345,346,334,349,352,353,356],{},"官方 supersplat-viewer 在加载时并不认为自己在处理本地文件。它会用 ",[182,343,344],{},"new URL(chunkRelPath, contentUrl)"," 拼出每个分块的地址，然后用 ",[182,347,348],{},"fetch",[182,350,351],{},"XMLHttpRequest"," 或 ",[182,354,355],{},"\u003Cimg>"," 去请求。对于普通的 Blob URL，这些相对子路径无法解析。",[11,358,360],{"id":359},"思路虚拟协议-四层拦截","思路：虚拟协议 + 四层拦截",[15,362,363,364,367],{},"我们注册一个虚拟协议 ",[182,365,366],{},"pkg:\u002F\u002F\u003CrootId>\u002F\u003C相对路径>","，把拖入的目录登记为一个根，并在查看器的生命周期内拦截四类请求入口：",[200,369,370,375,380,385],{},[25,371,372],{},[182,373,374],{},"window.fetch",[25,376,377],{},[182,378,379],{},"XMLHttpRequest.open",[25,381,382],{},[182,383,384],{},"HTMLImageElement.src",[25,386,387],{},[182,388,389],{},"HTMLMediaElement.src",[15,391,392,393,396,397,352,400,403],{},"拦截后，请求的 ",[182,394,395],{},"pkg:\u002F\u002F"," 地址会被映射回内存中对应的 ",[182,398,399],{},"File",[182,401,402],{},"Blob","。官方代码发出的仍然是普通的 URL 请求，因此无需理解这个虚拟协议。",[11,405,406],{"id":406},"为什么是四层",[15,408,409,410,412,413,415],{},"只拦截 ",[182,411,348],{}," 不够：官方的分块图片通过 ",[182,414,355],{}," 加载，体素碰撞数据则可能通过 XHR 读取。每一层都需要单独覆盖，否则会有一部分资源仍然指向无效地址。把四层放在同一个桥模块里统一管理，便于在生命周期结束时全部还原。",[11,417,419],{"id":418},"不-fork-的边界","不 Fork 的边界",[15,421,422],{},"这个方案的前提是官方包以黑盒 npm 依赖的形式引入。我们只使用它公开的入口：创建查看器的函数、查看器句柄、样式文件，以及 settings 的校验与迁移函数。",[15,424,425],{},"为了防止升级时悄悄破坏这些边界，项目中有一个 API 面审计脚本：它扫描源码实际用到的官方符号，生成一份清单。升级官方版本时，只需更新依赖，然后对照类型声明检查清单即可。维护成本基本集中在这份清单上，而不是散落在源码的修改里。",[15,427,428],{},"少数官方不支持的内容（如 GLB 网格、骨骼动画角色与程序化几何体）通过查看器暴露的应用对象挂入场景根，这同样属于公开接口的使用，而不是修改内部实现。",[11,430,431],{"id":431},"副作用与注意事项",[22,433,434,440],{},[25,435,436,439],{},[28,437,438],{},"生命周期要严格","：拦截会改写全局对象，离开页面或卸载查看器时必须还原。否则同一页面中的其他代码会收到意外的请求结果。",[25,441,442,445,446,449],{},[28,443,444],{},"加载揭示特效可关闭","：查看器的加载揭示效果默认开启，可以通过 ",[182,447,448],{},"?noreveal"," 参数关闭，便于调试。",[11,451,296],{"id":296},[15,453,454],{},"当官方组件不假设你的运行环境时，最稳妥的做法通常不是 fork，而是在它依赖的边界上做一层适配。这一层越薄、越有明确的入口与出口，升级成本就越低。",{"title":63,"searchDepth":64,"depth":64,"links":456},[457,458,459,460,461,462],{"id":327,"depth":64,"text":327},{"id":359,"depth":64,"text":360},{"id":406,"depth":64,"text":406},{"id":418,"depth":64,"text":419},{"id":431,"depth":64,"text":431},{"id":296,"depth":64,"text":296},{},"\u002Fnotes\u002Fpkg-url-offline-bridge",{"title":322,"description":63},"notes\u002Fpkg-url-offline-bridge","官方查看器假设资源都通过 HTTP 加载。要在纯静态页面里离线打开本地目录，我们用一个运行时 URL 桥把相对路径请求映射回内存中的文件，全程不修改官方代码。",[469,470,471,472],"前端","离线","查看器","浏览器 API","IZFPEZyxvMQ28nQBiBfba82YtJK2ClO1Mt16yKF5OSY",{"id":475,"title":476,"body":477,"date":309,"description":63,"extension":70,"meta":613,"navigation":72,"path":614,"seo":615,"stem":616,"summary":617,"tags":618,"__hash__":622},"notes\u002Fnotes\u002Fstreamed-sog-lod.md","Streamed SOG LOD：大规模高斯场景的流式加载",{"type":8,"value":478,"toc":605},[479,481,484,491,494,497,534,537,540,550,553,556,563,581,587,590,593,600,602],[11,480,13],{"id":13},[15,482,483],{},"一个中等规模的室外高斯场景，单文件就能达到数百 MB 到数 GB。浏览器不可能一次性下载并解析它，即使下载完成，显存也装不下全部高斯。流式加载的目标是：视角近的区域用高精度数据，远处用粗糙数据，并且只请求当前视野内需要的分块。",[15,485,486,487,490],{},"PlayCanvas 的 Streamed SOG 格式正是为此设计的：它把场景组织为多级 LOD，每一级再切分为若干分块，分块之间通过 ",[182,488,489],{},"lod-meta.json"," 描述关系。查看器根据相机位置决定加载哪些分块。",[11,492,493],{"id":493},"整体流程",[15,495,496],{},"在我们的离线管线中，LOD 生成分为四步：",[200,498,499,509,515,521],{},[25,500,501,504,505,508],{},[28,502,503],{},"准备 LOD0","：直接使用编排层物化好的 ",[182,506,507],{},"baked.ply","，它已经包含文档变换并与碰撞共用。",[25,510,511,514],{},[28,512,513],{},"逐级抽稀","：每一级的目标点数按比例递减，默认比例为 50%，直到点数降到阈值（默认一百万）以下，或达到最大级数。",[25,516,517,520],{},[28,518,519],{},"回读并堆叠","：把所有级别的 PLY 重新读出，堆叠为一个多 LOD 数据源。",[25,522,523,526,527,530,531,533],{},[28,524,525],{},"写出流式包","：调用 ",[182,528,529],{},"writeLodSource","，输出 ",[182,532,489],{}," 与分块目录。",[15,535,536],{},"第二步之前，LOD 转换 CLI 还会对 LOD0 做一次 Morton 空间排序。Morton 曲线把空间上相近的高斯放在文件中相邻的位置，使后续抽稀与分块读取的局部性更好。",[11,538,539],{"id":539},"为什么必须落盘再回读",[15,541,542,543,546,547,549],{},"这里踩过一个关键的坑。",[182,544,545],{},"decimateSource"," 的输出是 stream-once 的：它只能被读取一次。而 ",[182,548,529],{}," 每一级都需要读两遍。如果把抽稀的输出直接链式交给下一步，第二遍读取会得到空数据，或者在不报错的情况下写出残缺的分块。",[15,551,552],{},"因此每一级抽稀之后必须先写成临时 PLY 文件，再回读。这会多占用磁盘，但保证了每一步的输入都可以重复读取。这条规则被写进了 AGENTS.md 的不变量列表中，修改该流程时必须遵守。",[11,554,555],{"id":555},"两种抽稀策略",[15,557,558,559,562],{},"管线提供了 ",[182,560,561],{},"fromBase"," 选项，决定每一级从哪里抽稀：",[22,564,565,571],{},[25,566,567,570],{},[28,568,569],{},"逐级链式","（默认）：第 k 级从第 k-1 级抽稀。实现直观，但每一级的有损误差会叠加，远处的画质可能逐级下降。",[25,572,573,576,577,580],{},[28,574,575],{},"从 LOD0 直接抽稀","：第 k 级从 LOD0 抽稀到 ",[182,578,579],{},"round(count0 · ratio^k)"," 个点。每一级都要从大源重新读取，速度更慢，但画质更稳定。",[15,582,583,584,586],{},"默认仍保持链式，是为了不改变已有产物的行为；需要更稳定远景画质时可以开启 ",[182,585,561],{},"。",[11,588,589],{"id":589},"分块与内存",[15,591,592],{},"导出时的关键参数有两个：分块数量（默认 512）与分块范围（默认 16）。分块数决定了单个文件的大小，分块范围决定了空间划分的粒度。这两个参数对查看器的请求数量与每次请求的数据量都有直接影响，需要结合场景尺寸调整。",[15,594,595,596,599],{},"LOD 转换 CLI 在多机部署时还有一个保护：遇到 GPU 设备丢失（如 ",[182,597,598],{},"DXGI_ERROR_DEVICE_HUNG","）时自动切换到下一张 GPU，全部失败则回退到 CPU。它的输出是 JSONL 事件流，便于 Worker 统计每个阶段的耗时。",[11,601,296],{"id":296},[15,603,604],{},"流式 LOD 的核心不在于算法多复杂，而在于三件事：数据能否重复读取、中间结果能否落盘、参数能否与场景规模对应。把这三件事放在管线的固定位置处理，比事后调参更可靠。",{"title":63,"searchDepth":64,"depth":64,"links":606},[607,608,609,610,611,612],{"id":13,"depth":64,"text":13},{"id":493,"depth":64,"text":493},{"id":539,"depth":64,"text":539},{"id":555,"depth":64,"text":555},{"id":589,"depth":64,"text":589},{"id":296,"depth":64,"text":296},{},"\u002Fnotes\u002Fstreamed-sog-lod",{"title":476,"description":63},"notes\u002Fstreamed-sog-lod","高斯点云动辄上千万个点，无法整体下载。本文介绍我们如何把场景切分为多级 LOD 与分块，并在离线管线中保证生成过程的内存与磁盘可控。",[157,619,620,621,318],"LOD","SOG","流式加载","gYwEusOC-ydUETmWmOmEOa4rQnvz0aufyTGrCCQT7cg",1792474477994]