three.js编辑器开发实战:从渲染循环到撤销重做的完整指南
1. 为什么我不建议你从零手写渲染循环很多人第一次接触 three.js 编辑器开发脑子里蹦出来的第一个念头是我要自己写一套渲染管线。这个想法本身没错但如果你真的从requestAnimationFrame开始一行行搭大概率会在第三天卡在为什么拖拽物体时场景会闪烁这种问题上。我见过太多类似的案例包括我自己早期做的一个小工具当时为了追求所谓的完全可控硬是用原生 WebGL 写了个简易场景管理器结果光是处理相机矩阵和物体拾取的坐标转换就耗掉了整整一周。three.js 的价值不在于它帮你省了多少行代码而在于它把场景图Scene Graph、相机投影、光照模型、材质系统这些概念用一套统一的抽象封装好了。你要做的是一个编辑器不是渲染引擎。编辑器的核心诉求是让用户能直观地操作三维空间中的对象而不是证明我能手写矩阵运算。所以我的建议很明确用 three.js 做渲染底座把精力全部放在编辑器交互逻辑上。具体来说你需要 three.js 帮你解决这几件事场景管理THREE.Scene作为根节点所有物体挂上去就能被渲染相机控制OrbitControls直接给你轨道旋转、平移、缩放射线拾取Raycaster帮你把屏幕坐标转成三维空间中的物体引用变换控制TransformControls提供拖拽手柄省去自己写 gizmo 的功夫这四个模块是编辑器的骨架。剩下的工作比如属性面板、层级树、撤销重做、序列化保存才是你真正要花时间打磨的地方。注意TransformControls和OrbitControls在 three.js 的 examples 目录里不是核心包的一部分。你需要从three/examples/jsm/controls/路径导入别傻乎乎地去 npm 上找单独的包。我见过有人为了减少依赖把这两个控件用原生鼠标事件重写了一遍结果拖拽手柄的旋转轴判断逻辑写了八百行还没搞定。这不是勤奋这是跟自己过不去。2. 编辑器的最小可用骨架长什么样在动手写代码之前先把编辑器的功能边界划清楚。一个简单编辑器到底要支持什么我的定义是能加载模型、能选中物体、能变换物体、能修改材质、能保存场景。这五件事覆盖了 80% 的使用场景剩下的高级功能可以后续迭代。2.1 目录结构与模块划分我习惯把编辑器拆成四个独立模块每个模块只干一件事editor/ core/ SceneManager.js // 场景、相机、渲染器初始化 SelectionManager.js // 射线拾取与选中状态管理 TransformManager.js // 变换控制器封装 ui/ HierarchyPanel.js // 层级树面板 PropertyPanel.js // 属性编辑面板 Toolbar.js // 顶部工具栏 io/ SceneSerializer.js // 场景序列化与反序列化 main.js // 入口组装所有模块这样拆的好处是每个模块可以单独测试。比如你想验证射线拾取是否准确只需要在SelectionManager里写个测试用例不用启动整个编辑器。2.2 初始化场景的三个关键参数SceneManager的初始化看起来简单但有几个参数如果设错了后面会很难受import * as THREE from three; import { OrbitControls } from three/examples/jsm/controls/OrbitControls.js; class SceneManager { constructor(container) { this.scene new THREE.Scene(); this.scene.background new THREE.Color(0x2a2a2a); const { clientWidth, clientHeight } container; this.camera new THREE.PerspectiveCamera( 50, // fov clientWidth / clientHeight, // aspect 0.1, // near 2000 // far ); this.camera.position.set(5, 5, 5); this.renderer new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true // 截图功能需要 }); this.renderer.setSize(clientWidth, clientHeight); this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); this.renderer.shadowMap.enabled true; this.renderer.shadowMap.type THREE.PCFSoftShadowMap; container.appendChild(this.renderer.domElement); this.controls new OrbitControls(this.camera, this.renderer.domElement); this.controls.enableDamping true; this.controls.dampingFactor 0.05; this.setupLights(); this.setupGrid(); } }这里有几个点值得展开说。fov 选 50 而不是 75。75 是很多教程的默认值视野大、看起来开阔但在编辑器场景里75 的透视畸变太严重拖拽物体时距离感会失真。50 更接近人眼观察习惯操作精度更高。pixelRatio 限制在 2 以内。现在很多手机和笔记本的 devicePixelRatio 是 3 甚至 4如果不限制渲染分辨率会飙升到 4K 以上帧率直接掉到个位数。限制在 2 是画质和性能的平衡点。preserveDrawingBuffer 设为 true。这个参数默认是 false开启后会略微降低性能但如果你要做导出截图功能不开这个参数toDataURL()拿到的是一片空白。我当初排查这个问题花了两个小时最后发现是浏览器在渲染完成后清空了缓冲区。2.3 网格与光照的配置细节网格线GridHelper和坐标轴AxesHelper是编辑器的空间参照物没有它们用户根本分不清东南西北setupGrid() { const grid new THREE.GridHelper(20, 20, 0x444444, 0x333333); grid.material.opacity 0.5; grid.material.transparent true; this.scene.add(grid); const axes new THREE.AxesHelper(3); this.scene.add(axes); } setupLights() { const ambient new THREE.AmbientLight(0xffffff, 0.6); this.scene.add(ambient); const directional new THREE.DirectionalLight(0xffffff, 1.2); directional.position.set(10, 20, 10); directional.castShadow true; directional.shadow.mapSize.set(2048, 2048); directional.shadow.camera.left -15; directional.shadow.camera.right 15; directional.shadow.camera.top 15; directional.shadow.camera.bottom -15; this.scene.add(directional); }环境光给 0.6 是为了让背光面不至于全黑方向光给 1.2 是为了让正面有足够的对比度。阴影相机的范围要手动设置默认范围很小物体一多阴影就会消失。提示阴影贴图设为 2048 是画质和显存的折中。如果你在低端设备上跑可以降到 1024肉眼几乎看不出差别但显存占用少一半。3. 射线拾取编辑器交互的命门编辑器的核心交互是点击选中物体这个功能的底层依赖是射线拾取Raycasting。原理不复杂从相机位置发出一条穿过鼠标点击位置的射线检测这条射线与场景中哪些物体相交取最近的那个。但实际写起来坑比想象的多。3.1 坐标转换的完整链路鼠标事件的clientX/clientY是屏幕坐标原点在左上角。而 three.js 的射线拾取需要的是归一化设备坐标NDC原点在屏幕中心范围是 -1 到 1。转换公式如下const rect renderer.domElement.getBoundingClientRect(); const ndcX ((event.clientX - rect.left) / rect.width) * 2 - 1; const ndcY -((event.clientY - rect.top) / rect.height) * 2 1;注意 Y 轴要取负因为屏幕坐标 Y 向下NDC 的 Y 向上。这个符号搞反了点击位置会上下颠倒排查起来很隐蔽。然后设置射线const raycaster new THREE.Raycaster(); raycaster.setFromCamera(new THREE.Vector2(ndcX, ndcY), camera); const intersects raycaster.intersectObjects(scene.children, true);intersectObjects的第二个参数true表示递归检测子物体。如果你只传false那么挂在 Group 下面的 Mesh 就选不中。3.2 为什么你的点击总是选错物体我遇到过三种典型的选错场景每一种的原因都不一样第一种选中了辅助对象。网格线、坐标轴、变换手柄这些辅助对象也会被射线检测到。解决方案是给它们打标记拾取时过滤掉const intersects raycaster .intersectObjects(scene.children, true) .filter(hit !hit.object.userData.isHelper);第二种选中了不可见物体。有些物体你设了visible false但射线检测依然会命中。需要在过滤时加判断.filter(hit hit.object.visible)第三种选中了背面。默认情况下射线会穿透物体检测背面。如果你只想选中正面需要设置材质的side属性为THREE.FrontSide或者在拾取后检查法线方向。3.3 选中高亮的实现方式选中物体后要给它加个高亮效果最直观的做法是加一个包围盒BoxHelperselectObject(object) { if (this.currentHelper) { this.scene.remove(this.currentHelper); this.currentHelper.dispose(); } this.currentHelper new THREE.BoxHelper(object, 0x00ff00); this.scene.add(this.currentHelper); this.selected object; }BoxHelper会自动跟随物体的变换更新不用手动计算包围盒。但要注意切换选中物体时一定要dispose()掉旧的 helper否则会内存泄漏。我见过一个项目跑了一下午后浏览器卡死最后发现是每次点击都 new 一个 BoxHelper 但从不销毁。另一种高亮方式是修改材质的emissive属性但这种方式会污染原始材质撤销选中时还得恢复不如 BoxHelper 干净。4. 变换控制器拖拽手柄的封装与陷阱TransformControls是 three.js 官方提供的变换手柄支持平移、旋转、缩放三种模式。用起来很简单但有几个坑必须提前知道。4.1 模式切换与快捷键绑定import { TransformControls } from three/examples/jsm/controls/TransformControls.js; this.transformControls new TransformControls(this.camera, this.renderer.domElement); this.transformControls.addEventListener(dragging-changed, (event) { this.orbitControls.enabled !event.value; }); this.scene.add(this.transformControls); // 快捷键 window.addEventListener(keydown, (e) { switch (e.key.toLowerCase()) { case w: this.transformControls.setMode(translate); break; case e: this.transformControls.setMode(rotate); break; case r: this.transformControls.setMode(scale); break; } });dragging-changed事件是关键。拖拽手柄时如果不禁用 OrbitControls相机会跟着一起转操作体验极差。这个事件在拖拽开始时触发value true结束时value false。4.2 变换手柄的层级问题TransformControls本身是一个Object3D它需要被添加到场景中才能渲染。但如果你把它加到场景根节点它会出现在层级树面板里用户可能会误选中它。我的做法是把它加到一个独立的helperScene里或者给它打上userData.isHelper true标记在层级树渲染时过滤掉。4.3 吸附与步进编辑器里经常需要精确移动比如每次移动 0.5 个单位。TransformControls支持吸附设置this.transformControls.setTranslationSnap(0.5); this.transformControls.setRotationSnap(THREE.MathUtils.degToRad(15)); this.transformControls.setScaleSnap(0.1);按住 Shift 键时吸附生效松开则自由变换。这个交互逻辑和主流三维软件一致用户上手成本低。注意吸附值不要设得太小比如 0.01那样和自由变换没区别用户感觉不到吸附的存在。0.5 或 1 是比较合适的平移步进。5. 层级树与属性面板的联动编辑器的左侧通常是层级树右侧是属性面板。这两个面板的数据源都是场景图但更新时机不同。5.1 层级树的递归渲染层级树本质上是场景图的树形展示。递归遍历scene.children为每个节点生成一个列表项function buildTree(object, depth 0) { if (object.userData.isHelper) return null; const node { id: object.uuid, name: object.name || object.type, children: [] }; object.children.forEach(child { const childNode buildTree(child, depth 1); if (childNode) node.children.push(childNode); }); return node; }每次场景变化时重建整棵树是最简单的做法但性能差。更好的方式是监听scene的childAdded和childRemoved事件做增量更新。不过对于简单编辑器来说场景里通常不超过几百个物体全量重建完全够用。5.2 属性面板的双向绑定属性面板要显示选中物体的位置、旋转、缩放、材质颜色等信息。难点在于双向绑定面板改数值要同步到物体物体被拖拽后数值也要刷新到面板。我的做法是面板输入框的change事件触发时更新物体属性在渲染循环里检查选中物体的变换是否变化如果变化则刷新面板// 渲染循环中 const pos this.selected.position; if (!pos.equals(this.lastPosition)) { this.propertyPanel.updatePosition(pos); this.lastPosition.copy(pos); }用equals比较而不是直接比较引用因为 three.js 的 Vector3 是可变对象拖拽时引用不变但值变了。5.3 材质编辑的实时预览修改材质颜色时如果每次都重建材质性能开销很大。正确做法是直接修改现有材质的属性onColorChange(hex) { if (this.selected this.selected.material) { this.selected.material.color.set(hex); this.selected.material.needsUpdate true; } }needsUpdate在修改颜色时其实不需要设但修改贴图、透明度等属性时必须设否则 GPU 不会重新编译着色器。6. 场景序列化保存与加载的完整方案编辑器的最终产出是一个场景文件用户下次打开时能恢复之前的状态。three.js 提供了Object3D.toJSON()方法但直接用它有几个问题。6.1 toJSON 的局限性toJSON()会序列化物体的几何体、材质、变换等信息但不会保存自定义的userData外部加载的贴图引用只保存 URL编辑器的特定状态比如当前选中的物体所以我的做法是在toJSON的基础上做一层包装serialize() { const sceneData this.scene.toJSON(); return { version: 1.0, timestamp: Date.now(), scene: sceneData, editorState: { cameraPosition: this.camera.position.toArray(), cameraTarget: this.orbitControls.target.toArray() } }; }6.2 加载时的对象重建ObjectLoader可以反序列化toJSON的结果但要注意异步问题import { ObjectLoader } from three; async load(data) { const loader new ObjectLoader(); const scene await loader.parseAsync(data.scene); // 清空当前场景 this.scene.clear(); this.scene.add(...scene.children); // 恢复相机 this.camera.position.fromArray(data.editorState.cameraPosition); this.orbitControls.target.fromArray(data.editorState.cameraTarget); }parseAsync是必须的因为贴图加载是异步的。如果用同步的parse贴图会丢失。6.3 外部资源的路径处理如果场景里引用了外部模型或贴图序列化时保存的是相对路径。加载时如果路径变了资源就找不到。解决方案是在保存时把资源转成 base64 内嵌或者维护一个资源映射表。对于简单编辑器我建议直接内嵌 base64。虽然文件会变大但胜在可靠。一个 1MB 的贴图转成 base64 后大约 1.3MB对于场景文件来说可以接受。7. 撤销重做编辑器的灵魂功能没有撤销重做的编辑器是不完整的。用户拖错一个物体发现回不去体验直接崩盘。7.1 命令模式的设计撤销重做的经典实现是命令模式。每个操作封装成一个命令对象包含execute和undo两个方法class TransformCommand { constructor(object, oldPosition, newPosition) { this.object object; this.oldPosition oldPosition.clone(); this.newPosition newPosition.clone(); } execute() { this.object.position.copy(this.newPosition); } undo() { this.object.position.copy(this.oldPosition); } }维护两个栈undoStack和redoStack。执行新命令时压入undoStack并清空redoStack撤销时从undoStack弹出执行undo压入redoStack。7.2 拖拽操作的命令合并拖拽物体时TransformControls会连续触发objectChange事件。如果每次变化都生成一个命令撤销一次只回退一帧用户要按几百次才能回到起点。正确做法是只在拖拽开始时记录初始状态拖拽结束时生成一个命令this.transformControls.addEventListener(mouseDown, () { this.dragStartPosition this.selected.position.clone(); }); this.transformControls.addEventListener(mouseUp, () { const command new TransformCommand( this.selected, this.dragStartPosition, this.selected.position ); this.history.push(command); });这样一次拖拽对应一个撤销步骤符合用户直觉。7.3 历史栈的容量控制历史栈不能无限增长否则内存会爆。设一个上限比如 50 步超过时丢弃最早的命令push(command) { this.undoStack.push(command); if (this.undoStack.length 50) { this.undoStack.shift(); } this.redoStack []; }50 步对于大多数编辑场景够用了。用户如果连续操作超过 50 次还没保存那说明工作流本身有问题。8. 性能优化让编辑器跑满 60 帧编辑器卡顿是最劝退的体验。我总结了几条实战中验证有效的优化策略。8.1 按需渲染默认情况下three.js 每帧都在渲染哪怕场景没有任何变化。编辑器的场景通常是静态的只有用户操作时才需要重绘。改成按需渲染let needsRender true; function animate() { requestAnimationFrame(animate); orbitControls.update(); if (needsRender) { renderer.render(scene, camera); needsRender false; } } // 任何会改变画面的操作后 function invalidate() { needsRender true; }在OrbitControls的change事件、TransformControls的objectChange事件、属性面板修改后调用invalidate()。这样静止时 GPU 占用几乎为零笔记本风扇不会狂转。8.2 几何体合并如果场景里有大量重复的小物体比如一百个相同的立方体每个物体单独渲染会产生一百次 draw call。用InstancedMesh合并const geometry new THREE.BoxGeometry(1, 1, 1); const material new THREE.MeshStandardMaterial({ color: 0x44aa88 }); const instancedMesh new THREE.InstancedMesh(geometry, material, 100); const matrix new THREE.Matrix4(); for (let i 0; i 100; i) { matrix.setPosition(Math.random() * 10, 0, Math.random() * 10); instancedMesh.setMatrixAt(i, matrix); } scene.add(instancedMesh);但要注意InstancedMesh的每个实例不能单独选中和变换所以只适合做背景装饰不适合做可编辑物体。8.3 材质复用每次new THREE.MeshStandardMaterial()都会创建一个新的着色器程序。如果一百个物体用一百个材质GPU 要编译一百次。正确做法是复用材质const sharedMaterial new THREE.MeshStandardMaterial({ color: 0xffffff }); for (let i 0; i 100; i) { const mesh new THREE.Mesh(geometry, sharedMaterial); scene.add(mesh); }但复用的代价是修改一个物体的颜色会影响到所有共用该材质的物体。所以编辑器里通常是默认共享修改时克隆function onColorChange(object, color) { if (object.material.userData.shared) { object.material object.material.clone(); object.material.userData.shared false; } object.material.color.set(color); }8.4 阴影的代价阴影是性能杀手。每盏投射阴影的光源都会触发一次额外的渲染 pass。如果场景里有多盏方向光帧率会断崖式下跌。我的建议是编辑器里只保留一盏投射阴影的方向光其他光源不投射阴影。视觉上几乎看不出差别但性能提升明显。9. 那些文档里不会写的踩坑记录最后分享几个我在实际开发中踩过的坑每一个都花了不少时间排查。坑一OrbitControls 的 target 被意外修改。当选中物体后如果把orbitControls.target设为物体位置相机会自动看向物体。但用户旋转视角后target 会停留在物体上导致后续操作视角很别扭。解决方案是只在双击物体时设置 target单击选中不改变视角。坑二TransformControls 与射线拾取的冲突。拖拽手柄时鼠标事件会被 TransformControls 捕获但射线拾取依然在运行导致拖拽过程中选中状态频繁切换。解决方案是在dragging-changed事件里禁用射线拾取。坑三窗口 resize 后相机宽高比没更新。这是最常见的问题但很多人只更新了 renderer 的 size忘了更新相机的 aspectwindow.addEventListener(resize, () { const { clientWidth, clientHeight } container; camera.aspect clientWidth / clientHeight; camera.updateProjectionMatrix(); // 这行不能忘 renderer.setSize(clientWidth, clientHeight); });updateProjectionMatrix()不调用的话相机的投影矩阵还是旧的画面会被拉伸。坑四dispose 的遗漏。切换选中物体时旧的 BoxHelper 要 dispose删除物体时几何体和材质要 dispose关闭编辑器时renderer 要 dispose。不 dispose 的话显存会持续增长最终导致页面崩溃。我写了一个disposeObject工具函数递归清理所有资源function disposeObject(object) { if (object.geometry) object.geometry.dispose(); if (object.material) { const materials Array.isArray(object.material) ? object.material : [object.material]; materials.forEach(m { Object.values(m).forEach(v { if (v v.isTexture) v.dispose(); }); m.dispose(); }); } object.children.forEach(disposeObject); }这个函数在删除物体和清空场景时都要调用。坑五键盘快捷键与输入框冲突。用户正在属性面板的输入框里打字按下 W 键想输入字母结果触发了切换到平移模式。解决方案是判断当前焦点元素window.addEventListener(keydown, (e) { if (e.target.tagName INPUT || e.target.tagName TEXTAREA) return; // 处理快捷键 });这个判断要放在最前面否则所有快捷键都会在输入时误触发。坑六模型加载的坐标系差异。不同来源的模型可能使用不同的坐标系Y轴向上或Z轴向上。加载后如果发现模型躺倒了不要急着旋转先检查模型的up属性。有些格式如某些CAD导出默认Z轴向上需要手动调整。10. 后续可以扩展的方向这个编辑器骨架跑通之后有几个方向可以继续深挖。一是多选与成组支持框选多个物体并打组这需要扩展 SelectionManager 支持集合操作。二是材质节点编辑用可视化节点图替代简单的颜色选择器这需要引入节点编辑器库。三是协同编辑多人同时编辑一个场景这涉及操作变换OT或冲突无关复制数据类型CRDT的算法。四是模型导入导出支持 glTF、OBJ、FBX 等格式的互转。每个方向都可以独立展开成一个大话题但前提是先把基础骨架搭稳。我个人的经验是不要一开始就追求功能全面先把选中-变换-保存这条主链路跑通剩下的功能都是在这条链路上做加法。主链路不稳加再多功能也是空中楼阁。

