首页/新闻资讯/正文详情

单文件HTML实现网页版我的世界:体素渲染与地形生成实战

发布时间:2026/9/26 2:07:27 来源:云帆数科 栏目:资讯中心
单文件HTML实现网页版我的世界:体素渲染与地形生成实战
简介这份资源是一份用单个HTML文件实现网页版《我的世界》的完整游戏源码面向对前端开发、WebGL与3D网页游戏感兴趣的学习者和开发者。它把完整的Minecraft 1.12.2客户端封装进一个HTML文件中无需插件即可在现代浏览器中运行可用于研究单文件部署、资源压缩与浏览器端3D渲染等实践场景。压缩包内共1个文件为html格式包体约14.96MB解压后直接打开即可体验在中等配置电脑上可达30至60FPS首次加载约10至30秒内存占用约200至500MB兼容Chrome、Firefox、Edge等主流浏览器。目前已有5973人学习下载。通过阅读源码读者可以了解如何将大量游戏资源与逻辑整合进单一页面掌握前端工程化与性能优化的思路并以此为模板改造出属于自己的网页版沙盒游戏。1. 单文件网页版《我的世界》一个 HTML 文件凭什么能跑起方块世界你可能见过那种打开浏览器就能玩的方块小游戏地址栏里只有一个.html文件没有后端、没有构建工具、没有node_modules双击就能进。很多人第一反应是「这不就是个玩具吗」但真正拆开看它把体素渲染、地形生成、碰撞检测、第一人称相机这几件事全塞进了一个文件里而且跑得还挺顺。这就是「单 HTML 实现的网页版《我的世界》游戏源码」这个方向真正有意思的地方它不是让你复刻一个完整的 Minecraft而是用最小的工程代价把「体素世界」这套技术骨架讲清楚。适合谁看如果你写过 HTML、CSS、JS 基础语法想找一个能一口气读完、改得动、跑得起来的综合项目练手这个方向非常合适。如果你是想给团队做 3D 可视化原型、想理解 WebGL 渲染管线怎么落地它同样是一个低门槛入口。接下来我会按「先立住原理、再动手复现、最后讲坑」的顺序把这件事从零讲到能改。2. 体素世界的地基从坐标系到网格合并2.1 为什么是体素而不是普通 3D 模型普通 3D 游戏里的场景通常由美术在建模软件里做好一个个模型导出成 glTF 或 OBJ再加载进引擎。体素世界完全不是这个路子它的最小单位是一个个等大的立方体位置全部落在整数网格上。这样做的好处是地形可以用算法生成而不是靠美术堆破坏和放置只需要改一个数组下标不需要重新建模碰撞检测也可以退化成「查表」——某个格子有没有方块查一下就知道。代价也很明显如果每个方块都单独画一次一个 16×16×16 的区块就是 4096 次绘制调用浏览器直接跪。所以体素引擎的核心优化只有一个方向——合并。把相邻的、同类型的方块面合并成一个大网格只画露在外面的面内部面全部剔除。这一步做不做直接决定你的网页版是「能玩」还是「卡成 PPT」。2.2 用 Three.js 搭最小可跑骨架单 HTML 文件不等于从零写 WebGL。常见做法是引入 Three.js 的 CDN 版本用它管相机、场景、渲染循环我们只负责体素数据的组织和网格生成。下面是一个最小骨架能跑出一个带光照的方块地面。!DOCTYPE html html langzh-cn head meta charsetutf-8 title单文件体素世界最小骨架/title style html, body { margin: 0; height: 100%; overflow: hidden; } canvas { display: block; } /style /head body script typeimportmap { imports: { three: https://unpkg.com/three0.160.0/build/three.module.js } } /script script typemodule import * as THREE from three; // 1. 基础三件套场景、相机、渲染器 const scene new THREE.Scene(); scene.background new THREE.Color(0x87ceeb); // 天空色 const camera new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ); camera.position.set(8, 12, 8); camera.lookAt(8, 0, 8); const renderer new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); // 2. 光照环境光 平行光保证方块有明暗面 scene.add(new THREE.AmbientLight(0xffffff, 0.6)); const sun new THREE.DirectionalLight(0xffffff, 0.8); sun.position.set(1, 2, 1); scene.add(sun); // 3. 体素数据用一维数组存 16x16 的地面1 表示有方块 const SIZE 16; const voxels new Uint8Array(SIZE * SIZE); voxels.fill(1); // 4. 合并几何只生成朝上的面先跑通再说 const positions []; const indices []; let vi 0; for (let x 0; x SIZE; x) { for (let z 0; z SIZE; z) { const y 0; // 一个面的四个顶点 positions.push(x, y, z, x 1, y, z, x 1, y, z 1, x, y, z 1); indices.push(vi, vi 1, vi 2, vi, vi 2, vi 3); vi 4; } } const geo new THREE.BufferGeometry(); geo.setAttribute(position, new THREE.Float32BufferAttribute(positions, 3)); geo.setIndex(indices); geo.computeVertexNormals(); const mat new THREE.MeshLambertMaterial({ color: 0x4caf50 }); scene.add(new THREE.Mesh(geo, mat)); // 5. 渲染循环 function animate() { requestAnimationFrame(animate); renderer.render(scene, camera); } animate(); // 6. 窗口自适应 window.addEventListener(resize, () { camera.aspect window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }); /script /body /html这段代码的逻辑说明importmap让浏览器知道three这个裸模块名对应哪个地址这是单文件项目里引入依赖最干净的方式不需要打包工具。体素数据用Uint8Array一维数组存索引公式是x z * SIZE比二维数组省内存也更快。几何生成部分只处理了朝上的面因为地面只需要顶面可见这是「面剔除」的最简形态。参数说明SIZE控制地面边长改成 32 或 64 会明显增加顶点数但因为是合并网格绘制调用仍然只有一次。camera的far设成 1000 是为了后续加远距离地形留余量。MeshLambertMaterial对光照有反应但不计算高光体素风格用它最合适换成MeshBasicMaterial会失去明暗方块会糊成一片。2.3 面剔除只画露在外面的那一面上面只画了顶面真实体素世界需要判断六个方向。核心逻辑是对每个方块检查它上下左右前后六个邻居如果邻居是空的就生成对应方向的面。下面是一个可复用的面定义表。// 六个方向法线方向 四个顶点偏移逆时针保证正面朝外 const FACES [ { dir: [ 1, 0, 0], corners: [[1,1,0],[1,0,0],[1,1,1],[1,0,1]] }, // 右 { dir: [-1, 0, 0], corners: [[0,1,1],[0,0,1],[0,1,0],[0,0,0]] }, // 左 { dir: [ 0, 1, 0], corners: [[0,1,0],[0,1,1],[1,1,0],[1,1,1]] }, // 上 { dir: [ 0,-1, 0], corners: [[0,0,1],[0,0,0],[1,0,1],[1,0,0]] }, // 下 { dir: [ 0, 0, 1], corners: [[0,1,1],[0,0,1],[1,1,1],[1,0,1]] }, // 前 { dir: [ 0, 0,-1], corners: [[1,1,0],[1,0,0],[0,1,0],[0,0,0]] }, // 后 ]; function isSolid(x, y, z) { if (x 0 || y 0 || z 0 || x SIZE || z SIZE) return false; return voxels[x z * SIZE] 1; } function buildMesh() { const positions [], indices []; let vi 0; for (let x 0; x SIZE; x) { for (let z 0; z SIZE; z) { const y 0; if (!isSolid(x, y, z)) continue; for (const face of FACES) { const [dx, dy, dz] face.dir; // 邻居是实心就跳过这个面 if (isSolid(x dx, y dy, z dz)) continue; for (const [cx, cy, cz] of face.corners) { positions.push(x cx, y cy, z cz); } indices.push(vi, vi 1, vi 2, vi, vi 2, vi 3); vi 4; } } } // ... 后续和上面一样塞进 BufferGeometry }逻辑说明FACES表把「方向」和「顶点顺序」绑定在一起顶点按逆时针排列这样默认的正面剔除才会把面朝外。isSolid越界返回false意味着世界边缘的方块会生成外侧面视觉上不会出现「世界被切掉」的穿帮。参数说明顶点顺序不能随便改改错会导致面朝内、被背面剔除掉现象是「方块看起来透明」。如果你发现某个方向的面消失先检查corners的顺序是不是逆时针。3. 地形生成与第一人称控制让世界像那么回事3.1 用值噪声生成起伏地形一个纯平的地面没有「世界感」。常见做法是用噪声函数生成高度图。不引入额外库的话可以手写一个简单的值噪声够用且可控。// 简易值噪声用整数哈希 双线性插值 function hash(x, z) { let h x * 374761393 z * 668265263; h (h ^ (h 13)) * 1274126177; return ((h ^ (h 16)) 0) / 4294967295; } function lerp(a, b, t) { return a (b - a) * t; } function valueNoise(x, z) { const x0 Math.floor(x), z0 Math.floor(z); const tx x - x0, tz z - z0; const v00 hash(x0, z0), v10 hash(x0 1, z0); const v01 hash(x0, z0 1), v11 hash(x0 1, z0 1); return lerp(lerp(v00, v10, tx), lerp(v01, v11, tx), tz); } // 生成 32x32 地形高度在 0~6 之间 const WORLD 32; const heightMap new Uint8Array(WORLD * WORLD); for (let x 0; x WORLD; x) { for (let z 0; z WORLD; z) { const n valueNoise(x / 8, z / 8) * 0.7 valueNoise(x / 4, z / 4) * 0.3; heightMap[x z * WORLD] Math.floor(n * 6); } }逻辑说明hash把坐标打散成 0~1 的伪随机数valueNoise在整数格点上取值再做双线性插值得到连续起伏。两层噪声叠加是常见做法低频决定大山丘高频决定小起伏权重 0.7 和 0.3 是我一般会用的起点。参数说明x / 8里的 8 是频率数值越大起伏越密* 6控制最大高度改大会让地形更陡。如果你想让地形更「我的世界」一点可以把高度取整后按高度分层给不同颜色比如 0~1 是沙子、2~4 是草、5 以上是石头。3.2 第一人称相机指针锁定与 WASD 移动网页里做第一人称绕不开 Pointer Lock API。点击画布后锁定鼠标移动鼠标控制视角按 WASD 移动空格跳跃。下面是最小实现。const player { pos: new THREE.Vector3(16, 10, 16), vel: new THREE.Vector3(), yaw: 0, pitch: 0, onGround: false, }; const keys {}; document.addEventListener(keydown, e keys[e.code] true); document.addEventListener(keyup, e keys[e.code] false); // 指针锁定 renderer.domElement.addEventListener(click, () { renderer.domElement.requestPointerLock(); }); document.addEventListener(mousemove, e { if (document.pointerLockElement ! renderer.domElement) return; player.yaw - e.movementX * 0.002; player.pitch - e.movementY * 0.002; // 限制俯仰角避免翻转 player.pitch Math.max(-Math.PI / 2, Math.min(Math.PI / 2, player.pitch)); }); function updatePlayer(dt) { const speed 6; const dir new THREE.Vector3(); if (keys[KeyW]) dir.z - 1; if (keys[KeyS]) dir.z 1; if (keys[KeyA]) dir.x - 1; if (keys[KeyD]) dir.x 1; dir.normalize().applyAxisAngle(new THREE.Vector3(0, 1, 0), player.yaw); player.vel.x dir.x * speed; player.vel.z dir.z * speed; player.vel.y - 20 * dt; // 重力 player.pos.addScaledVector(player.vel, dt); // 简易地面碰撞查高度图 const gx Math.floor(player.pos.x); const gz Math.floor(player.pos.z); if (gx 0 gz 0 gx WORLD gz WORLD) { const groundY heightMap[gx gz * WORLD] 1; if (player.pos.y groundY) { player.pos.y groundY; player.vel.y 0; player.onGround true; } } // 相机跟随 camera.position.copy(player.pos); camera.rotation.order YXZ; camera.rotation.y player.yaw; camera.rotation.x player.pitch; }逻辑说明yaw和pitch分别控制左右和上下视角rotation.order YXZ是 FPS 相机的标准顺序先偏航再俯仰避免出现「歪头」效果。移动方向先按本地坐标算再用applyAxisAngle绕 Y 轴旋转到世界方向这样按 W 永远是「朝前」而不是「朝北」。参数说明0.002是鼠标灵敏度嫌快就调小。speed 6是移动速度单位是格/秒。重力20和跳跃初速度要配套调常见做法是跳跃初速度设成 8 左右能跳约 1.6 格高刚好跳上一个方块。3.3 把渲染循环改成带 dt 的固定节奏上面骨架里的animate没有传时间差物理更新会跟帧率绑定高刷屏上人物会飞。正确做法是记录上一帧时间算出dt再传给updatePlayer。let last performance.now(); function animate(now) { requestAnimationFrame(animate); const dt Math.min((now - last) / 1000, 0.05); // 上限 50ms防跳帧穿模 last now; updatePlayer(dt); renderer.render(scene, camera); } requestAnimationFrame(animate);逻辑说明Math.min(..., 0.05)是后悔药切标签页回来时dt可能有好几秒不夹住的话人物会瞬移穿地。这是血泪经验别省这一行。参数说明0.05对应 20 FPS 的下限低于这个帧率物理会变慢但不会穿模。如果你的场景更复杂可以把这个值调到 0.033。4. 避坑与排查单文件体素项目最容易翻车的 5 个地方4.1 现象方块面朝内看起来像透明原因顶点顺序不是逆时针或者FACES表里某个方向的corners写反了。Three.js 默认开启背面剔除面朝内就看不见。解决逐个方向检查corners顺序确保从外部看是逆时针。临时排查可以把material.side设成THREE.DoubleSide如果面出现了就说明是朝向问题改完再改回默认。4.2 现象地形边缘出现黑边或闪烁原因相邻区块的顶点没有对齐或者两个面共面导致 Z-fighting。体素世界里常见于「地面」和「方块底面」同时生成。解决面剔除逻辑要严格邻居是实心就绝不生成面。如果确实需要两个面贴合给其中一个加极小偏移或者用polygonOffset。4.3 现象鼠标锁定后视角乱转松手还在转原因mousemove监听没有判断pointerLockElement或者灵敏度乘数太大。解决监听里先判断锁定状态再处理movementX/Y。灵敏度从0.002起步不要一上来就0.01。4.4 现象切标签页回来人物穿地原因dt没有夹上限恢复时累积了巨大时间差。解决dt用Math.min夹到 0.05 以内物理更新里再做一次位置修正双保险。4.5 现象方块多了以后帧率骤降原因每个方块单独一个 Mesh绘制调用爆炸。解决按区块合并几何一个区块一个 Mesh。区块大小常见取 16×16×16太大合并耗时长太小绘制调用多。改完用浏览器 Performance 面板看 draw call 数量正常应该是个位数到几十。5. 进阶把单文件体素世界改造成能玩的「小世界」走到这里你已经有了地形、相机、碰撞和渲染循环。接下来这一步是把它从「能跑」变成「能玩」。我一般会先加方块交互用射线检测从相机往前打命中哪个方块就改哪个格子的数据然后重建那个区块的网格。射线检测用 Three.js 的Raycaster最省事但体素世界有更快的做法——DDA 步进沿着射线一格一格走遇到实心就停。DDA 的代码不长但能避免Raycaster对合并网格的遍历开销。// DDA 体素射线返回第一个命中的方块坐标和法线 function raycastVoxel(origin, dir, maxDist 8) { let x Math.floor(origin.x), y Math.floor(origin.y), z Math.floor(origin.z); const stepX Math.sign(dir.x), stepY Math.sign(dir.y), stepZ Math.sign(dir.z); const tDeltaX Math.abs(1 / dir.x), tDeltaY Math.abs(1 / dir.y), tDeltaZ Math.abs(1 / dir.z); let tMaxX ((stepX 0 ? x 1 - origin.x : origin.x - x)) * tDeltaX; let tMaxY ((stepY 0 ? y 1 - origin.y : origin.y - y)) * tDeltaY; let tMaxZ ((stepZ 0 ? z 1 - origin.z : origin.z - z)) * tDeltaZ; let normal [0, 0, 0]; for (let i 0; i 64; i) { if (isSolid(x, y, z)) return { x, y, z, normal }; if (tMaxX tMaxY tMaxX tMaxZ) { x stepX; tMaxX tDeltaX; normal [-stepX, 0, 0]; } else if (tMaxY tMaxZ) { y stepY; tMaxY tDeltaY; normal [0, -stepY, 0]; } else { z stepZ; tMaxZ tDeltaZ; normal [0, 0, -stepZ]; } if (tMaxX maxDist tMaxY maxDist tMaxZ maxDist) break; } return null; }逻辑说明DDA 的核心是维护三个方向上的「下一次跨格距离」tMax每次走最小的那个保证射线按格子顺序推进不会漏格。normal记录是从哪个面进来的放置方块时用「命中坐标 法线」就是新方块的位置。参数说明maxDist 8是交互距离改成 5 更接近生存模式手感。循环上限 64 是保险防止射线在空世界里跑太久。验证方法很简单在updatePlayer后面加一段按鼠标左键破坏、右键放置然后重建对应区块的网格。重建时只重建受影响的区块不要全图重建否则放一个方块卡一下。区块索引用Math.floor(x / 16)算重建函数接收区块坐标只遍历那个区块的体素。我自己的习惯是每加一个功能就先在控制台打一行日志确认数据变了再去看画面。体素项目里「数据对了画面不对」和「数据就不对」是两类完全不同的 bug先分清能省一半时间。希望帮到你。本文还有配套的精品资源点击获取

相关推荐

Fortify SCA插件原理与DevSecOps工程化落地指南
Fortify SCA插件原理与DevSecOps工程化落地指南

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

PaddleNLP GPT 量化感知训练(QAT)实战指南:从 INT8 训练、敏感度分析到模型导出与验证
PaddleNLP GPT 量化感知训练(QAT)实战指南:从 INT8 训练、敏感度分析到模型导出与验证

人工智能大模型预训练微调LoRARLHF强化学习分布式训练 【免费下载链接】PaddleNLP Easy-to-use and powerful LLM and SLM library with awesome model zoo. 项目地址: https://gitcode.com/gh_mirrors/pa/PaddleNLP 点击查看 免费下载 导读 本文面向希望在推理阶… · 2026/9/26 2:07:27

SQL Server 2019下载安装失败真相:验证错误、通道失效与避坑指南
SQL Server 2019下载安装失败真相:验证错误、通道失效与避坑指南

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

苍穹外卖
苍穹外卖

引言标题欧克,接下来让我们步入正题项目背景:黑马程序员《苍穹外卖》作为Java企业级开发实战项目,涵盖SpringBoot、MySQL、Redis等技术栈学习目标:掌握外卖系统核心功能开发与全栈技术整合技术选择技术选型技术栈概览后端框架&… · 2026/9/26 2:41:12

判据、全绿、缺口、漂移与自查 —— 线上补课(下)
判据、全绿、缺口、漂移与自查 —— 线上补课(下)

系列:gdev-master(NVIDIA/nouveau 用户态 GPGPU 运行时)从 C/C 到 Rust 的移植工程 第三部换了一个问题:前两部回答「怎么搬」,这一部回答「凭什么说对了」。 一句话核心最值钱的一格每写一条检查,同时写… · 2026/9/26 2:41:00

通达信涨停回踩缩量选股公式:BARSLAST函数实战与参数调优
通达信涨停回踩缩量选股公式:BARSLAST函数实战与参数调优

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

Tauri桌面应用上线前,必须先查的3个安全细节
Tauri桌面应用上线前,必须先查的3个安全细节

Tauri 桌面应用上线前,必须先查的 3 个安全细节发布元信息(发布前删掉此区块) 标题备选:《Tauri 桌面应用上线前,必须先查的 3 个安全细节》《本地应用不是更安全:Tauri 应用的 3 个致命细节》关键词&#… · 2026/9/26 2:41:00

MaaEnd架构深度剖析:明日方舟终末地自动化工具的四层设计——Pipeline低代码、Go Service与C++算法层完整拆解
MaaEnd架构深度剖析:明日方舟终末地自动化工具的四层设计——Pipeline低代码、Go Service与C++算法层完整拆解

MaaEnd架构深度剖析:明日方舟终末地自动化工具的四层设计——Pipeline低代码、Go Service与C算法层完整拆解 【免费下载链接】MaaEnd MaaEnd 终末地小助手:基于视觉 AI 的「明日方舟:终末地」自动化工具 项目地址: https://gitcode.com/gh_… · 2026/9/26 2:40:54

Reflector 7.4.1.179 绿色注册版:两大插件集成,反编译与依赖分析一步到位
Reflector 7.4.1.179 绿色注册版:两大插件集成,反编译与依赖分析一步到位

简介:这是一份面向.NET开发者的反编译工具整合包,主角是经典工具.NET Reflector,版本为7.4.1.179,已注册的绿色版,解压即可运行。它解决了原版Reflector只能逐个查看方法、无法批量导出源码的痛点,并真正集… · 2026/9/26 2:40:42

数据库课后习题答案别硬背:当测试用例集刷,效率翻倍
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍

简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21

OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置

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

向下兼容与向上兼容:接口设计中的兼容性策略与工程实践
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践

一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码