十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

纯前端实现:用ASCII字符渲染3D赛博朋克城市

纯前端实现:用ASCII字符渲染3D赛博朋克城市 这次我们来看一个很有意思的前端作品用一个 HTML 文件在浏览器里渲染出一座完整的 3D 赛博朋克城市。整座城市没有任何外部 3D 模型文件不需要下载贴图也不依赖重型 3D 库而是直接用 ASCII 字符一格格画出来的。这类创意项目通常会在演示标题里写“只用一个 HTML”背后其实暗含一个非常硬核的前提所有建筑、道路、天空、灯光效果全部靠浏览器端 JS 计算生成。对前端开发来说这个作品最大的价值不是画面有多精美而是它把 3D 投影、深度排序、字符亮度映射、程序化生成这几件事全部压缩在一个页面里。把这套逻辑拆开看你会发现它其实很适合用来理解浏览器端 3D 渲染的底层套路。即使你平时不碰 Three.js也能从这种“硬算投影”的写法里学到不少东西。这篇文章我会按以下几个部分展开先拆解 ASCII 3D 城市的实现原理再给出一套可以直接运行的最小原型 HTML 代码然后带你做功能测试和性能观察最后整理常见问题和最佳实践。下面给出的代码是我的简化实现版本用于演示“用字符渲染 3D 城市”的核心流程不等同于原作品的完整源码。如果你想看完整原版效果建议去搜原项目名称并亲自打开页面体验。1. 核心能力速览先把这款效果的关键能力列出来这样你可以在继续读之前快速判断它适不适合自己。能力项说明项目类型纯前端创意渲染ASCII 字符 3D 城市来源信息从标题看来自 Grow Now! Games 相关创意项目渲染方式ASCII 字符亮度映射 透视投影 深度排序外部依赖无原则上只需要一个 HTML 文件3D 模型不需要建筑由程序化规则生成运行环境现代浏览器Chrome / Edge / Firefox 均可启动方式直接用浏览器打开 HTML 文件或通过本地静态服务器访问是否需要服务器本地直接打开也能跑用静态服务器更稳定GPU 需求不依赖 GPU/WebGL主要靠 CPU 绘制 Canvas 2D显存占用无实际显存压力属于浏览器页面效果API 能力无服务端 API如需嵌入其他系统可以考虑封装成前端函数批量任务场景不涉及批量任务可通过修改配置批量生成不同城市布局适合场景技术学习、网页背景动画、前端创意展示、赛博朋克风格 Demo这个表里最关键的信息是它不需要 GPU不需要 3D 模型文件不需要复杂的构建工具。只要一个浏览器就能跑起来。所以它的上手门槛非常低普通办公电脑也能流畅运行前提是字符分辨率不要设置得太高。2. 适用场景与使用边界2.1 适合哪些人前端开发者想了解“没有 Three.js 时不依赖 WebGL 怎么做 3D 视觉效果”。图形学初学者透视投影、深度排序这些概念在代码里很直观适合用来建立直觉。创意网页作者想做一个赛博朋克风格的页面背景或者加载动画这种字符城市很有辨识度。技术博主 / 演示作者单文件、零依赖复制到任何环境都能快速演示非常适合写进技术文章或教学案例。2.2 不适合什么场景需要精细 3D 模型、物理碰撞、真实光照的正式游戏不适合用字符渲染。需要大量数据交互、服务端运算的项目这不是它的定位。低端 Android 设备或老式浏览器Canvas 的逐字符绘制可能卡顿。希望完全精确复刻原作品的场景原项目可能有自己独特的算法、交互和视觉细节通常需要按官方渠道获取或阅读源码。2.3 版权与合规边界这类创意项目通常没有为“二次分发”预设完整授权。如果要把别人的成品直接搬进自己的网站、课程或商业产品中建议先确认作者声明。若文章里附带的原型代码由你基于原理自行实现它可以作为学习材料使用但如果用于商业项目最好基于自己的实现继续扩展并保留必要的技术说明。另外赛博朋克视觉风格容易让人联想到现实街道、建筑或品牌符号发布和商用前需要确认素材不涉及商标、肖像或敏感地点信息。3. 环境准备与前置条件这个项目的环境要求非常低。如果你只想打开成品只需要一个浏览器。如果你想自己修改代码再准备一个代码编辑器即可。3.1 浏览器版本建议使用 Chrome、Edge 或 Firefox 的较新版本。核心 API 是 Canvas 2D 的fillText和requestAnimationFrame这两个接口在现代浏览器里都很稳定。IE 完全不用考虑。3.2 代码编辑器VS Code、Sublime Text、记事本都可以。不需要安装任何语言环境不需要 Node.js也不需要 npm。因为整个项目就是单个 HTML 文件内部包含 CSS 和 JavaScript。3.3 本地静态服务器可选直接双击 HTML 文件通常也能运行。不过某些浏览器对本地文件的 Canvas 或字体资源会有安全限制用本地静态服务器更稳妥。最简单的做法是使用 VS Code 的 Live Server 插件或者在项目目录下执行# 在 HTML 文件所在目录启动一个静态服务器 python -m http.server 8080启动后访问http://localhost:8080/你的文件名.html即可。3.4 端口与进程如果 8080 被占用换一个端口python -m http.server 8090使用后关闭终端窗口即可结束服务。这个项目本身不监听网络端口所以不存在端口冲突问题。只有当你启动本地静态服务器时才需要留意端口占用。4. ASCII 3D 城市渲染的原理拆解这部分是本文的核心。理解原理之后你不仅会改参数还能按自己的需求扩展出新的效果。4.1 字符即像素ASCII 亮度映射ASCII 艺术的核心思想是用不同字符代表不同亮度。常见字符梯度是%#*-:.从左到右字符形状密度逐渐降低视觉亮度也降低。一个离相机很近的建筑可以用或%这类密集字符填充远处建筑用.或空格填充。这样即使没有颜色也能通过字符密度判断远近。在实际绘制时我们会根据物体的距离计算一个 0 到 1 的亮度系数再映射到字符数组的下标。代码里通常这样写const chars %#*-:. ; const t Math.min(1, (distance - near) / (far - near)); const index Math.max(0, Math.min(chars.length - 1, Math.floor(t * chars.length))); const char chars[index];t越大表示越远字符就越偏向右边的空白字符。4.2 透视投影把 3D 点搬到屏幕上要把 3D 城市画到 2D 屏幕上需要做透视投影。核心思路是物体离相机越远在屏幕上看起来越靠中间、越小。假设相机朝向 z 轴正方向一个三维点(x, y, z)到屏幕的坐标可以近似为const scale fov / z; const screenX centerX x * scale * k1; const screenY centerY - y * scale * k2;其中fov是视野参数k1和k2是横向和纵向的缩放系数。z是物体相对相机的深度。投影后原本在三维空间里的建筑坐标就变成了屏幕上的列号、行号也就是 ASCII 字符网格坐标。这个公式非常简单但只要 z 值大于零投影结果就是符合透视直觉的z 越大scale 越小画面上的建筑越小。4.3 深度排序与遮挡多个建筑同时出现在屏幕上的时候要决定谁画在前面。一个简单的做法是“画家算法”先画远处的建筑再画近处的建筑近处建筑会自然覆盖远处建筑。我的原型代码里会对建筑按相对深度排序const visible buildings .map(b ({ b, relZ: b.z - cameraZ })) .filter(item item.relZ 0.5) .sort((a, b) b.relZ - a.relZ);这样画出的城市前后遮挡关系基本正确。它不是最严谨的 Z-buffer但用在字符城市这种低分辨率场景下已经足够。4.4 程序化生成城市城市不需要预制模型而是用规则生成。最简单的规则是在道路两侧随机生成建筑每个建筑有固定的横向偏移、深度位置、宽度和高度。for (let i 0; i 50; i) { const side i % 2 0 ? -1 : 1; buildings.push({ x: side * (1.6 Math.random() * 0.8), // 横向偏移 z: 8 Math.random() * 36, // 深度位置 w: 0.8 Math.random() * 0.8, // 宽度 h: 0.8 Math.random() * 2.2, // 高度 }); }这样每一次刷新页面城市布局都会不同因为Math.random()的结果不同。如果希望固定种子、每次生成同一座城市可以用随机数生成器替换Math.random()比如简单的哈希随机函数。4.5 动画循环与相机运动城市的“飞行感”来自相机持续向前移动。动画循环里每一帧让cameraZ增加一个很小的量然后重新渲染。相机越过所有建筑后重置位置循环播放。function animate() { cameraZ speed; if (cameraZ 46) { cameraZ 0; } render(); requestAnimationFrame(animate); } animate();这个循环是浏览器端动画的标准写法。requestAnimationFrame会尽量让动画保持在显示器的刷新率大约 60 帧每秒。如果页面标签页切到后台浏览器会自动降帧节省 CPU。5. 最小可运行原型实现下面给出一份完整可运行的 HTML 代码基于伪 3D 投影解析加上 ASCII 字符填充实现一个移动视角的赛博朋克街区。你可以直接复制保存为.html文件然后用浏览器打开。5.1 全局参数先定义字符网格分辨率。用 80 列乘以 30 行的字符网格每个字符的像素尺寸为 10 乘 18。整体 Canvas 画布是 800 乘 540 像素。CSS 里可以把 Canvas 放大到 960 乘 540形成更明显的像素颗粒感。5.2 建筑数据生成建筑沿一条虚拟街道两侧分布。横坐标x为负表示在街道左侧为正表示在右侧。z是建筑距离相机路径的深度。w控制建筑宽度h控制建筑高度。5.3 渲染循环每一帧做三件事清屏、绘制天空和地面、按远近距离绘制建筑。建筑投影后根据深度选择字符和颜色填充到对应的字符网格区域。5.4 完整代码!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleASCII 3D Cyberpunk City/title style body { margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center; background: #000; } canvas { background: #000; border: 1px solid #222; width: 960px; height: 540px; image-rendering: pixelated; } /style /head body canvas idscreen/canvas script const canvas document.getElementById(screen); const ctx canvas.getContext(2d); // 字符网格分辨率 const COLS 80; const ROWS 30; const CW 10; const CH 18; canvas.width COLS * CW; canvas.height ROWS * CH; ctx.font CH - 4 px monospace; ctx.textAlign center; ctx.textBaseline top; // 亮度字符集从密到疏亮 - 暗 const chars %#*-:. ; // 程序化生成城市街道两侧随机建筑 const buildings []; for (let i 0; i 50; i) { const side i % 2 0 ? -1 : 1; buildings.push({ x: side * (1.6 Math.random() * 0.8), z: 8 Math.random() * 36, w: 0.8 Math.random() * 0.8, h: 0.8 Math.random() * 2.2 }); } // 相机参数 let cameraZ 0; const speed 0.25; const horizonRow ROWS * 0.45; function render() { // 清屏 ctx.fillStyle #050505; ctx.fillRect(0, 0, canvas.width, canvas.height); // 天空区域稀疏点阵 ctx.fillStyle #0b0b0b; for (let row 0; row horizonRow; row) { for (let col 0; col COLS; col) { ctx.fillText(., col * CW CW / 2, row * CH); } } // 地面区域更亮的点阵 ctx.fillStyle #111; for (let row Math.floor(horizonRow); row ROWS; row) { for (let col 0; col COLS; col) { ctx.fillText(:, col * CW CW / 2, row * CH); } } // 按距离排序远的先画 const visible buildings .map(b ({ b, relZ: b.z - cameraZ })) .filter(item item.relZ 0.5) .sort((a, b) b.relZ - a.relZ); for (const { b, relZ } of visible) { const scale 16 / relZ; const bottomRow horizonRow scale * 5; const topRow bottomRow - b.h * scale * 14; const colCenter COLS / 2 b.x * scale * 7; const widthCols Math.max(1, b.w * scale * 7); // 根据距离选择字符 const t Math.min(1, (relZ - 2) / 38); const rampIndex Math.max(0, Math.min(chars.length - 1, Math.floor(t * chars.length))); const c chars[rampIndex]; // 颜色随距离变化 if (relZ 12) { ctx.fillStyle #0f0; } else if (relZ 24) { ctx.fillStyle #0b8; } else { ctx.fillStyle #046; } const rowStart Math.max(0, Math.floor(topRow)); const rowEnd Math.min(ROWS - 1, Math.ceil(bottomRow)); const colStart Math.max(0, Math.floor(colCenter - widthCols / 2)); const colEnd Math.min(COLS - 1, Math.ceil(colCenter widthCols / 2)); for (let r rowStart; r rowEnd; r) { for (let col colStart; col colEnd; col) { ctx.fillText(c, col * CW CW / 2, r * CH); } } } } function animate() { cameraZ speed; if (cameraZ 46) { cameraZ 0; } render(); requestAnimationFrame(animate); } animate(); /script /body /html这段代码在本地保存后直接双击就能运行。如果双击后页面显示空白或者 Canvas 区域完全没有内容优先检查浏览器版本然后尝试通过本地静态服务器访问。5.5 如何调整城市风格想要更多建筑把生成循环改成for (let i 0; i 80; i)。想要更宽的街道把x的基数 1.6 改成 2.5。想要更密集的高楼把h的随机范围改大比如0.8 Math.random() * 4。想要不同字符风格替换chars字符串比如改用█▓▒░ 。想要改变移动速度调整speed数值越大飞得越快。这些修改都在 HTML 文件内部的script区域改完保存并刷新浏览器即可。6. 功能测试与效果验证6.1 启动测试用 Chrome 或 Edge 打开 HTML 文件。预期看到屏幕上半部分是暗色天空下半部分是道路地面。道路两侧有高低不同的建筑建筑由字符填充。建筑随着时间向屏幕后方移动形成向前飞行的效果。近处建筑偏亮绿色远处建筑偏暗蓝色。如果看到这些说明最基础的功能已经跑通。6.2 运动速度测试把speed从 0.25 改成 0.6刷新页面。预期飞行速度明显加快。如果速度过快导致视觉眩晕说明这个值设置得太高。适合演示的速度通常在 0.2 到 0.4 之间。6.3 建筑密度测试把建筑数量从 50 改成 100观察两点街道两边的建筑会更密集城市感更丰富。帧率可能下降。如果卡顿明显说明 Canvas 的逐字符绘制成了瓶颈。建议小范围调整从 60 开始试找到效果与性能的平衡点。6.4 字符效果测试把chars改成只包含空格和几个字符比如 城市会被压缩成极端的黑白对比。改成.:-*#%则会把字符映射关系反转近处字符反而稀疏。这时可以看到字符密度对视觉深度的直接影响。6.5 判断成功标准建筑应该有近大远小的透视效果。近处建筑应该覆盖远处建筑位置关系正确。每帧画面保持连贯没有明显闪烁。CPU 占用在可接受范围内普通桌面浏览器能带动。如果打开开发者工具Console 面板没有报错。6.6 失败时优先检查如果页面全黑先看是否有语法错误。如果建筑没有移动检查requestAnimationFrame调用是否被注释。如果建筑扎堆在屏幕中央检查x基数是否设置得太小。7. 资源占用与性能观察7.1 如何观察资源占用在浏览器中按 F12 打开开发者工具切换到 Performance 面板点击 Record 录制几秒钟动画。录制结束后可以看到每一帧的耗时。对 Canvas 字符渲染来说主要耗时集中在fillText调用上因为每个字符都是一次独立的文本绘制操作。7.2 影响性能的关键参数参数影响COLS和ROWS字符网格越大每帧需要绘制的字符越多CPU 占用越高建筑数量需要排序和绘制的物体越多CPU 占用越高建筑投影面积建筑在屏幕上占的字符区域越大fillText调用次数越多speed速度过快会导致每帧重绘范围变化大但通常不是主要瓶颈字符集长度只影响索引计算几乎不影响性能Canvas 物理尺寸物理尺寸越大fillText的清屏和绘制开销可能越高其中影响最大的是COLS和ROWS。比如从 80 乘 30 改成 160 乘 60字符数量会从 2400 变成 9600四倍增长。如果本机性能不够优先降低这个分辨率。7.3 降低性能开销的方法降低字符分辨率比如COLS 60、ROWS 24。减少建筑数量从 50 改成 30。限制建筑投影区域在绘制前判断建筑是否完全在屏幕外如果是直接跳过。用离屏 Canvas 缓存静态背景天空和地面如果没有变化可以只画一次。减少每帧颜色切换提前把颜色相近的建筑分组绘制。如果追求更高的帧率和更复杂的场景后期可以考虑用 WebGL 做字符纹理采样但这不是本文范围。7.4 为什么这个项目不需要 GPU因为字符渲染本质上是把字符画到 Canvas 2D 上CPU 就能完成。它没有复杂的顶点着色、贴图采样和光照计算。所以只要 CPU 性能尚可就能跑起来。这也意味着它与“显存占用”这个概念基本无关。8. 常见问题与排查方法问题现象可能原因排查方式解决方案打开后页面全黑JS 语法错误或浏览器不支持 Canvas按 F12 查看 Console 报错根据报错修正代码换 Chrome/Edge 最新版页面打开但只有背景没有建筑建筑数量为 0或visible被过滤掉检查buildings数组长度和relZ条件确认z cameraZ 0.5增加建筑数量建筑不移动动画循环没有启动检查是否有requestAnimationFrame(animate)确认 animate 函数被调用建筑都在同一侧x基数判断逻辑有误打印side和x值确认side正负分配正确字符过于密集看不出建筑形状建筑宽度widthCols过大观察widthCols
返回列表