相关新闻

AI Agent工程化落地:记忆管理、可观测性与安全实践

AI Agent工程化落地:记忆管理、可观测性与安全实践

今天是2026年10月7日,我把过去两天盯着的AI应用和Agent方向的信息,整理成了一份可以拿出来分享的行业日报。做这个领域越久,越觉得“看动态”和“看门道”是两回事:同一个新闻,有人看到的是产品发布,有人看…

2026/10/12 10:01:51 阅读更多 →
信创环境下用Java实现PDF转OFD的完整方案与避坑指南

信创环境下用Java实现PDF转OFD的完整方案与避坑指南

简介:面向Java开发者和国产化适配工程师的资源,围绕OFD这一国标版式文档格式,系统展示在Java环境下将PDF转换为OFD的完整实现思路,涵盖PDF解析、页面结构还原、字体与图片处理、OFD标准封装等关键环节,适合正在开展信创…

2026/10/12 10:01:51 阅读更多 →
Windows 安装 pycryptodome 报 Microsoft Visual C++ 14.0 错误?离线包安装与静默部署指南

Windows 安装 pycryptodome 报 Microsoft Visual C++ 14.0 错误?离线包安装与静默部署指南

简介:这份资源是面向Python开发者与C初学者的Visual C Build Tools 2015离线安装包,专门解决安装pycryptodome等依赖C语言扩展的库时提示缺少Visual C 14.0环境、在线安装又因网络或兼容性问题反复失败的问题。资源包内共1个docx文档,大小约7…

