ARTICLE DETAIL

资讯详情

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

openFrameworks 路径转网格实战:用字体轮廓构建可交互的 3D 挤压文字

openFrameworks 路径转网格实战:用字体轮廓构建可交互的 3D 挤压文字 图形学音视频【免费下载链接】openFrameworksopenFrameworks is a community-developed cross platform toolkit for creative coding in C.项目地址https://gitcode.com/gh_mirrors/op/openFrameworks点击查看免费下载本指南以 openFrameworks 仓库中的 pathsToMeshesExample 为骨架完整讲解「如何把字体生成的 2D 路径Path转化为带挤压厚度的 3D 网格Mesh」这一经典创作管线。你将学会加载字体、用getStringAsPoints提取路径、用getOutline获取轮廓折线、手工构建三角面、合并多个网格并配合ofEasyCam、ofLight与ofMaterial渲染出可实时交互的立体文字场景。示例运行效果与学习目标启动示例后屏幕上会出现由玫红色材质渲染、带挤压厚度的 3D 文字openframeworks每个字符都包围着半透明的绿色线框包围盒顶部还有一层淡绿色的文字轮廓与填充层作为参考视图。窗口左上角会实时显示当前输入的字符串与挤压量Extrusion amount (up / down): 100。该示例的完整运行效果见 pathsToMeshesExample.png其源码位于 src/ofApp.cpp、src/ofApp.h 与 src/main.cpp。README 中列出的学习目标如下加载一个字体ofTrueTypeFont从传给字体的字符串中获取路径Path、折线Polyline与网格Mesh使用折线构建挤压Extrude网格计算网格的包围盒Bounds与质心Centroid让网格顶点相对其质心进行偏移。需要特别留意的 API 与手法包括font.getStringAsPoints(...)基于字符串从字体得到vectorofPathpath.getOutline()从路径得到vectorofPolylinepmesh.addIndex(...)手工装配三角形索引compedMesh.append(pmesh)把多个网格合并进同一个网格ofEnableDepthTest()确保按 z 深度而非绘制顺序渲染node.setOrientation( glm::angleAxis(ofDegToRad(spin), axis) )旋转ofNode使用ofEasyCam的cam.begin()/cam.end()渲染整个场景。字体路径管线从字符串到 ofPath核心入口是ofTrueTypeFont::getStringAsPoints。在示例的setup()中字体以较大字号加载随后直接把字符串交给该函数font.load( frabk.ttf, 164, true, true, true ); message openframeworks; stringToMeshNodes( message, 100.0 );ofTrueTypeFont::load的 5 个参数依次为字体文件、字号164 磅、_bAntiAliased抗锯齿、_bFullCharacterSet加载完整字符集与makeContours生成轮廓数据。示例使用的字体文件位于 bin/data/frabk.ttf。getStringAsPoints的声明见 ofTrueTypeFont.hofPath getCharacterAsPoints(uint32_t character, bool vflip true, bool filled true) const; std::vectorofPath getStringAsPoints(const std::string str, bool vflip true, bool filled true) const;在其实现ofTrueTypeFont.cpp中可以看到函数先检查字体是否成功加载否则打印getStringAsPoints(): font not allocated错误并返回空数组再通过iterateString逐字符调用getCharacterAsPoints并把每个字符按排版位置translate到对应坐标。也就是说返回的每个ofPath都已经带上了正确的前进量advance与字距kerning偏移可以直接用于构建网格。示例中调用了两次区别仅在第三个参数filled// 填充模式有网格tessellation无轮廓 vectorofPath fontPaths font.getStringAsPoints(astring, true, true); // 非填充模式只有轮廓 vectorofPath fontPathsOutlined font.getStringAsPoints(astring, true, false);vflip参数控制是否翻转 y 轴使字形坐标与屏幕/相机坐标对齐示例随后又手动对顶点做了pv.y -pv.y;翻转确保文字正面朝向相机。ofPath 的两张面孔getTessellation 与 getOutlineofPath内部同时维护两条数据通路见 ofPath.hconst ofMesh getTessellation() const返回经三角化tessellation后的填充网格const std::vectorofPolyline getOutline() const返回轮廓折线集合。实现细节见 ofPath.cppgetOutline在绕线模式winding mode不是OF_POLY_WINDING_ODD时会先触发tessellate()再从缓存返回getTessellation则总是惰性触发tessellate()并返回cachedTessellation。这意味两次调用共享同一次三角化结果代价可控。示例中分别用这两条通路构建网格的不同部分// 前后面使用填充网格 ofMesh pathMesh path.getTessellation(); // 侧面使用轮廓折线 auto outlines path.getOutline();getOutline返回的ofPolyline是构建侧面侧壁的关键素材——折线的每个采样点都可以延伸出一个沿 z 方向的矩形面片把前后两个面连接成封闭的立体字形。手工构建挤压网格正面、背面与侧面stringToMeshNodes是示例的核心函数它为每个字符构建一个MeshNode定义见 ofApp.h内部持有mesh合并后的总网格、srcMeshes正面、offsetMeshes背面、sideMeshes侧面、polylines以及ofNode node和顶点包围盒min/max。正面Front Face正面直接来自填充网格的三角化结果。字体网格本身没有法线而正面的法线方向已知统一朝 z因此可以批量赋值ofMesh pathMesh path.getTessellation(); auto pathVerts pathMesh.getVertices(); for( auto pv : pathVerts ) { pv.y -pv.y; // 翻转 y对齐相机朝上方向 } vectorglm::vec3 meshNormals; meshNormals.assign( pathMesh.getNumVertices(), glm::vec3(0,0,1) ); meshNode.srcMeshes.push_back(pathMesh); meshNode.srcMeshes.back().addNormals( meshNormals ); compedMesh.append(meshNode.srcMeshes.back());背面Back Face背面是正面的副本法线反转指向 -z并把所有顶点沿 z 负方向平移extrudeAmount从而形成厚度ofMesh offsetMesh pathMesh; // 深拷贝正面 meshNormals.assign( offsetMesh.getNumVertices(), glm::vec3(0,0,-1) ); offsetMesh.addNormals( meshNormals ); auto verts offsetMesh.getVertices(); for( auto v : verts ) { v.z - extrudeAmount; } meshNode.offsetMeshes.push_back(offsetMesh); compedMesh.append(offsetMesh);侧面Side Faces侧面是手工装配的三角带遍历轮廓折线的每个点把该点与其沿 -z 偏移extrudeAmount的副本作为一条「竖边」的两个顶点相邻两条竖边构成一个四边形拆成两个三角形后用addIndex连接pmesh.setMode(OF_PRIMITIVE_TRIANGLES); for( int k 0; k outline.size(); k ) { auto pvert outline[k]; pvert.y -pvert.y; pmesh.addVertex( pvert ); // 前侧顶点 pmesh.addVertex( pvert - glm::vec3(0,0,extrudeAmount) ); // 后侧顶点 auto normal outline.getNormalAtIndex(k); // 折线法线即侧壁朝向 normal.y * -1.f; pmesh.addNormal( normal ); pmesh.addNormal( normal ); if( k outline.size()-1 ) { pmesh.addIndex(k*21); pmesh.addIndex(k*20); pmesh.addIndex(k*22); pmesh.addIndex(k*23); pmesh.addIndex(k*21); pmesh.addIndex(k*22); } else { // 最后一个点闭合回第一个点 pmesh.addIndex(k*21); pmesh.addIndex(k*20); pmesh.addIndex(0); pmesh.addIndex(1); pmesh.addIndex(k*21); pmesh.addIndex(0); } } if( pmesh.getNumVertices() 0 ) { meshNode.sideMeshes.push_back(pmesh); compedMesh.append(pmesh); } meshNode.polylines.push_back( outline );这里的索引数学值得展开第k个采样点对应顶点k*2前侧与k*21后侧。四边形由(k*2, k*21, k*22, k*23)四个顶点构成拆成三角形(k*21, k*2, k*22)与(k*23, k*21, k*22)当k是最后一个点时把k*22替换为顶点 0、k*23替换为顶点 1完成轮廓闭合。outline.getNormalAtIndex(k)见 ofPolyline.h提供折线在该点的法线作为侧壁的朝向保证光照下侧壁明暗正确。append会把源网格的顶点、法线、索引等整体追加到目标网格末尾见 ofMesh.h因此正面、背面与所有侧面可以依次合并进同一个compedMesh最终每个字符只提交一次draw()。质心计算与顶点相对化ofMesh::getCentroid()见 ofMesh.h返回所有顶点的平均位置。示例用它做了两件事把每个字符网格的质心作为对应ofNode的位置meshNode.node.setPosition( meshNode.mesh.getCentroid() );求出整行文字所有字符质心的平均值meshCentroid然后把每个网格的所有顶点正面、背面、侧面、折线都减去各自节点的位置使顶点相对质心居中for( auto vert : meshVerts ) { vert vert - meshNode.node.getPosition(); } ... meshNode.node.setPosition( meshNode.node.getPosition() - meshCentroid );这一步是关键的设计决策顶点相对质心居中后每个字符就可以通过其ofNode独立旋转、平移而无需重新计算顶点坐标。这正是update()中每个字符能各自绕轴旋转的前提见 ofApp.cppfloat elapsedTime ofGetElapsedTimef(); glm::vec3 axis(0,-1, 0); if( ofGetFrameNum() % 200 100 ) { axis glm::vec3(1,0,0); // 每 200 帧切换一次旋转轴 } for( int i 0; i meshNodes.size(); i ) { float sine sinf( (float)i elapsedTime ); float spin ofMap(sine, 0.4, 0.6, 0, 360, true ); meshNodes[i].node.setOrientation( glm::angleAxis(ofDegToRad(spin), axis )); }ofMap把正弦值从区间[0.4, 0.6]映射到[0, 360]true表示 clamp产生平滑往返的旋转动画glm::angleAxis构建绕指定轴旋转spin度的四元数通过setOrientation应用到ofNode。包围盒的遍历计算同样在此完成先用极大值初始化min/max再对每个顶点逐轴比较更新calculateVertexBounds见 ofApp.h。drawBounds见 ofApp.cpp则用OF_PRIMITIVE_LINES模式从 8 个角点各画 3 条棱拼出线框包围盒用于可视化每个字符的局部边界与整行文字的全局边界。渲染设置深度测试、光照、材质与相机draw()中展示了固定管线GL 2.1见下文下 3D 网格渲染的标准组合ofBackgroundGradient(ofColor(10), ofColor(50)); // 渐变背景 ofEnableDepthTest(); // 按 z 深度排序渲染 camera.begin(); // ofEasyCam 视角 // ... 绘制全局包围盒先平移到 -meshCentroid... ofEnableLighting(); material.begin(); for( auto meshNode : meshNodes ) { meshNode.node.transformGL(); // 应用节点变换 meshNode.mesh.draw(); meshNode.node.restoreTransformGL(); } material.end(); ofDisableLighting(); // ... 绘制每个字符的线框包围盒、轮廓与分层展示 ... camera.end(); ofDisableDepthTest();ofEnableDepthTest()让片段按 z 深度而非绘制顺序遮蔽是透明/半透明叠加层与实心网格共存时的必要开关。灯光在setup()中配置为点光源setPointLight并设置环境色、漫反射与高光色材质为玫红色RGB 247,59,103setShininess(100)提供高反射质感light.setup(); light.enable(); light.setPointLight(); light.setAmbientColor(ofFloatColor(0.6)); light.setDiffuseColor(ofFloatColor(1,1,1)); light.setSpecularColor(ofFloatColor(1,1,1)); light.setPosition( -50, -300, 600 ); material.setAmbientColor(ofColor(247,59,103)); material.setDiffuseColor(ofColor(247,59,103)); material.setSpecularColor(ofFloatColor(1.0,1.0)); material.setShininess(100);相机使用ofEasyCam在setup()中设定观察距离并做一次轨道旋转之后即可用鼠标拖拽、滚轮缩放视角camera.setDistance(1100); camera.orbitDeg(0, -30, 1100);交互与网格实时重建keyPressed见 ofApp.cpp实现了完整的实时编辑逻辑任意输入都会触发stringToMeshNodes重建网格普通字符key 300追加到messageOF_KEY_DEL或退格ASCII 8删除最后一个字符OF_KEY_RETURNASCII 13插入换行支持多行文字OF_KEY_UP/OF_KEY_DOWN每次将extrusion增减 10且不小于 0用于调节挤压厚度。} else if( key OF_KEY_UP ) { extrusion 10; bRebuildMeshestrue; } else if( key OF_KEY_DOWN ) { extrusion - 10; if( extrusion 0 ) { extrusion 0; } bRebuildMeshestrue; } ... if( bRebuildMeshes ) { stringToMeshNodes(message, extrusion); }由于重建逻辑全部集中在stringToMeshNodes整个流程——重新提取路径、重算质心、重算包围盒、顶点相对化——都是幂等的输入变化后场景可无缝更新。示例默认挤压量为extrusion 100见 ofApp.h。运行前提与窗口配置该示例的入口 main.cpp 使用 GLFW 窗口并默认运行在 OpenGL 2.1 固定管线ofGLWindowSettings settings; settings.setSize(1200, 768); settings.windowMode OF_WINDOW; // 也可设为 OF_FULLSCREEN settings.setGLVersion(2, 1); // 固定管线 //settings.setGLVersion(3, 2); // 可切换为可编程管线若改为setGLVersion(3, 2)需同步把material/ofLight的固定管线光照逻辑迁移为 shader 方案。运行前请确认字体文件frabk.ttf位于应用的bin/data目录下示例已自带否则font.load失败会导致getStringAsPoints返回空路径。涉及的核心类与延伸阅读本示例集中使用了以下 openFrameworks 类其声明均可在仓库源码中进一步研读ofTrueTypeFont字体加载与字符路径提取ofTrueTypeFont.h、ofTrueTypeFont.cppofPath/ofPolyline路径与折线数据ofPath.h、ofPolyline.hofMesh顶点/法线/索引容器与质心计算、append合并ofMesh.hofNode每个字符独立的变换节点ofEasyCam、ofLight、ofMaterial场景相机与固定管线光照材质。如果希望更系统地掌握网格顶点与索引的底层用法可参考 ofMesh 相关文档 中的索引装配注释以及仓库内其他 3D 示例如 3DPrimitivesExample 的基础图元网格、pointCloudExample 的顶点批量操作。理解「字体 → 路径 → 折线 → 网格」这条数据链后你还可以把同样的方法延伸到 SVG 路径解析、自定义矢量图形拉伸为立体模型等创作场景。赞分享图形学音视频【免费下载链接】openFrameworksopenFrameworks is a community-developed cross platform toolkit for creative coding in C.项目地址https://gitcode.com/gh_mirrors/op/openFrameworks点击查看免费下载相关推荐meetingsdk-web-sample特殊环境部署指南中国区CDN加速与Zoom for Government配置详解meetingsdk web sample特殊环境部署指南中国区CDN加速与Zoom for Government配置详解 meetingsdk web sathree.js Shape 类详解从 2D 轮廓定义到挤出与平面网格的完整路径three.js Shape 类详解从 2D 轮廓定义到挤出与平面网格的完整路径 本文基于 three.js 官方文档 Shape https://link.前端3D渲染图形学three.js ShapePath 深度指南把路径/字形轮廓转换为可渲染 Shape 的核心工具three.js ShapePath 深度指南把路径/字形轮廓转换为可渲染 Shape 的核心工具 ShapePath 是 three.js 中负责“把一系列前端3D渲染图形学上一篇oh-my-opencode社区与支持如何参与开源贡献和获取帮助下一篇Unity运动匹配技术完全指南打造流畅角色动画的终极方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表