2026/10/12 10:01:51 阅读更多 →

最新新闻

Spring @Required 注解废弃解析:依赖注入校验演进与构造器注入迁移

Spring @Required 注解废弃解析:依赖注入校验演进与构造器注入迁移

1. 一个被遗忘的注解,为什么还值得聊Required这个注解,如果你是从 Spring Boot 时代入行的,大概率没见过它,甚至在 IDE 里敲出来会发现它上面压着一道醒目的删除线。它属于那种"存在于文档里、消失在代码里"的元老级 AP…

2026/10/12 10:43:37 阅读更多 →
C++实战:课程安排中的约束建模与搜索优化

C++实战:课程安排中的约束建模与搜索优化

1. 从"排课"这个老问题说起:为什么值得用C认真做一遍课程安排这件事,看起来是个教务处的活儿,但真正动手写过的人都知道,它本质上是一个带约束的组合优化问题。每学期开始前,教务老师面对的是几十个班级、上…

2026/10/12 10:43:37 阅读更多 →
电磁场与电磁波教案:时变场与麦克斯韦方程组的课堂推导

电磁场与电磁波教案:时变场与麦克斯韦方程组的课堂推导

简介:面向电子信息类专业《电磁场与电磁波》课程的教案文档,适合高校教师备课、课后复盘及学生自学,内容覆盖法拉第电磁感应定律、位移电流、麦克斯韦方程组及不同介质交界面边界条件等核心知识点,强调积分形式与微分形式的对应转…

2026/10/12 10:43:37 阅读更多 →
Ubuntu 20.04 LTS 服务器安装与初始化配置全攻略

Ubuntu 20.04 LTS 服务器安装与初始化配置全攻略

简介:《Ubuntu 20.04 LTS操作系统安装及初始化配置指南》是一份面向Linux初学者、希望摆脱单一Windows/macOS环境的技术爱好者及开发者的实操文档,内容覆盖从镜像下载、Rufus启动盘制作到BIOS/UEFI启动项调整的完整前置流程,并详细讲解安装引…

2026/10/12 10:43:37 阅读更多 →
[特殊字符]Cursor降智别慌!WindSurf零代码开发MCP Server,五分钟给Claude和AutoGen挂载LightRAG知识库(附TaoToken统一Key配置)

[特殊字符]Cursor降智别慌!WindSurf零代码开发MCP Server,五分钟给Claude和AutoGen挂载LightRAG知识库(附TaoToken统一Key配置)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 10:43:37 阅读更多 →
从 Ubuntu 25.10 到 26.04 LTS:一次跨版本升级的完整技术路线图与 TaoToken 配置实践

从 Ubuntu 25.10 到 26.04 LTS:一次跨版本升级的完整技术路线图与 TaoToken 配置实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 10:42:36 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天,画面可以做到绝对的锐利、平滑与无瑕。然而,当一张秋日手账插画或拍立得照片过于“平整无瑕”时,往往会散发出一种冰冷生硬的“数码塑料感(Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中,横排(Horizontal Layout)早已经成为了绝对的主流。然而,当我们翻开泛黄的线装古籍、宋版木刻诗集,或是欣赏一张茶道雅集的手写便签时,那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点,很多人心里都会悄悄亮起一盏警示灯。 在心理学上,这种现象有一个专门的称谓——“周日夜晚焦虑症(Sunday Scaries)”。明天又是周一,闹钟又要重新在七点响彻卧房;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:54 阅读更多 →