浏览代码

Added image classify api in AGI

Toby Chui 3 年之前
父节点
当前提交
2e26ecc502
共有 100 个文件被更改,包括 8104 次插入2 次删除
  1. 47 2
      AGI Documentation.md
  2. 二进制
      a.jpg
  3. 64 0
      legacy/WebMC/.gitignore
  4. 18 0
      legacy/WebMC/README.md
  5. 67 0
      legacy/WebMC/index.html
  6. 11 0
      legacy/WebMC/init.agi
  7. 20 0
      legacy/WebMC/mc.css
  8. 28 0
      legacy/WebMC/server.js
  9. 66 0
      legacy/WebMC/src/Entity/Entity.js
  10. 20 0
      legacy/WebMC/src/Entity/EntityController.js
  11. 147 0
      legacy/WebMC/src/Entity/Player.js
  12. 361 0
      legacy/WebMC/src/Entity/PlayerLocalController.js
  13. 158 0
      legacy/WebMC/src/Renderer/Camera.js
  14. 136 0
      legacy/WebMC/src/Renderer/HighlightSelectedBlock.js
  15. 133 0
      legacy/WebMC/src/Renderer/Program.js
  16. 206 0
      legacy/WebMC/src/Renderer/Render.js
  17. 74 0
      legacy/WebMC/src/Renderer/WelcomePageRenderer.js
  18. 647 0
      legacy/WebMC/src/Renderer/WorldChunkModule.js
  19. 63 0
      legacy/WebMC/src/Renderer/WorldRenderer.js
  20. 185 0
      legacy/WebMC/src/Renderer/glsl.js
  21. 104 0
      legacy/WebMC/src/UI/Component.js
  22. 65 0
      legacy/WebMC/src/UI/HowToPlayPage.html
  23. 15 0
      legacy/WebMC/src/UI/HowToPlayPage.js
  24. 25 0
      legacy/WebMC/src/UI/LoadTerrainPage.html
  25. 32 0
      legacy/WebMC/src/UI/LoadTerrainPage.js
  26. 26 0
      legacy/WebMC/src/UI/MCButton.html
  27. 39 0
      legacy/WebMC/src/UI/MCButton.js
  28. 31 0
      legacy/WebMC/src/UI/MCCrosshairs.js
  29. 71 0
      legacy/WebMC/src/UI/MCFullScreenBtn.js
  30. 119 0
      legacy/WebMC/src/UI/MCHotbar.html
  31. 109 0
      legacy/WebMC/src/UI/MCHotbar.js
  32. 91 0
      legacy/WebMC/src/UI/MCInventory.html
  33. 45 0
      legacy/WebMC/src/UI/MCInventory.js
  34. 56 0
      legacy/WebMC/src/UI/MCMoveBtns.html
  35. 117 0
      legacy/WebMC/src/UI/MCMoveBtns.js
  36. 135 0
      legacy/WebMC/src/UI/Page.js
  37. 28 0
      legacy/WebMC/src/UI/PausePage.html
  38. 21 0
      legacy/WebMC/src/UI/PausePage.js
  39. 24 0
      legacy/WebMC/src/UI/PlayPage.html
  40. 90 0
      legacy/WebMC/src/UI/PlayPage.js
  41. 93 0
      legacy/WebMC/src/UI/PreloadPage.js
  42. 34 0
      legacy/WebMC/src/UI/SettingPage.html
  43. 30 0
      legacy/WebMC/src/UI/SettingPage.js
  44. 60 0
      legacy/WebMC/src/UI/WelcomePage.html
  45. 47 0
      legacy/WebMC/src/UI/WelcomePage.js
  46. 17 0
      legacy/WebMC/src/UI/index.js
  47. 237 0
      legacy/WebMC/src/World/Block.js
  48. 224 0
      legacy/WebMC/src/World/Block.txt
  49. 101 0
      legacy/WebMC/src/World/Chunk.js
  50. 374 0
      legacy/WebMC/src/World/World.js
  51. 238 0
      legacy/WebMC/src/World/WorldFluidCal.js
  52. 313 0
      legacy/WebMC/src/World/WorldLight.js
  53. 94 0
      legacy/WebMC/src/World/blocks.json
  54. 180 0
      legacy/WebMC/src/World/noise.js
  55. 3 0
      legacy/WebMC/src/World/worldDefaultConfig.json
  56. 21 0
      legacy/WebMC/src/globalVeriable.js
  57. 16 0
      legacy/WebMC/src/main.js
  58. 380 0
      legacy/WebMC/src/processingPictures.js
  59. 79 0
      legacy/WebMC/src/utils/EventDispatcher.js
  60. 42 0
      legacy/WebMC/src/utils/FiniteStateMachine.js
  61. 501 0
      legacy/WebMC/src/utils/gmath.js
  62. 34 0
      legacy/WebMC/src/utils/isWebGL2Context.js
  63. 84 0
      legacy/WebMC/src/utils/loadResources.js
  64. 二进制
      legacy/WebMC/texture/background.png
  65. 二进制
      legacy/WebMC/texture/gui.png
  66. 二进制
      legacy/WebMC/texture/icons.png
  67. 二进制
      legacy/WebMC/texture/jumpingBlock.gif
  68. 二进制
      legacy/WebMC/texture/mc-font.ttf
  69. 二进制
      legacy/WebMC/texture/panorama.png
  70. 二进制
      legacy/WebMC/texture/spritesheet.png
  71. 二进制
      legacy/WebMC/texture/terrain-atlas.png
  72. 二进制
      legacy/WebMC/texture/title.png
  73. 5 0
      main.go
  74. 66 0
      mod/agi/agi.image.go
  75. 146 0
      mod/neuralnet/neuralnet.go
  76. 114 0
      out.txt
  77. 8 0
      system/neuralnet/cfg/coco.data
  78. 789 0
      system/neuralnet/cfg/yolov3.cfg
  79. 二进制
      system/neuralnet/darknet_darwin_amd64
  80. 二进制
      system/neuralnet/darknet_linux_amd64
  81. 二进制
      system/neuralnet/darknet_windows_amd64.exe
  82. 80 0
      system/neuralnet/data/coco.names
  83. 二进制
      system/neuralnet/data/labels/100_0.png
  84. 二进制
      system/neuralnet/data/labels/100_1.png
  85. 二进制
      system/neuralnet/data/labels/100_2.png
  86. 二进制
      system/neuralnet/data/labels/100_3.png
  87. 二进制
      system/neuralnet/data/labels/100_4.png
  88. 二进制
      system/neuralnet/data/labels/100_5.png
  89. 二进制
      system/neuralnet/data/labels/100_6.png
  90. 二进制
      system/neuralnet/data/labels/100_7.png
  91. 二进制
      system/neuralnet/data/labels/101_0.png
  92. 二进制
      system/neuralnet/data/labels/101_1.png
  93. 二进制
      system/neuralnet/data/labels/101_2.png
  94. 二进制
      system/neuralnet/data/labels/101_3.png
  95. 二进制
      system/neuralnet/data/labels/101_4.png
  96. 二进制
      system/neuralnet/data/labels/101_5.png
  97. 二进制
      system/neuralnet/data/labels/101_6.png
  98. 二进制
      system/neuralnet/data/labels/101_7.png
  99. 二进制
      system/neuralnet/data/labels/102_0.png
  100. 二进制
      system/neuralnet/data/labels/102_1.png

+ 47 - 2
AGI Documentation.md

@@ -503,7 +503,14 @@ imagelib.getImageDimension("user:/Desktop/test.jpg"); 									//return [width,
 imagelib.resizeImage("user:/Desktop/input.png", "user:/Desktop/output.png", 500, 300); 	//Resize input.png to 500 x 300 pixal and write to output.png
 imagelib.loadThumbString("user:/Desktop/test.jpg"); //Load the given file's thumbnail as base64 string, return false if failed
 imagelib.cropImage("user:/Desktop/test.jpg", "user:/Desktop/out.jpg",100,100,200,200)); 
-/*
+//Classify an image using neural network, since v1.119
+imagelib.classify("tmp:/classify.jpg", "yolo3"); 
+
+```
+
+#### Crop Image Options
+
+```
 Crop the given image with the following arguemnts: 
 
 1) Input file (virtual path)
@@ -514,12 +521,50 @@ Crop the given image with the following arguemnts:
 6) Crop Height
 
 return true if success, false if failed
-*/
+```
+
+
+
+#### AI Classifier Options (since v1.119)
+
+**ImageLib AI Classifier requires darknet to operate normally. If your ArozOS is a trim down version or using a host architecture that ArozOS did not ship with a valid darknet binary executable in ```system/neuralnet/``` folder, this will always return```false```.**
+
+```
+Classify allow the following classifier options
+
+1) default / darknet19
+2) yolo3
+```
 
+The output of the classifier will output the followings
 
 ```
+Name (string, the name of object detected)
+Percentage (float, the confidence of detection)
+Positions (integer array, the pixel location of the detected object in left, top, width, height sequence)
+```
+
+Here is an example code for parsing the output, or you can also directly throw it into the JSON stringify and process it on frontend
+
+```javascript
+ var results = imagelib.classify("tmp:/classify.jpg"); 
+    var responses = [];
+    for (var i = 0; i < results.length; i++){
+        responses.push({
+            "object": results[i].Name,
+            "confidence": results[i].Percentage,
+            "position_x": results[i].Positions[0],
+            "position_y": results[i].Positions[1],
+            "width": results[i].Positions[2],
+            "height": results[i].Positions[3]
+        });
+    }
+```
+
+
 
 ### http
+
 A basic http function group that allow GET / POST / HEAD / Download request to other web resources
 
 ```

二进制
a.jpg


+ 64 - 0
legacy/WebMC/.gitignore

@@ -0,0 +1,64 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# TypeScript v1 declaration files
+typings/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+
+# next.js build output
+.next
+
+# IDEA
+.idea/

+ 18 - 0
legacy/WebMC/README.md

@@ -0,0 +1,18 @@
+# WebMC
+
+这是一个使用JavaScript编写的基于WebGL的网页版minecraft。
+
+> Any application that can be written in JavaScript, will eventually be written in JavaScript.  
+  --Jeff Atwood (co founder of Stack OverFlow)
+  >> 任何能够用 JavaScript 实现的应用,最终都必将用 JavaScript 实现。  
+     --Jeff Atwood(Stack OverFlow 的联合创始人)
+
+这就是网上著名的“[Atwood定律](https://blog.codinghorror.com/the-principle-of-least-power/)”,看到这个定律后,一直跃跃欲试,想用js实现下最喜欢的mc。从0开始制作,不使用任何第三方库。虽然很多地方会尝试重复造轮子,但我喜欢这种从零开始随心所欲创造的感觉。~~(虽然现在啥都没有实现)~~
+
+虽然知道这句话是从 _[the Principle of Least Power](https://www.w3.org/DesignIssues/Principles.html)_ 和[图灵完备](https://en.wikipedia.org/wiki/Turing_completeness)角度出发的 ~~(大概)~~,但咱还是要学PHPer来一句:JavaScript是世界上最好的语言。
+
+# How to Run
+
+需要运行在服务器上,因为有纹理图片的跨域问题。  
+若是有Node.js环境,那可以执行`node server.js`指令来运行一个简单的服务器,然后通过`http://localhost:3000/`进行访问。  
+若是你正在使用的IDE有自带的静态服务器功能,那可以通过IDE在浏览器中打开`/index.html`文件。

+ 67 - 0
legacy/WebMC/index.html

@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8"/>
+    <meta name="viewport" Content ="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
+    <title>WebMC</title>
+    <link id="favicon" rel="icon" href="#" type="image/x-icon" />
+    <link rel="stylesheet" type="text/css" href="./mc.css" />
+    <script src="./src/main.js" type="module"></script>
+    <style id="mcpage-preload">
+        mcpage-preload {
+            position: absolute; top: 0;
+            width: 100vw;
+            text-align: center;
+            height: 100vh;
+            overflow: hidden;
+            background: #1D1F21;
+            transition: opacity 1s ease;
+            z-index: 100000;
+        }
+        mcpage-preload p {
+            padding-top: calc(40vh + 1em);
+            margin-top: 0;
+            animation: loading-blink 1.3s infinite ease-out;
+        }
+        @keyframes loading-blink {
+            50% { color: transparent; }
+        }
+        mcpage-preload img {
+            position: fixed;
+            bottom: 60%; left: 50%;
+            transform: translateX(-50%);
+        }
+        mcpage-preload slot > ul, ::slotted(ul) {
+            list-style: none;
+            margin: 20px;
+            padding: 0;
+            white-space: nowrap;
+            text-overflow: ellipsis;
+            overflow: hidden;
+            line-height: 1.2em;
+        }
+        mcpage-preload .left {
+            text-align: start;
+            direction: rtl;
+            width: 50%;
+            float: left;
+        }
+        mcpage-preload .right {
+            text-align: start;
+            width: 50%;
+            float: right;
+        }
+    </style>
+</head>
+<body>
+    <mcpage-preload>
+        <img src="./texture/jumpingBlock.gif" />
+        <p>Loading...</p>
+        <div><progress value="0" max="0"></progress>&emsp;<span class="loadingCount">0</span> / <span class="loadedCount">0</span></div>
+        <div class="left"><slot name="loading"><ul></ul></slot></div>
+        <div class="right"><slot name="loaded"><ul></ul></slot></div>
+        <ul slot="loading"></ul>
+        <ul slot="loaded"></ul>
+    </mcpage-preload>
+</body>
+</html>

+ 11 - 0
legacy/WebMC/init.agi

@@ -0,0 +1,11 @@
+//Define the launchInfo for the module
+var moduleLaunchInfo = {
+    Name: "WebMC",
+	Group: "Network",
+	IconPath: "WebMC/texture/jumpingBlock.gif",
+	Version: "0.1",
+	StartDir: "WebMC/index.html"
+}
+
+//Register the module
+registerModule(JSON.stringify(moduleLaunchInfo));

+ 20 - 0
legacy/WebMC/mc.css

@@ -0,0 +1,20 @@
+@font-face {
+    font-family: mc-font;
+    src: url(./texture/mc-font.ttf);
+}
+body {
+    padding: 0;
+    margin: 0;
+    image-rendering: pixelated;
+    height: 100vh;
+    overflow: hidden;
+    background: #1D1F21;
+    color: #DDD;
+    -webkit-touch-callout:none;
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none;
+    font-family: mc-font;
+    -webkit-tap-highlight-color: transparent;
+}

+ 28 - 0
legacy/WebMC/server.js

@@ -0,0 +1,28 @@
+const http = require("http"),
+      fs   = require("fs"),
+      url  = require("url"),
+      path = require("path");
+let server = http.createServer(function(request, response) {
+    let pathObj = url.parse(request.url, true);
+    if (pathObj.pathname === "/" || pathObj === "/index")
+        pathObj.pathname = "/index.html";
+    let filePath = path.join(path.resolve(), pathObj.pathname);
+    let mime = ((ext = path.extname(filePath)) => {
+        let t = {".png": "image/png", ".js": "application/javascript", ".css": "text/css"};
+        return t[ext] || "text/html";
+    })();
+    fs.readFile(filePath, "binary", function(err, fileContent) {
+        if (err) {
+            console.log("404 " + filePath);
+            response.writeHead(404, "not found");
+            response.end("<h1>404 Not Found</h1>");
+        } else {
+            console.log("ok " + filePath);
+            response.setHeader("Content-Type", mime);
+            response.write(fileContent, "binary");
+            response.end();
+        }
+    });
+});
+server.listen(3000);
+console.log('visit http://localhost:3000');

+ 66 - 0
legacy/WebMC/src/Entity/Entity.js

@@ -0,0 +1,66 @@
+import { vec3 } from "../utils/gmath.js";
+
+class Entity {
+    constructor(hitboxes, {
+        eyePos = [
+            (hitboxes.max[0] - hitboxes.min[0] / 2),
+            (hitboxes.max[1] - hitboxes.min[1] / 2),
+            (hitboxes.max[2] - hitboxes.min[2] / 2)
+        ],
+        pitch = 0, yaw = 0,
+        position = [0, 0, 0],
+        world = null,
+        controller = null,
+        camera = null,
+    } = {}) {
+        this.moveSpeed = 0;
+        this.gravityAcceleration = 0;
+        this.position = vec3.create(...position);
+        this.pitch = pitch; // 垂直角 始于xz平面 以y+为正方向 弧度制
+        this.yaw = yaw;   // 水平角 始于z- 以x-为正方向 弧度制
+        this.hitboxes = hitboxes;
+        this.eyePos = vec3.create(...eyePos);
+        this.forward = vec3.create();
+        this.direction = vec3.create();
+        this.model = null;
+        this.setWorld(world);
+        this.setCamera(camera);
+        this.setController(controller);
+    };
+    getEyePosition() {
+        return vec3.add(this.eyePos, this.position);
+    };
+    getGloBox() {
+        return {
+            min: vec3.add(this.hitboxes.min, this.position),
+            max: vec3.add(this.hitboxes.max, this.position)
+        };
+    };
+    getDirection(scale = 1) {
+        vec3.create(0, 0, -scale, this.direction);
+        vec3.rotateX(this.direction, this.pitch, this.direction);
+        vec3.rotateY(this.direction, this.yaw, this.direction);
+        return this.direction;
+    };
+    setCamera(camera = null) {
+        if (camera === this.camera) return;
+        if (this.camera) this.camera.bindEntity(null);
+        this.camera = camera;
+        if (camera) camera.bindEntity(this);
+    };
+    setController(controller = null) {
+        if (controller === this.controller) return;
+        if (this.controller) this.controller.setEntity(null);
+        this.controller = controller;
+        if (controller) controller.setEntity(this);
+    };
+    setWorld(world = null) {
+        this.world = world;
+    };
+    update(dt) {};
+};
+
+export {
+    Entity,
+    Entity as default
+};

+ 20 - 0
legacy/WebMC/src/Entity/EntityController.js

@@ -0,0 +1,20 @@
+
+import { EventDispatcher } from "../utils/EventDispatcher.js";
+
+class EntityController extends EventDispatcher {
+    constructor(entity = null) {
+        super();
+        this.setEntity(entity);
+    };
+    setEntity(entity = null) {
+        if (entity === this.entity) return;
+        if (this.entity) this.entity.setController(this);
+        this.entity = entity;
+        if (entity) entity.setController(this);
+    };
+};
+
+export {
+    EntityController,
+    EntityController as default
+};

+ 147 - 0
legacy/WebMC/src/Entity/Player.js

@@ -0,0 +1,147 @@
+import Entity from "./Entity.js";
+import { vec3, vec2, EPSILON } from "../utils/gmath.js";
+import Block from "../World/Block.js";
+
+class Player extends Entity {
+    constructor(world = null, {
+        position = [0, 10, 0],
+        pitch = 0, yaw = 0,
+    } = {}) {
+        super({
+            min: [-0.25, 0, -0.25],
+            max: [0.25, 1.8, 0.25]
+        }, {eyePos: [0, 1.65, 0], position, pitch, yaw, world});
+
+        this.normalMoveSpeed = 4.317;
+        this.runMoveSpeed = 5.612;
+        this.flyMoveSpeed = 11;
+        this.flyRunMoveSpeed = 22;
+        this.moveSpeed = this.normalMoveSpeed;     // block / s
+        super.gravityAcceleration = 30; // block / s ^ 2
+        // h = v^2 / 2g = 1.25 -> v = √2gh
+        this.jumpSpeed = Math.sqrt(2 * this.gravityAcceleration * 1.25);
+        this.normalJumpSpeed = this.jumpSpeed;
+        this.flyJumpSpeed = 20;
+
+        this._horiMoveDir = vec2.create();
+        this.horiVelocity = vec2.create();
+        this.horiAcceleration = 0;
+        this._vertMoveDir = 0;
+        this.vertVelocity = 0;
+        this.vertAcceleration = -this.gravityAcceleration;
+
+        this.rest = vec3.create();
+        this.lastChunk = [];
+        this.isFly = false; this.isRun = false;
+
+        this._onHandItem = Block.getBlockByBlockName("air");
+        this._velocity = vec3.create();
+        this._acceleration = vec3.create();
+        this.eyeInFluid = false;
+    };
+    get onHandItem() { return this._onHandItem; };
+    set onHandItem(value) {
+        if (value instanceof Block)
+            this._onHandItem = value;
+        else if (typeof value === "string")
+            this._onHandItem = Block.getBlockByBlockName(value) || this._onHandItem;
+    };
+    get horiMoveDir() { return this._horiMoveDir; };
+    set horiMoveDir(arr) {
+        vec2.create(arr[0], arr[1], this._horiMoveDir);
+        vec2.normalize(this._horiMoveDir, this._horiMoveDir);
+    };
+    get vertMoveDir() { return this._vertMoveDir; };
+    set vertMoveDir(val) { this._vertMoveDir = val? val > 0? 1: -1: 0; };
+    get velocity() {
+        return vec3.create(this.horiVelocity[0], this.vertVelocity, this.horiVelocity[1], this._velocity);
+    };
+    get acceleration() {
+        return vec3.create(this.horiAcceleration, this.vertAcceleration, this.horiAcceleration, this._acceleration);
+    };
+    toFlyMode(fly = false) {
+        this.isFly = fly;
+        if (fly) {
+            this.vertAcceleration = 0;
+            this.moveSpeed = this.isRun? this.flyRunMoveSpeed: this.flyMoveSpeed;
+        }
+        else {
+            this.vertAcceleration = -this.gravityAcceleration;
+            this.moveSpeed = this.isRun? this.normalMoveSpeed: this.runMoveSpeed;
+        }
+    };
+    toRunMode(run = false) {
+        this.isRun = run;
+        if (run) {
+            this.moveSpeed = this.isFly? this.flyRunMoveSpeed: this.runMoveSpeed;
+            if (this.camera) {
+                this.camera.changeFovyWithAnimation(10);
+            }
+        }
+        else {
+            this.moveSpeed = this.isFly? this.flyMoveSpeed: this.normalMoveSpeed;
+            if (this.camera) {
+                this.camera.changeFovyWithAnimation(0);
+            }
+        }
+    };
+    get onGround() { return this.rest[1] === -1; };
+    move_and_collide(motion, dt) {
+        let ds = vec3.scale(motion, dt);
+        let chunkFn = (x, y, z) => {
+            let b = this.world.getBlock(x, y, z);
+            return b && b.name !== "air" && b.renderType !== Block.renderType.FLOWER && b.renderType !== Block.renderType.FLUID;
+        };
+        vec3.create(0, 0, 0, this.rest);
+        for (let i = 0, dSpatium = vec3.create(); i < 3; dSpatium[i++] = 0) {
+            dSpatium[i] = ds[i];
+            let hit = this.world.hitboxesCollision(this.getGloBox(), dSpatium, chunkFn);
+            while (hit) {
+                this.position[hit.axis] = hit.pos - (hit.step > 0
+                    ? this.hitboxes.max[hit.axis]: this.hitboxes.min[hit.axis]);
+                this.rest[hit.axis] = hit.step;
+                motion[hit.axis] = ds[hit.axis] = dSpatium[hit.axis] = 0;
+                if (hit.axis !== 1) this.toRunMode(false);
+                hit = this.world.hitboxesCollision(this.getGloBox(), dSpatium, chunkFn);
+            }
+        }
+        this.horiVelocity.set([motion[0], motion[2]]);
+        this.vertVelocity = motion[1];
+        vec3.add(this.position, ds, this.position);
+        this.eyeInFluid = this.world.getBlock(...this.getEyePosition())?.isFluid ?? false;
+    };
+    update(dt) {
+        if (!this.world) return;
+        dt /= 1000;
+        if (this.isFly) {
+            this.vertVelocity = this.vertMoveDir * this.flyJumpSpeed;
+            this.vertVelocity += this.vertAcceleration * dt;
+        }
+        else {
+            if (this.onGround)
+                this.vertVelocity = Math.max(0, this.vertMoveDir) * this.jumpSpeed;
+            this.vertVelocity += this.vertAcceleration * dt;
+        }
+        if (this.isFly) {
+            this.horiAcceleration = 12;
+        }
+        else {
+            let pos = this.position,
+                block = this.world.getBlock(pos[0], pos[1] - 1, pos[2]),
+                blockFriction = block? (block.friction || 1): 1;
+            this.horiAcceleration = blockFriction * 20;
+        }
+        let horiVel = vec2.create();
+        if (vec2.length(this.horiMoveDir) > EPSILON) {
+            let deltaYaw = vec2.angle(vec2.create(0, 1, horiVel), this.horiMoveDir);
+            vec2.rotateOrigin(vec2.create(0, -this.moveSpeed, horiVel), -(this.yaw + deltaYaw), horiVel);
+        }
+        vec2.move_toward(this.horiVelocity, horiVel, this.horiAcceleration * dt, this.horiVelocity);
+        this.move_and_collide(this.velocity, dt);
+    };
+};
+
+export {
+    Player,
+    Player as default
+};

+ 361 - 0
legacy/WebMC/src/Entity/PlayerLocalController.js

@@ -0,0 +1,361 @@
+
+import { EntityController } from "./EntityController.js";
+import { Block } from "../World/Block.js";
+import { vec3 } from "../utils/gmath.js";
+import { pm } from "../UI/Page.js";
+
+class PlayerLocalController extends EntityController {
+    constructor(player = null, {
+        playPage = pm.getPageByID("play"),
+        canvas = playPage? playPage.mainCanvas: null,
+        moveButtons = playPage? playPage.moveButtons: null,
+        hotbarUI = playPage? playPage.hotbar: null,
+        mousemoveSensitivity = 200,
+    } = {}) {
+        super(player);
+        this.playPage = playPage;
+        this.hotbarUI = hotbarUI;
+        this.mousemoveSensitivity = mousemoveSensitivity;
+        this.eventHandler = this.eventHandler.bind(this);
+        this["pause=>play"] = this.requestPointerLock.bind(this);
+        this["play=>pause"] = this.exitPointerLock.bind(this);
+        this._locked = false;
+        this.showStopPage = false;
+        this.canvasLastTouchPos = this.canvasBeginTouch = this.canvasTouchTimer = null;
+        this.canvasTouchMoveLen = 0;
+        this.canvasDestroying = false;
+        this.keys = [];
+        this.setCanvas(canvas);
+        this.setMoveBtns(moveButtons);
+
+        this.hotbar = [];
+        const listBlocks = Block.listBlocks();
+        this.inventoryStore = listBlocks;
+        for (let b of listBlocks)
+            playPage.inventory.appendItem(b);
+        for (let i = 0; i < hotbarUI.length; ++i) {
+            this.hotbar.push(listBlocks[i]);
+            hotbarUI.setItem(listBlocks[i], i);
+        }
+        hotbarUI.addEventListener("selectBlock", e => {
+            this.entity.onHandItem = e.detail || Block.getBlockByBlockName("air");
+        });
+        playPage.addEventListener("closeInventory", e => {
+            this.requestPointerLock();
+        });
+        playPage.addEventListener("showInventory", e => {
+            this.exitPointerLock();
+        });
+    };
+    dispose() {
+        this.setEntity();
+        this.setCanvas();
+        this.setMoveBtns();
+    };
+    get locked() { return window.isTouchDevice || this._locked; };
+    setCanvas(canvas = null) {
+        if (this.canvas) {
+            this.exitPointerLock();
+            for (let eventType of ["keydown", "keyup", "pointerlockchange", ])
+                this.docOfCanvas.removeEventListener(eventType, this.eventHandler);
+            for (let eventType of ["mousedown", "mouseup", "mousemove", "wheel", "touchstart", "touchend", "touchcancel", "touchmove", ])
+                this.canvas.removeEventListener(eventType, this.eventHandler);
+            if (this.canvasTouchTimer !== null) window.clearTimeout(this.canvasTouchTimer);
+            this.canvasTouchTimer = null;
+            pm.removeEventListener("pause=>play", this["pause=>play"]);
+            pm.removeEventListener("play=>pause", this["play=>pause"]);
+        }
+        this.canvas = canvas; this.rootOfCanvas = this.docOfCanvas = null;
+        if (!canvas) return;
+        this.docOfCanvas = canvas.ownerDocument;
+        this.rootOfCanvas = canvas.getRootNode();
+        for (let eventType of ["keydown", "keyup", "pointerlockchange", ])
+            this.docOfCanvas.addEventListener(eventType, this.eventHandler);
+        for (let eventType of ["mousedown", "mouseup", "mousemove", "wheel", ])
+            canvas.addEventListener(eventType, this.eventHandler, { passive: true, });
+        if (window.isTouchDevice)
+            for (let eventType of ["touchstart", "touchend", "touchcancel", "touchmove", ])
+                canvas.addEventListener(eventType, this.eventHandler);
+        pm.addEventListener("pause=>play", this["pause=>play"]);
+        pm.addEventListener("play=>pause", this["play=>pause"]);
+    };
+    setMoveBtns(moveBtns = null) {
+        if (this.moveBtns) {
+            for (let btn of "up,left,down,right,jump,upleft,upright,flyup,flydown,fly,sneak".split(",")) {
+                this.moveBtns.removeEventListener(btn + "BtnPress", this.eventHandler);
+                this.moveBtns.removeEventListener(btn + "BtnUp", this.eventHandler);
+            }
+            this.moveBtns.removeEventListener("flyBtnDblPress", this.eventHandler);
+        }
+        this.moveBtns = moveBtns;
+        if (!moveBtns) return;
+        for (let [btn, keys] of [
+            ["up", ["w"]],
+            ["left", ["a"]],
+            ["down", ["s"]],
+            ["right", ["d"]],
+            ["jump", [" "]],
+            ["upleft", ["w", "a"]],
+            ["upright", ["w", "d"]],
+            ["flyup", [" "]],
+            ["flydown", ["Shift"]],
+            // ["fly"],
+            // ["sneak"],
+        ]) {
+            this[btn + "BtnPress"] = () => {
+                keys.forEach(key => this.dispatchKeyEvent("down", key));
+            };
+            this[btn + "BtnUp"] = () => {
+                for (let i = keys.length - 1; i >= 0; --i)
+                    this.dispatchKeyEvent("up", keys[i]);
+            };
+            this.moveBtns.addEventListener(btn + "BtnPress", this.eventHandler);
+            this.moveBtns.addEventListener(btn + "BtnUp", this.eventHandler);
+        }
+        this["flyBtnDblPress"] = () => {
+            this.entity.toFlyMode && this.entity.toFlyMode(false);
+            const {moveBtns} = this;
+            moveBtns.activeFlyBtn(false);
+        };
+        this.moveBtns.addEventListener("flyBtnDblPress", this.eventHandler);
+    };
+    requestPointerLock() {
+        if (!this.canvas || window.isTouchDevice) return;
+        this.canvas.requestPointerLock();
+    };
+    exitPointerLock() {
+        if (this.canvas === null || window.isTouchDevice) return;
+        this.docOfCanvas.exitPointerLock();
+    };
+    eventHandler(event) {
+        if (!this.entity) return;
+        const { type } = event;
+        if (type in this) this[type](event);
+        this.dispatchEvent(type, event);
+    };
+    _setEntityPitchAndYaw(movementX, movementY) {
+        let i = this.mousemoveSensitivity * (Math.PI / 180);
+        // movementX left- right+    movementY up- down+
+        this.entity.yaw -= movementX * i / this.canvas.width;
+        this.entity.pitch -= movementY * i / this.canvas.height;
+        if (this.entity.pitch > Math.PI / 2)
+            this.entity.pitch = Math.PI / 2;
+        else if (this.entity.pitch < -Math.PI / 2)
+            this.entity.pitch = -Math.PI / 2;
+    };
+    mousemove(e) {
+        if (!this.locked) return;
+        this._setEntityPitchAndYaw(
+            (e.movementX || e.mozMovementX || e.webkitMovementX || 0),
+            (e.movementY || e.mozMovementY || e.webkitMovementY || 0)
+        );
+    };
+    mousedown(e) {
+        if (!this.locked) {
+            this.requestPointerLock();
+            return;
+        }
+        if (e.button !== 0 && e.button !== 2) return;
+        if (e.button === 0) this.mouseRightBtnDown = true;
+        if (e.button === 2) this.mouseLeftBtnDown = true;
+        const destroyOrPlaceBlock = () => {
+            let entity = this.entity,
+                world = entity.world,
+                start = entity.getEyePosition(),
+                end = entity.getDirection(20);
+            vec3.add(start, end, end);
+            let hit = world.rayTraceBlock(start, end, (x, y, z) => {
+                let b = world.getBlock(x, y, z);
+                return b && b.name !== "air";
+            });
+            if (hit === null || hit.axis === "") return;
+            let pos = hit.blockPos;
+            if (this.mouseLeftBtnDown) {
+                pos["xyz".indexOf(hit.axis[0])] += hit.axis[1] === '-'? -1: 1;
+                let box = this.entity.getGloBox();
+                box.min = box.min.map(n => Math.floor(n));
+                box.max = box.max.map(n => Math.ceil(n));
+                if (pos[0] >= box.min[0] && pos[1] >= box.min[1] && pos[2] >= box.min[2]
+                && pos[0] < box.max[0] && pos[1] < box.max[1] && pos[2] < box.max[2])
+                    return;
+                let blockName = this.entity.onHandItem.name;
+                if (blockName !== "air") world.setBlock(...pos, blockName);
+            }
+            else if (this.mouseRightBtnDown) {
+                world.setBlock(...pos, "air");
+            }
+        };
+        destroyOrPlaceBlock();
+        if (this.destroyOrPlaceBlockTimer !== null)
+            window.clearInterval(this.destroyOrPlaceBlockTimer);
+        this.destroyOrPlaceBlockTimer = window.setInterval(destroyOrPlaceBlock, 300);
+    };
+    mouseup(e) {
+        if (!this.locked) return;
+        if (e.button === 0) this.mouseRightBtnDown = false;
+        if (e.button === 2) this.mouseLeftBtnDown = false;
+        if (!(this.mouseRightBtnDown || this.mouseLeftBtnDown) && this.destroyOrPlaceBlockTimer !== null) {
+            window.clearInterval(this.destroyOrPlaceBlockTimer);
+            this.destroyOrPlaceBlockTimer = null;
+        }
+    };
+    keydown(e) {
+        if (!this.locked) return;
+        if (e.cancelable) e.preventDefault();
+        if (e.repeat) return;
+        if (e.key == 'E' || e.key == 'e') {
+            if (this.locked) {
+                this.showStopPage = false;
+                this.playPage.showInventory();
+            }
+            else this.playPage.closeInventory();
+        }
+        if (window.isTouchDevice) {
+            if (e.repeat !== true) {
+                if (e.keyCode) this.keys[e.keyCode] = (this.keys[e.keyCode] || 0) + 1;
+                this.keys[e.key] = this.keys[e.code] = (this.keys[e.key] || 0) + 1;
+            }
+        }
+        else {
+            if (e.keyCode) this.keys[e.keyCode] = true;
+            this.keys[e.key] = this.keys[e.code] = true;
+        }
+        if (e.key == ' ') {
+            if (this.spaceDownTime === 0)
+                this.spaceDownTime = new Date();
+            else if ((new Date()) - this.spaceDownTime < 300) {
+                this.entity.toFlyMode && this.entity.toFlyMode(!this.entity.isFly);
+                this.moveBtns.activeFlyBtn(this.entity.isFly);
+                this.spaceDownTime = 0;
+            }
+            else this.spaceDownTime = new Date();
+        }
+        if (e.code == "KeyW") {
+            if (this.moveDownTime === 0)
+                this.moveDownTime = new Date();
+            else if ((new Date()) - this.moveDownTime < 300) {
+                this.entity.toRunMode && this.entity.toRunMode(!this.entity.isRun);
+                this.moveDownTime = 0;
+            }
+            else this.moveDownTime = new Date();
+        }
+        this.entity.vertMoveDir = (this.keys.Space || 0) - (this.keys.Shift || this.keys.KeyX || 0);
+        this.entity.horiMoveDir = [
+            (this.keys.KeyD || 0) - (this.keys.KeyA || 0),
+            (this.keys.KeyW || 0) - (this.keys.KeyS || 0),
+        ];
+    };
+    keyup(e) {
+        if (!this.locked) return;
+        if (e.cancelable) e.preventDefault();
+        if (window.isTouchDevice) {
+            if (e.keyCode) this.keys[e.keyCode] = (this.keys[e.keyCode] || 1) - 1;
+            this.keys[e.key] = this.keys[e.code] = (this.keys[e.key] || 1) - 1;
+        }
+        else {
+            if (e.keyCode) this.keys[e.keyCode] = false;
+            this.keys[e.key] = this.keys[e.code] = false;
+        }
+        if (!this.keys.KeyW) {
+            this.entity.toRunMode && this.entity.toRunMode(false);
+        }
+        this.entity.vertMoveDir = (this.keys.Space || 0) - (this.keys.Shift || this.keys.KeyX || 0);
+        this.entity.horiMoveDir = [
+            (this.keys.KeyD || 0) - (this.keys.KeyA || 0),
+            (this.keys.KeyW || 0) - (this.keys.KeyS || 0),
+        ];
+    };
+    wheel(e) {
+        if (!this.locked) return;
+        const t = new Date();
+        if (t - this.lastWeelTime < 5) return;
+        if (e.deltaY < 0) {
+            // wheelup
+            this.hotbarUI.selectNext();
+        }
+        else if (e.deltaY > 0) {
+            // wheeldown
+            this.hotbarUI.selectPrev();
+        }
+        this.lastWeelTime = t;
+    };
+    pointerlockchange(e) {
+        let locked = this.rootOfCanvas.pointerLockElement === this.canvas;
+        if (this.locked === locked) return;
+        if (!locked && this.showStopPage) {
+            pm.openPageByID("pause");
+        }
+        this.showStopPage = true;
+        this._locked = locked;
+    };
+    dispatchMouseEventByTouchEvt(type, touchEvt, {
+        button = 0, buttons = button,
+        movementX = 0, movementY = 0
+    } = {}) {
+        this.canvas.dispatchEvent(new MouseEvent("mouse" + type, {
+            bubbles: true, cancelable: true, relatedTarget: this.canvas,
+            screenX: touchEvt.changedTouches[0].screenX, screenY: touchEvt.changedTouches[0].screenY,
+            clientX: touchEvt.changedTouches[0].clientX, clientY: touchEvt.changedTouches[0].clientY,
+            ...(type !== "move"? {button, buttons,}: {movementX, movementY,}),
+        }));
+    };
+    touchstart(e) {
+        if (e.cancelable) e.preventDefault();
+        this.canvasLastTouchPos = this.canvasBeginTouch = e;
+        this.canvasTouchMoveLen = 0;
+        this.canvasDestroying = false;
+        if (this.canvasTouchTimer !== null) window.clearTimeout(this.canvasTouchTimer);
+        this.canvasTouchTimer = window.setTimeout(() => {
+            if (this.canvasTouchMoveLen < 10) {
+                this.canvasDestroying = true;
+                this.dispatchMouseEventByTouchEvt("down", this.canvasLastTouchPos);
+            }
+            this.canvasTouchTimer = null;
+        }, 300);
+    };
+    get touchcancel() { return this.touchend; };
+    touchend(e) {
+        if (e.cancelable) e.preventDefault();
+        if (e.timeStamp - this.canvasBeginTouch.timeStamp < 150) {
+            this.dispatchMouseEventByTouchEvt("down", e, {button: 2});
+            this.dispatchMouseEventByTouchEvt("up", e, {button: 2});
+        }
+        else if (this.canvasDestroying) {
+            this.dispatchMouseEventByTouchEvt("up", e);
+        }
+        if (this.canvasTouchTimer !== null) window.clearTimeout(this.canvasTouchTimer);
+        this.canvasLastTouchPos = null;
+    };
+    touchmove(e) {
+        if (e.cancelable) e.preventDefault();
+        if (!this.canvasLastTouchPos) {
+            this.canvasLastTouchPos = e;
+            return;
+        }
+        let movementX = e.targetTouches[0].screenX - this.canvasLastTouchPos.targetTouches[0].screenX,
+            movementY = e.targetTouches[0].screenY - this.canvasLastTouchPos.targetTouches[0].screenY;
+        this.canvasTouchMoveLen += Math.sqrt(movementX ** 2 + movementY ** 2);
+        let ratio = this.canvas.width / this.canvas.height;
+        movementX *= ratio * 2.5;
+        movementY *= 2.5;
+        this._setEntityPitchAndYaw(movementX,movementY);
+        this.canvasLastTouchPos = e;
+    };
+    dispatchKeyEvent(type, key,
+        code = {" ": "Space", "Shift": "ShiftLeft"}[key] || "Key" + key.toUpperCase(),
+        keyCode = key == "Shift"? 16: key.toUpperCase().charCodeAt(0),
+        repeat = false
+    ) {
+        this.docOfCanvas.dispatchEvent(new KeyboardEvent("key" + type, {
+            bubbles: true, cancelable: true,
+            key, code, keyCode, repeat, which: keyCode,
+        }));
+    };
+};
+
+
+
+export {
+    PlayerLocalController,
+    PlayerLocalController as default,
+};

+ 158 - 0
legacy/WebMC/src/Renderer/Camera.js

@@ -0,0 +1,158 @@
+import {vec3, mat4} from "../utils/gmath.js";
+
+class Camera {
+    static get projectionType() {
+        return {
+            perspective: "perspective",
+            ortho: "orthographic",
+        };
+    };
+    static get viewType() {
+        return {
+            fps: "fpsView",
+            lookAt: "lookAt",
+        };
+    };
+    constructor(aspectRatio, {
+        projectionType = Camera.projectionType.perspective,
+        viewType = Camera.viewType.fps,
+        fovy = 90, near = 0.1, far = 256,
+        left = -1, right = 1, bottom = -1, top = 1,
+        position = [0, 0, 3], pitch = 0, yaw = 0, rollZ = 0,
+        target = [0, 0, 0], up = [0, 1, 0],
+        entity = null,
+    } = {}) {
+        this.projectionType = projectionType;
+        this.viewType = viewType;
+        this.aspectRatio = aspectRatio;
+        this.fovy = this.nowFovy = fovy; this.near = near; this.far = far;
+        this.left = left; this.right = right; this.bottom = bottom; this.top = top;
+        this.position = vec3.create(...position);
+        this.pitch = pitch; this.yaw = yaw; this.rollZ = rollZ;
+        this.target = target; this.up = up;
+        switch (viewType) {
+        case Camera.viewType.fps:
+            this.vM = mat4.fpsView(this.position, pitch, yaw, rollZ);
+            break;
+        case Camera.viewType.lookAt:
+            this.vM = mat4.lookAt(this.position, target, up);
+            break;
+        default:
+            throw "Unrecognized view type";
+        }
+        switch (projectionType) {
+        case Camera.projectionType.perspective:
+            this.pM = mat4.perspective(this.fovy, this.aspectRatio, this.near, this.far);
+            break;
+        case Camera.projectionType.ortho:
+            this.pM = mat4.ortho(this.left, this.right, this.bottom, this.top, this.near, this.far);
+            break;
+        default:
+            throw "Unrecognized projection type";
+        }
+        this.pvM = mat4.multiply(this.pM, this.vM);
+        this.pChange = this.vChange = false;
+        this.bindEntity(entity);
+    };
+    setPos(pos) { this.position = vec3.create(...pos); this.vChange = true; return this; };
+    setPitch(pitch) { this.pitch = pitch; this.vChange = true; return this; };
+    setYaw(yaw) { this.yaw = yaw; this.vChange = true; return this; };
+    setRollZ(z) { this.rollZ = z; this.vChange = true; return this; };
+    setTarget(target) { this.target = target; this.vChange = true; return this; }
+    setUp(up) { this.up = up; this.vChange = true; return this; };
+
+    setFovy(fovy) { if (fovy == this.fovy) return this; this.fovy = this.nowFovy = fovy; this.pChange = true; return this; };
+    setNear(near) { this.near = near; this.pChange = true; return this; };
+    setFar(far) { this.far = far; this.pChange = true; return this; };
+    setAspectRatio(aspectRatio) { this.aspectRatio = aspectRatio; this.pChange = true; return this; };
+    setLeft(left) { this.left = left; this.pChange = true; return this; };
+    setRight(right) { this.right = right; this.pChange = true; return this; };
+    setBottom(bottom) { this.bottom = bottom; this.pChange = true; return this; };
+    setTop(top) { this.top = top; this.pChange = true; return this; };
+
+    _linearGradient(sx, ex, sy, ey, x) {
+        x = Math.max(0, Math.min((x - sx) / (ex - sx), 1));
+        return sy > ey
+            ? sy - (sy - ey) * x
+            : sy + (ey - sy) * x;
+    };
+    _powerGradient(sx, ex, sy, ey, x) {
+        x = Math.max(0, Math.min((x - sx) / (ex - sx), 1));
+        return sy > ey
+            ? ey + (sy - ey) * ((-x + 1) ** 2)
+            : sy + (ey - sy) * (x ** 0.5);
+    };
+    changeFovyWithAnimation(deltaFovy = 0, deltaTime = 250) {
+        if (deltaTime == 0) return this.setFovy(this.fovy + deltaFovy);
+        if (this.fovy + deltaFovy === this.endFovy) return this;
+        let now = this.nowTime = (new Date()).getTime();
+        if (!("beginFovy" in this)) {
+            this.endFovy = this.nowFovy = this.fovy;
+            this.fovyAnimationEndTime = now;
+        }
+        if (this.fovyAnimationEndTime <= now) {
+            this.fovyAnimationBeginTime = now;
+            this.fovyAnimationEndTime = now + deltaTime;
+        }
+        else {
+            this.fovyAnimationEndTime = now + (now - this.fovyAnimationBeginTime);
+            this.fovyAnimationBeginTime = this.fovyAnimationEndTime - deltaTime;
+        }
+        this.beginFovy = this.endFovy;
+        this.endFovy = this.fovy + deltaFovy;
+        return this;
+    };
+
+    get projection() {
+        if (this.pChange) {
+            this.pChange = false;
+            return this.projectionType === Camera.projectionType.perspective
+                ? mat4.perspective(this.nowFovy, this.aspectRatio, this.near, this.far, this.pM)
+                : mat4.ortho(this.left, this.right, this.bottom, this.top, this.near, this.far, this.pM);
+        }
+        return this.pM;
+    };
+    get view() {
+        if (this.entity) {
+            let e = this.entity,
+                pos = e.getEyePosition();
+            if (this.pitch == e.pitch && this.yaw == e.yaw && vec3.exactEquals(pos, e.position))
+                return this.vM;
+            vec3.create(...pos, this.position);
+            this.pitch = e.pitch; this.yaw = e.yaw;
+            return mat4.fpsView(this.position, this.pitch, this.yaw, this.rollZ, this.vM);
+        }
+        if (this.vChange) {
+            this.vChange = false;
+            return this.viewType === Camera.viewType.fps
+                ? mat4.fpsView(this.position, this.pitch, this.yaw, this.rollZ, this.vM)
+                : mat4.lookAt(this.position, this.target, this.up, this.vM);
+        }
+        return this.vM;
+    };
+    get projview() {
+        let now = (new Date()).getTime();
+        // 60fps -> 0.0166spf -> 16 mspf
+        if (this.nowFovy != this.endFovy && now - this.nowTime > 16) {
+            this.nowTime = now;
+            this.pChange = true;
+            this.nowFovy = this._powerGradient(
+                this.fovyAnimationBeginTime, this.fovyAnimationEndTime,
+                this.beginFovy, this.endFovy, now);
+        }
+        if (this.entity || this.pChange || this.vChange)
+            return mat4.multiply(this.projection, this.view, this.pvM);
+        return this.pvM;
+    };
+    bindEntity(entity = null) {
+        if (entity === this.entity) return;
+        if (this.entity) this.entity.setCamera(null);
+        this.entity = entity;
+        if (entity) entity.setCamera(this);
+    };
+};
+
+export {
+    Camera,
+    Camera as default
+};

+ 136 - 0
legacy/WebMC/src/Renderer/HighlightSelectedBlock.js

@@ -0,0 +1,136 @@
+import { Block } from "../World/Block.js";
+import { vec3, mat4 } from "../utils/gmath.js";
+
+class HighlightSelectedBlock {
+    constructor(world, renderer = world.renderer) {
+        this.world = world;
+        this.renderer = renderer;
+        this.mvp = mat4.identity();
+        const {ctx} = renderer;
+        this.meshs = new Map();
+        for (let renderType of Object.values(Block.renderType)) {
+            let isFluid = renderType === Block.renderType.FLUID;
+            if (isFluid) renderType = Block.renderType.NORMAL;
+            let blockEles = Block.getElementsByRenderType(renderType);
+            let lineVer = [], vers = Block.getVertexsByRenderType(renderType), surfaceMesh = {};
+            for (let f in vers) {
+                if (renderType !== Block.renderType.CACTUS || (f != "y+" && f != "y-"))
+                    lineVer.push(...vers[f]);
+                surfaceMesh[f] = {
+                    ver: renderer.createVbo(vers[f]),
+                    ele: renderer.createIbo(blockEles[f]),
+                    col: renderer.createVbo([], ctx.DYNAMIC_DRAW),
+                };
+            }
+            let lineEle = (len => {
+                if (!len) return [];
+                let base = [0,1, 1,2, 2,3, 3,0], out = [];
+                for(let i = 0, j = 0; i < len; j = ++i*4)
+                    out.push(...base.map(x => x + j));
+                return out;
+            })(lineVer.length / 12);
+            let defaultCol = [...Array(lineVer.length / 3 * 4)].map((_, i) => i % 4 === 3? 0.5: 1.0);
+            if (isFluid) renderType = Block.renderType.FLUID;
+            this.meshs.set(renderType, {
+                line: {
+                    ver: renderer.createVbo(lineVer),
+                    ele: renderer.createIbo(lineEle),
+                    defaultCol: renderer.createVbo(defaultCol),
+                    col: renderer.createVbo([], ctx.DYNAMIC_DRAW),
+                },
+                surface: surfaceMesh,
+            });
+        }
+    };
+    draw() {
+        const {world} = this, {mainPlayer} = world;
+        if (mainPlayer.camera === null) return;
+        let start = mainPlayer.getEyePosition(),
+            end = mainPlayer.getDirection(20);
+        vec3.add(start, end, end);
+        let hit = world.rayTraceBlock(start, end, (x, y, z) => {
+            let b = world.getBlock(x, y, z);
+            return b && b.name !== "air";
+        });
+        if (hit === null) return;
+
+        const {renderer} = this, {ctx} = renderer;
+        let [bx, by, bz] = hit.blockPos,
+            block = world.getBlock(bx, by, bz),
+            selector = renderer.getProgram("selector").use(),
+            linecol = [], surfaceCol = [];
+        mat4.identity(this.mvp);
+        mat4.translate(this.mvp, hit.blockPos, this.mvp);
+        mat4.multiply(mainPlayer.camera.projview, this.mvp, this.mvp);
+        selector.setUni("mvp", this.mvp);
+        let mesh = this.meshs.get(block.renderType), lineMesh = mesh.line, surfaceMeshs = mesh.surface;
+        switch (block.renderType) {
+        case Block.renderType.FLUID:
+        case Block.renderType.CACTUS:
+        case Block.renderType.NORMAL: {
+            if (!hit.axis) break;
+            let [dx, dy, dz] = ({"x+":[1,0,0], "x-":[-1,0,0], "y+":[0,1,0], "y-":[0,-1,0], "z+":[0,0,1], "z-":[0,0,-1]})[hit.axis];
+            let l = world.getLight(bx + dx, by + dy, bz + dz);
+            linecol = [...Array(lineMesh.ver.length / 3 * 4)]
+                .map((_, i) => i % 4 === 3? 0.4: Math.min(1, Math.pow(0.9, 15 - l) + 0.1));
+            surfaceCol = [...Array(surfaceMeshs[hit.axis].ver.length / 3 * 4)]
+                .map((_, i) => i % 4 === 3? 0.1: Math.min(1, Math.pow(0.9, 15 - l) + 0.1));
+            break;}
+        case Block.renderType.FLOWER: {
+            let l = world.getLight(bx, by, bz);
+            linecol = [...Array(lineMesh.ver.length / 3 * 4)]
+                .map((_, i) => i % 4 === 3? 0.4: Math.min(1, Math.pow(0.9, 15 - l) + 0.1));
+            surfaceCol = [...Array(surfaceMeshs.face.ver.length / 3 * 4)]
+                .map((_, i) => i % 4 === 3? 0.1: Math.min(1, Math.pow(0.9, 15 - l) + 0.1));
+            break;}
+        }
+        let lineColBO = lineMesh.defaultCol;
+        if (linecol.length) {
+            lineColBO = lineMesh.col;
+            renderer.bindBoData(lineColBO, linecol, {drawType: ctx.DYNAMIC_DRAW});
+        }
+        // draw line
+        ctx.enable(ctx.BLEND);
+        ctx.blendFunc(ctx.SRC_ALPHA, ctx.ONE_MINUS_SRC_ALPHA);
+        ctx.bindBuffer(lineMesh.ele.type, lineMesh.ele);
+        selector.setAtt("pos", lineMesh.ver).setAtt("col", lineColBO);
+        ctx.drawElements(ctx.LINES, lineMesh.ele.length, ctx.UNSIGNED_SHORT, 0);
+        ctx.disable(ctx.BLEND);
+        
+        if (!hit.axis) return;
+        let surfaceMesh = block.renderType === Block.renderType.FLOWER
+                ? surfaceMeshs.face
+                : surfaceMeshs[hit.axis],
+            surfaceColBO = surfaceMesh.col;
+        renderer.bindBoData(surfaceColBO, surfaceCol, {drawType: ctx.DYNAMIC_DRAW});
+        // draw surface
+        ctx.disable(ctx.CULL_FACE);
+        ctx.enable(ctx.BLEND);
+        ctx.blendFunc(ctx.SRC_ALPHA, ctx.ONE_MINUS_SRC_ALPHA);
+        ctx.enable(ctx.POLYGON_OFFSET_FILL);
+        ctx.polygonOffset(-1.0, -1.0);
+        ctx.depthMask(false);
+        ctx.bindBuffer(surfaceMesh.ele.type, surfaceMesh.ele);
+        selector.setAtt("pos", surfaceMesh.ver).setAtt("col", surfaceColBO);
+        ctx.drawElements(ctx.TRIANGLES, surfaceMesh.ele.length, ctx.UNSIGNED_SHORT, 0);
+        ctx.depthMask(true);
+        ctx.disable(ctx.POLYGON_OFFSET_FILL);
+        ctx.disable(ctx.BLEND);
+        ctx.enable(ctx.CULL_FACE);
+    };
+    dispose() {
+        const {ctx} = this.renderer;
+        for (let [, mesh] of this.meshs) {
+            for (let k of ["ver", "ele", "col"]) {
+                ctx.deleteBuffer(mesh.line[k]);
+                Object.values(mesh.surface).forEach(surfaceMesh => ctx.deleteBuffer(surfaceMesh[k]));
+            }
+            ctx.deleteBuffer(mesh.line.defaultCol);
+        }
+    };
+};
+
+export {
+    HighlightSelectedBlock,
+    HighlightSelectedBlock as default,
+};

+ 133 - 0
legacy/WebMC/src/Renderer/Program.js

@@ -0,0 +1,133 @@
+
+function createShader(ctx, type, src) {
+    const s = ctx.createShader(type);
+    ctx.shaderSource(s, src);
+    ctx.compileShader(s);
+    if (!ctx.getShaderParameter(s, ctx.COMPILE_STATUS))
+        throw "error compile " + (type === ctx.VERTEX_SHADER? "vertex": "fragment") + " shader: "
+            + ctx.getShaderInfoLog(s);
+    return s;
+}
+
+function createProgram(ctx, vertShader, fragShader) {
+    const p = ctx.createProgram();
+    ctx.attachShader(p, vertShader);
+    ctx.attachShader(p, fragShader);
+    ctx.linkProgram(p);
+    if (!ctx.getProgramParameter(p, ctx.LINK_STATUS))
+        throw "error link program: " + ctx.getProgramInfoLog(p);
+    return p;
+}
+
+class Program {
+    constructor(ctx, vertSrc, fragSrc) {
+        this.ctx = this.gl = ctx;
+        let vs = createShader(ctx, ctx.VERTEX_SHADER, vertSrc),
+            fs = createShader(ctx, ctx.FRAGMENT_SHADER, fragSrc);
+        this.shaders = { vs, fs };
+        let prog = this.prog = this.program = createProgram(ctx, vs, fs);
+        const getCurrentVars = (varsType, aou = varsType === ctx.ACTIVE_ATTRIBUTES? "Attrib": "Uniform") =>
+            [...Array(ctx.getProgramParameter(prog, varsType))]
+            .map((_, i) => {
+                const {size, type, name} = ctx["getActive" + aou](prog, i),
+                      loc                = ctx[`get${aou}Location`](prog, name);
+                return {size, type, name: name.split("[")[0], loc};
+            })
+            .reduce((ac, {name, size, type, loc}) => {
+                ac[name] = {name, size, type, loc};
+                return ac;
+            }, {});
+        this.vars = {
+            atts: getCurrentVars(ctx.ACTIVE_ATTRIBUTES),
+            unis: getCurrentVars(ctx.ACTIVE_UNIFORMS)
+        };
+    };
+    use() { this.ctx.useProgram(this.prog); return this; };
+    getAtt(name) { return this.vars.atts[name].loc; };
+    getUni(name) { return this.vars.unis[name].loc; };
+    setAtt(name, bufferData, size = undefined, attDataType = this.ctx.FLOAT, normalized = false, stride = 0, offset = 0) {
+        const ctx = this.ctx, att = this.vars.atts[name];
+        if (!att) throw "Cannot get attribute " + name;
+        if (size === undefined) {
+            switch (att.type) {
+                case ctx.FLOAT:
+                    size = 1; break;
+                case ctx.FLOAT_VEC2: case ctx.FLOAT_MAT2:
+                    size = 2; break;
+                case ctx.FLOAT_VEC3: case ctx.FLOAT_MAT3:
+                    size = 3; break;
+                case ctx.FLOAT_VEC4: case ctx.FLOAT_MAT4:
+                    size = 4; break;
+                default:
+                    console.error("Don't know gl type", att.type, "for attribute", att.name);
+                    throw "Don't know attribute type";
+            }
+        }
+        let bufferType = bufferData.type || ctx.ARRAY_BUFFER;
+        ctx.bindBuffer(bufferType, bufferData);
+        ctx.enableVertexAttribArray(att.loc);
+        ctx.vertexAttribPointer(att.loc, size, attDataType, normalized, stride, offset);
+        ctx.bindBuffer(bufferType, null);
+        return this;
+    };
+    setUni(name, value) {
+        const ctx = this.ctx, uni = this.vars.unis[name];
+        switch (uni.type) {
+            case ctx.FLOAT_MAT4:
+                ctx.uniformMatrix4fv(uni.loc, false, value);
+                break;
+            case ctx.FLOAT_MAT3:
+                ctx.uniformMatrix3fv(uni.loc, false, value);
+                break;
+            case ctx.FLOAT_MAT2:
+                ctx.uniformMatrix2fv(uni.loc, false, value);
+                break;
+            case ctx.FLOAT:
+                ctx.uniform1f(uni.loc, value);
+                break;
+            case ctx.INT: case ctx.SAMPLER_CUBE: case ctx.SAMPLER_2D:
+            case ctx.SAMPLER_2D_ARRAY:
+                ctx.uniform1i(uni.loc, value);
+                break;
+            case ctx.FLOAT_VEC2:
+                ctx.uniform2fv(uni.loc, value);
+                break;
+            case ctx.FLOAT_VEC3:
+                ctx.uniform3fv(uni.loc, value);
+                break;
+            case ctx.FLOAT_VEC4:
+                ctx.uniform4fv(uni.loc, value);
+                break;
+            case ctx.INT_VEC2:
+                ctx.uniform2iv(uni.loc, value);
+                break;
+            case ctx.INT_VEC3:
+                ctx.uniform3iv(uni.loc, value);
+                break;
+            case ctx.INT_VEC4:
+                ctx.uniform4iv(uni.loc, value);
+                break;
+            default:
+                console.warn("Don't know gl type", uni.type, "for uniform", uni.name);
+                throw "Don't know uniform type";
+        }
+        return this;
+    };
+    bindTex(uniName, tex, texType = tex.type || ctx.TEXTURE_2D, unit = 0) {
+        const ctx = this.ctx;
+        ctx.activeTexture(ctx.TEXTURE0 + unit);
+        ctx.bindTexture(texType, tex);
+        return this.setUni(uniName, unit);
+    };
+    dispose() {
+        const {ctx} = this;
+        ctx.deleteShader(this.shaders.vs);
+        ctx.deleteShader(this.shaders.fs);
+        ctx.deleteProgram(this.program);
+    };
+};
+
+export {
+    Program,
+    Program as default
+};

+ 206 - 0
legacy/WebMC/src/Renderer/Render.js

@@ -0,0 +1,206 @@
+import Program from "./Program.js";
+import { isWebGL2Context } from "../utils/isWebGL2Context.js";
+
+class Render {
+    constructor(canvas) {
+        let ctx = this.gl = this.ctx =
+            canvas.getContext("webgl2") ||
+            canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
+        if (!ctx) throw "Cannot get the WebGL context";
+        this.isWebGL2 = "isSupportWebGL2" in window? window.isSupportWebGL2: isWebGL2Context(ctx);
+        this.prgCache = {};
+        this.texCache = {};
+        this.camera = [];
+        this.frame = this.frame.bind(this);
+        this.timer = null;
+        this.lastFrameTime = window.performance.now();
+        this.dpr = window.devicePixelRatio;
+    };
+    get aspectRatio() {
+        return this.ctx.canvas.width / this.ctx.canvas.height;
+    };
+    addCamera(camera) {
+        this.camera.push(camera);
+        return this;
+    };
+    createProgram(name, vectSrc, fragSrc) {
+        return this.prgCache[name] = new Program(this.ctx, vectSrc, fragSrc);
+    };
+    getProgram(name) { return this.prgCache[name]; };
+
+    frame(timestamp = this.lastFrameTime) {
+        this.timer = window.requestAnimationFrame(this.frame);
+        if (this.onRender) this.onRender(timestamp, timestamp - this.lastFrameTime);
+        this.lastFrameTime = timestamp;
+    };
+    play() {
+        if (this.timer !== null) return;
+        this.lastFrameTime = window.performance.now();
+        this.frame();
+    };
+    stop() {
+        if (this.timer === null) return;
+        window.cancelAnimationFrame(this.timer);
+        this.timer = null;
+    };
+
+    setSize(w, h, dpr = this.dpr) {
+        const c = this.ctx.canvas;
+        this.dpr = dpr;
+        w = (w * dpr) | 0; h = (h * dpr) | 0;
+        c.width = w; c.height = h;
+        this.ctx.viewport(0, 0, w, h);
+        this.camera.forEach(camera => camera.setAspectRatio(w / h));
+        return {w, h};
+    };
+
+    fitScreen(wp = 1, hp = 1) {
+        return this.setSize(
+            window.innerWidth * wp,
+            window.innerHeight * hp
+        );
+    };
+
+    createIbo(data, drawType = this.ctx.STATIC_DRAW) {
+        return this.createBo(data, this.ctx.ELEMENT_ARRAY_BUFFER, drawType);
+    };
+
+    createVbo(data, drawType = this.ctx.STATIC_DRAW) {
+        return this.createBo(data, this.ctx.ARRAY_BUFFER, drawType);
+    };
+
+    createBo(data, boType, drawType = this.ctx.STATIC_DRAW) {
+        return this.bindBoData(this.ctx.createBuffer(), data, {boType, drawType});
+    }
+
+    bindBoData(bufferObj, data, {
+        boType = bufferObj.type,
+        drawType = this.ctx.STATIC_DRAW,
+    } = {}) {
+        const ctx = this.ctx;
+        if (!(data.buffer instanceof ArrayBuffer)) {
+            if (boType === ctx.ELEMENT_ARRAY_BUFFER)
+                data = new Int16Array(data);
+            else if (boType === ctx.ARRAY_BUFFER)
+                data = new Float32Array(data);
+        }
+        bufferObj.length = data.length;
+        bufferObj.type = boType;
+        ctx.bindBuffer(boType, bufferObj);
+        ctx.bufferData(boType, data, drawType);
+        ctx.bindBuffer(boType, null);
+        return bufferObj;
+    };
+
+    _getImageName(img) {
+        let uri = img.outerHTML.match(/src="([^"]*)"/);
+        return uri? uri[1]: String(Math.random());
+    };
+    createTexture(img, name = this._getImageName(img), doYFlip = false) {
+        const {ctx} = this,
+              tex  = ctx.createTexture();
+        if (doYFlip) ctx.pixelStorei(ctx.UNPACK_FLIP_Y_WEBGL, true);
+        ctx.bindTexture(ctx.TEXTURE_2D, tex);
+        ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_MAG_FILTER, ctx.NEAREST);
+        ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_MIN_FILTER, ctx.NEAREST);
+        ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_WRAP_S, ctx.CLAMP_TO_EDGE);
+        ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_WRAP_T, ctx.CLAMP_TO_EDGE);
+        ctx.texImage2D(ctx.TEXTURE_2D, 0, ctx.RGBA, ctx.RGBA, ctx.UNSIGNED_BYTE,
+            img.mipmap && img.mipmap[0]? img.mipmap[0]: img);
+        if (img.mipmap) {
+            ctx.generateMipmap(ctx.TEXTURE_2D);
+            ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_MIN_FILTER, ctx.NEAREST_MIPMAP_LINEAR);
+            for (let i = 1; i < img.mipmap.length; ++i)
+                ctx.texImage2D(ctx.TEXTURE_2D, i, ctx.RGBA, ctx.RGBA, ctx.UNSIGNED_BYTE, img.mipmap[i]);
+        }
+        ctx.bindTexture(ctx.TEXTURE_2D, null);
+        if (doYFlip) ctx.pixelStorei(ctx.UNPACK_FLIP_Y_WEBGL, false);
+        this.texCache[name] = tex;
+        tex.name = name;
+        tex.type = ctx.TEXTURE_2D;
+        return tex;
+    };
+    createTextureArray(img, {
+        singleW = img.texture4array && img.texture4array.singleW,
+        singleH = img.texture4array && img.texture4array.singleH,
+        altesCount = img.texture4array && img.texture4array.altesCount,
+        name = this._getImageName(img),
+        doYFlip = false,
+        useMips = true,
+    } = {}) {
+        if (!window.isSupportWebGL2 || !this.isWebGL2) throw "not support webgl2";
+        if (img.texture4array) img = img.texture4array;
+        const {ctx} = this,
+              tex  = ctx.createTexture();
+        if (doYFlip) ctx.pixelStorei(ctx.UNPACK_FLIP_Y_WEBGL, true);
+        ctx.bindTexture(ctx.TEXTURE_2D_ARRAY, tex);
+        ctx.texParameteri(ctx.TEXTURE_2D_ARRAY, ctx.TEXTURE_MAG_FILTER, ctx.NEAREST);
+        ctx.texParameteri(ctx.TEXTURE_2D_ARRAY, ctx.TEXTURE_MIN_FILTER, ctx.NEAREST);
+        ctx.texParameteri(ctx.TEXTURE_2D_ARRAY, ctx.TEXTURE_WRAP_S, ctx.CLAMP_TO_EDGE);
+        ctx.texParameteri(ctx.TEXTURE_2D_ARRAY, ctx.TEXTURE_WRAP_T, ctx.CLAMP_TO_EDGE);
+        if (singleW >= ctx.getParameter(ctx.MAX_TEXTURE_SIZE)) throw "width out of range";
+        if (singleH >= ctx.getParameter(ctx.MAX_TEXTURE_SIZE)) throw "height out of range";
+        if (altesCount >= ctx.getParameter(ctx.MAX_ARRAY_TEXTURE_LAYERS)) throw "depth out of range";
+        if (Array.isArray(img)) {
+            ctx.texImage3D(ctx.TEXTURE_2D_ARRAY, 0, ctx.RGBA, singleW, singleH, altesCount, 0, ctx.RGBA, ctx.UNSIGNED_BYTE, null);
+            for (let i = 0; i < img.length; ++i)
+                ctx.texSubImage3D(ctx.TEXTURE_2D_ARRAY, 0, 0, 0, i, singleW, singleH, 1, ctx.RGBA, ctx.UNSIGNED_BYTE, img[i]);
+        }
+        else
+            ctx.texImage3D(ctx.TEXTURE_2D_ARRAY, 0, ctx.RGBA, singleW, singleH, altesCount, 0, ctx.RGBA, ctx.UNSIGNED_BYTE, img);
+        if (useMips) {
+            ctx.generateMipmap(ctx.TEXTURE_2D_ARRAY);
+            ctx.texParameteri(ctx.TEXTURE_2D_ARRAY, ctx.TEXTURE_MIN_FILTER, ctx.NEAREST_MIPMAP_LINEAR);
+        }
+        ctx.bindTexture(ctx.TEXTURE_2D_ARRAY, null);
+        if (doYFlip) ctx.pixelStorei(ctx.UNPACK_FLIP_Y_WEBGL, false);
+        this.texCache[name] = tex;
+        tex.name = name;
+        tex.type = ctx.TEXTURE_2D_ARRAY;
+        return tex;
+    };
+    createCubemapsTexture(imgs, name = Math.random(), doYFlip = false) {
+        const {ctx} = this, tex = ctx.createTexture();
+        if (doYFlip) ctx.pixelStorei(ctx.UNPACK_FLIP_Y_WEBGL, true);
+        ctx.bindTexture(ctx.TEXTURE_CUBE_MAP, tex);
+        ctx.texParameteri(ctx.TEXTURE_CUBE_MAP, ctx.TEXTURE_MAG_FILTER, ctx.NEAREST);
+        ctx.texParameteri(ctx.TEXTURE_CUBE_MAP, ctx.TEXTURE_MIN_FILTER, ctx.NEAREST);
+        ctx.texParameteri(ctx.TEXTURE_CUBE_MAP, ctx.TEXTURE_WRAP_S, ctx.CLAMP_TO_EDGE);
+        ctx.texParameteri(ctx.TEXTURE_CUBE_MAP, ctx.TEXTURE_WRAP_T, ctx.CLAMP_TO_EDGE);
+        for (let i = 0; i < 6; ++i) {
+            ctx.texImage2D(ctx.TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, ctx.RGBA, ctx.RGBA, ctx.UNSIGNED_BYTE, imgs[i]);
+        }
+        ctx.generateMipmap(ctx.TEXTURE_CUBE_MAP);
+        ctx.bindTexture(ctx.TEXTURE_CUBE_MAP, null);
+        if (doYFlip) ctx.pixelStorei(ctx.UNPACK_FLIP_Y_WEBGL, true);
+        this.texCache[name] = tex;
+        tex.name = name;
+        tex.type = ctx.TEXTURE_CUBE_MAP;
+        return tex;
+    };
+    getTexture(name) { return this.texCache[name]; };
+    getOrCreateTexture(img, name = img instanceof Image && this._getImageName(img), doYFlip = false) {
+        let cache = this.getTexture(name);
+        if (cache) return cache;
+        if (Array.isArray(img)) return this.createCubemapsTexture(img, name, doYFlip);
+        if (img.texture4array)
+            try { return this.createTextureArray(img, { name, doYFlip }); }
+            catch (e) {
+                console.warn(e);
+                window.isSupportWebGL2 = this.isWebGL2 = false;
+            }
+        return this.createTexture(img, name, doYFlip);
+    };
+
+    dispose() {
+        this.stop();
+        const {ctx} = this;
+        Object.values(this.texCache).forEach(tex => ctx.deleteTexture(tex));
+        Object.values(this.prgCache).forEach(prg => prg.dispose());
+    };
+};
+
+export {
+    Render,
+    Render as default
+};

+ 74 - 0
legacy/WebMC/src/Renderer/WelcomePageRenderer.js

@@ -0,0 +1,74 @@
+
+import {mat4, degree2radian as d2r} from "../utils/gmath.js";
+import Render from "./Render.js";
+import Camera from "./Camera.js";
+import * as glsl from "./glsl.js";
+import {waitResource} from "../utils/loadResources.js";
+
+let texImgs = null;
+waitResource("welcomePage/textures").then(imgs => texImgs = imgs);
+
+class WelcomeRenderer extends Render {
+    constructor(canvas) {
+        super(canvas);
+        this.fitScreen();
+        new ResizeObserver(async e => {
+            await new Promise(s => setTimeout(s, 0));
+            this.fitScreen();
+        }).observe(canvas);
+        const {ctx} = this;
+        this.prg = this.createProgram("welcomePage", glsl.welcomePage.vert, glsl.welcomePage.frag)
+                    .use().bindTex("uTexture", this.createCubemapsTexture(texImgs, "welcomePage/textures"));
+        ctx.texParameteri(ctx.TEXTURE_CUBE_MAP, ctx.TEXTURE_MAG_FILTER, ctx.LINEAR);
+        ctx.texParameteri(ctx.TEXTURE_CUBE_MAP, ctx.TEXTURE_MIN_FILTER, ctx.LINEAR);
+        let mainCamera = this.mainCamera = new Camera(this.aspectRatio, {
+            viewType: Camera.viewType.lookAt,
+            fovy: 120, position: [0, 0, 0],
+            lookAt: [-1, 0, 0], up: [0, 1, 0],
+            far: 10,
+        });
+        this.addCamera(mainCamera);
+        let vertexPosition = [
+                -1, 1,-1, -1,-1,-1, -1,-1, 1, -1, 1, 1,
+                -1, 1, 1, -1,-1, 1,  1,-1, 1,  1, 1, 1,
+                 1, 1, 1,  1,-1, 1,  1,-1,-1,  1, 1,-1,
+                 1, 1,-1,  1,-1,-1, -1,-1,-1, -1, 1,-1,
+                 1, 1,-1, -1, 1,-1, -1, 1, 1,  1, 1, 1,
+                -1,-1,-1,  1,-1,-1,  1,-1, 1, -1,-1, 1,
+            ],
+            element = (len => {
+                let base = [0,1,2, 0,2,3], out = [];
+                for (let i = 0, j = 0; i <= len; j = i++ * 4)
+                    out.push(...base.map(x => x + j));
+                return out;
+            })(vertexPosition.length / 12);
+        this.bos = {
+            ver: this.createVbo(vertexPosition),
+            ele: this.createIbo(element),
+        };
+        this.mM = mat4.identity();
+        this.mvpM = mat4.identity();
+    };
+    get vpM() { return this.mainCamera.projview; };
+    onRender() {
+        const {ctx, prg, mM, vpM, mvpM, bos} = this;
+        mat4.rotate(mM, d2r(1 / 70), [0, 1, 0], mM);
+        mat4.multiply(vpM, mM, mvpM);
+        prg.use()
+            .setUni("uMvpMatrix", mvpM)
+            .setAtt("aPosition", bos.ver);
+        ctx.clear(ctx.COLOR_BUFFER_BIT);
+        ctx.bindBuffer(bos.ele.type, bos.ele);
+        ctx.drawElements(ctx.TRIANGLES, bos.ele.length, ctx.UNSIGNED_SHORT, 0);
+        ctx.flush();
+    };
+    dispose() {
+        super.dispose();
+        this.ctx.deleteBuffer(this.bos.ver);
+        this.ctx.deleteBuffer(this.bos.ele);
+    };
+};
+
+export {
+    WelcomeRenderer,
+};

+ 647 - 0
legacy/WebMC/src/Renderer/WorldChunkModule.js

@@ -0,0 +1,647 @@
+// 计算顶点,材质坐标,下标
+import { Block } from "../World/Block.js";
+import { 
+    Chunk,
+    CHUNK_X_SIZE as X_SIZE,
+    CHUNK_Y_SIZE as Y_SIZE,
+    CHUNK_Z_SIZE as Z_SIZE,
+} from "../World/Chunk.js";
+import { manhattanDis } from "../utils/gmath.js";
+
+const rxyz2int = Chunk.getLinearBlockIndex;
+
+const calCol = (verNum, blockLight) => {
+    let ans = new Array(verNum * 4);
+    for (let i = 0; i < verNum * 4; i += 4) {
+        ans[i] = ans[i + 1] = ans[i + 2] = Math.pow(0.9, 15 - blockLight);
+        ans[i + 3] = 1;
+    }
+    return ans;
+};
+
+class ChunksModule {
+    constructor(world, renderer) {
+        /* render cache (help to quick update chunk module without recalculate)
+         * blockFace: chunkKey => [yxz]["x+|x-|y+|y-|z+|z-|face"]: {
+         *   ver: vertex coordiate (length == 3n)
+         *   tex: texture uv corrdiate (length == 2n)
+         *   col: vertex color (length == 4n)
+         *   ele: element (length == 3n/2)
+         * }
+         */
+        this.blockFace = {};
+        /** meshs: chunkKey => {
+         *     ver[], col[], tex[], ele[],
+         *     disableCullFace: { ver[], col[], tex[], ele[] }
+         *     buffer object: bo { ver, col, tex, ele, disableCullFace&fluidSurface: { ver, col, tex, ele } }
+         * }
+         */
+        this.meshs = {};
+        this.needUpdateMeshChunks = new Set();
+        this.needUpdateColMeshChunks = new Set();
+        this.needUpdateTile = [];
+        this.setRenderer(renderer);
+        this.setWorld(world);
+    };
+    setWorld(world) {
+        this.world = world;
+        for (let chunkKey in world.chunkMap)
+            this.buildChunkModule(chunkKey);
+        world.addEventListener("onTileChanges", this.updateTile.bind(this));
+        world.addEventListener("onChunkLoad", chunk => {
+            this.buildChunkModule(chunk.chunkKey);
+            for (let [dx, dy, dz] of [[1,0,0], [-1,0,0], [0,1,0], [0,-1,0], [0,0,1], [0,0,-1]]) {
+                this.buildChunkModule(Chunk.chunkKeyByChunkXYZ(chunk.x + dx, chunk.y + dy, chunk.z + dz));
+            }
+        });
+    };
+    setRenderer(renderer = null) {
+        if (!renderer) return;
+        this.renderer = renderer;
+        this.updateMeshs();
+    };
+    buildChunkModule(chunkKey) {
+        const world = this.world, chunk = world.getChunkByChunkKey(chunkKey);
+        if (chunk === null) return;
+        let blockFace = this.blockFace[chunkKey] || [...Array(X_SIZE * Y_SIZE * Z_SIZE)].map(() => ({}));
+        this.blockFace[chunkKey] = blockFace;
+        // build vertex
+        for (let j = 0; j < Y_SIZE; ++j)
+        for (let k = 0; k < Z_SIZE; ++k)
+        for (let i = 0; i < X_SIZE; ++i) {
+            let cblock = chunk.getBlock(i, j, k);
+            if (cblock.name === "air") continue;
+            let [wx, wy, wz] = chunk.blockRXYZ2BlockXYZ(i, j, k),
+                bf = blockFace[rxyz2int(i, j, k)];
+            // 如果周围方块透明 绘制
+            switch(cblock.renderType) {
+            case Block.renderType.FLUID:
+            case Block.renderType.CACTUS:
+            case Block.renderType.NORMAL: {
+                [[1,0,0,"x+"], [-1,0,0,"x-"], [0,1,0,"y+"], [0,-1,0,"y-"], [0,0,1,"z+"], [0,0,-1,"z-"]]
+                .forEach(([dx, dy, dz, face]) => {
+                    let rx = i + dx, ry = j + dy, rz = k + dz,
+                        inOtherChunk = chunk.inOtherChunk(rx, ry, rz),
+                        b = inOtherChunk
+                            ? world.getBlock(wx + dx, wy + dy, wz + dz)
+                            : chunk.getBlock(rx, ry, rz);
+                    if (b && b.isOpaque) return delete bf[face];
+                    if (cblock.isGlass && b && b.isGlass) return delete bf[face];
+                    let verNum = cblock.vertexs[face].length / 3,
+                        bff = bf[face] || {},
+                        bl = inOtherChunk
+                            ? world.getLight(wx + dx, wy + dy, wz + dz)
+                            : chunk.getLight(rx, ry, rz);
+                    if (bl === null) bl = 15;
+                    bff.disableCullFace = cblock.renderType === Block.renderType.CACTUS;
+                    if (cblock.isLeaves && !(b && b.isLeaves)) bff.disableCullFace = true;
+                    bff.ver = cblock.vertexs[face].map((v, ind) => ind%3===0? v+wx: ind%3===1? v+wy: v+wz);
+                    bff.col = calCol(verNum, bl);
+                    bff.ele = cblock.elements[face];
+                    bff.tex = cblock.texture.uv[face];
+                    bf[face] = bff;
+                });
+                break;}
+            case Block.renderType.FLOWER: {
+                let aroundOpaque = 0;
+                for (let [dx, dy, dz] of [[1,0,0], [-1,0,0], [0,1,0], [0,-1,0], [0,0,1], [0,0,-1]]) {
+                    let rx = i + dx, ry = j + dy, rz = k + dz,
+                        b = chunk.inOtherChunk(rx, ry, rz)
+                            ? world.getBlock(wx + dx, wy + dy, wz + dz)
+                            : chunk.getBlock(rx, ry, rz);
+                    if (b && b.isOpaque) ++aroundOpaque;
+                }
+                if (aroundOpaque === 6) {
+                    delete bf.face;
+                    break;
+                }
+                let bl = chunk.getLight(i, j, k),
+                    verNum = cblock.vertexs.face.length / 3;
+                bf.face = {
+                    disableCullFace: true,
+                    ver: cblock.vertexs.face.map((v, ind) => ind%3===0? v+wx: ind%3===1? v+wy: v+wz),
+                    col: calCol(verNum, bl),
+                    ele: cblock.elements.face,
+                    tex: cblock.texture.uv.face,
+                };
+                break;}
+            }
+        }
+        chunk.updatedLightMap = false;
+        this.needUpdateMeshChunks.add(chunkKey);
+    };
+    updateTile(blockX, blockY, blockZ) {
+        this.needUpdateTile.push([blockX, blockY, blockZ]);
+    };
+    updateTiles(blockXYZs) {
+        // console.log(performance.now())
+        while (blockXYZs.length) {
+            let [blockX, blockY, blockZ] = blockXYZs.shift();
+            const world = this.world, cblock = world.getBlock(blockX, blockY, blockZ);
+            if (cblock === null) return;
+            let bf = {};
+            // handle center
+            let yp = {};
+            if (cblock.name !== "air") switch (cblock.renderType) {
+                case Block.renderType.FLUID: {
+                    if (cblock.bd > cblock.maxLevel) {
+                        yp.y0 = yp.y1 = yp.y2 = yp.y3 = yp.y4 = 1;
+                    }
+                    else {
+                        // 液体影响 该顶点的高度等于周围相同液体高度的平均值
+                        // 空气影响 斜对角是没有碰撞箱的方块会让顶点变低【压力板和梯子除外
+                        // 左上角
+                        let luflevel = Infinity;
+                        for (let [dx, dz] of [[-1, -1], [0, -1], [-1, 0], [0, 0]]) {
+                            const longID = world.getTile(blockX + dx, blockY, blockZ + dz);
+                            if (!longID) continue;
+                            let abd = longID.bd;
+                            const ablock = Block.getBlockByBlockLongID(longID);
+                            if (ablock === cblock
+                            || (cblock.name === "water" && ablock.name === "flowing_water")
+                            || (cblock.name === "flowing_water" && ablock.name === "water")
+                            || (cblock.name === "lava" && ablock.name === "flowing_lava")
+                            || (cblock.name === "flowing_lava" && ablock.name === "lava"))
+                                luflevel = Math.min(luflevel, abd > cblock.maxLevel? -Infinity: abd);
+                        }
+                        // 左下角
+                        let lbflevel = Infinity;
+                        for (let [dx, dz] of [[-1, 0], [0, 0], [-1, 1], [0, 1]]) {
+                            const longID = world.getTile(blockX + dx, blockY, blockZ + dz);
+                            if (!longID) continue;
+                            let abd = longID.bd;
+                            const ablock = Block.getBlockByBlockLongID(longID);
+                            if (ablock === cblock
+                            || (cblock.name === "water" && ablock.name === "flowing_water")
+                            || (cblock.name === "flowing_water" && ablock.name === "water")
+                            || (cblock.name === "lava" && ablock.name === "flowing_lava")
+                            || (cblock.name === "flowing_lava" && ablock.name === "lava"))
+                                lbflevel = Math.min(lbflevel, abd > cblock.maxLevel? -Infinity: abd);
+                        }
+                        // 右上角
+                        let ruflevel = Infinity;
+                        for (let [dx, dz] of [[0,-1], [1,-1], [0, 0], [1, 0]]) {
+                            const longID = world.getTile(blockX + dx, blockY, blockZ + dz);
+                            if (!longID) continue;
+                            let abd = longID.bd;
+                            const ablock = Block.getBlockByBlockLongID(longID);
+                            if (ablock === cblock
+                            || (cblock.name === "water" && ablock.name === "flowing_water")
+                            || (cblock.name === "flowing_water" && ablock.name === "water")
+                            || (cblock.name === "lava" && ablock.name === "flowing_lava")
+                            || (cblock.name === "flowing_lava" && ablock.name === "lava"))
+                                ruflevel = Math.min(ruflevel, abd > cblock.maxLevel? -Infinity: abd);
+                        }
+                        // 右下角
+                        let rbflevel = Infinity;
+                        for (let [dx, dz] of [[0, 0], [1, 0], [0, 1], [1, 1]]) {
+                            const longID = world.getTile(blockX + dx, blockY, blockZ + dz);
+                            if (!longID) continue;
+                            let abd = longID.bd;
+                            const ablock = Block.getBlockByBlockLongID(longID);
+                            if (ablock === cblock
+                            || (cblock.name === "water" && ablock.name === "flowing_water")
+                            || (cblock.name === "flowing_water" && ablock.name === "water")
+                            || (cblock.name === "lava" && ablock.name === "flowing_lava")
+                            || (cblock.name === "flowing_lava" && ablock.name === "lava"))
+                                rbflevel = Math.min(rbflevel, abd > cblock.maxLevel? -Infinity: abd);
+                        }
+                        let level2ver = level => level == -Infinity? 1: ((cblock.maxLevel + 1) - level - 0.75) / (cblock.maxLevel + 1);
+                        yp = {
+                            y0: level2ver(luflevel), y1: level2ver(lbflevel),
+                            y2: level2ver(rbflevel), y3: level2ver(ruflevel),
+                        };
+                        yp.y4 = (yp.y0 + yp.y1 + yp.y2 + yp.y3) / 4;
+                        // console.log(blockX, blockY, blockZ, cid, cbd, {luflevel, lbflevel, rbflevel, ruflevel}, yp);
+                    }
+
+                    [[1,0,0,"x+"], [-1,0,0,"x-"], [0,1,0,"y+"], [0,-1,0,"y-"], [0,0,1,"z+"], [0,0,-1,"z-"]]
+                    .forEach(([dx, dy, dz, face]) => {
+                        let wx = blockX + dx, wy = blockY + dy, wz = blockZ + dz,
+                            b = world.getBlock(wx, wy, wz);
+                        if (b && (b.isOpaque || b.isFluid)) return;
+                        let bl = world.getLight(wx, wy, wz),
+                            verNum = cblock.vertexs[face].length / 3;
+                        if (bl === null) bl = 15;
+                        let ver = cblock.vertexs[face].map(v => typeof v == "string"? yp[v]: v),
+                            uv = cblock.texture.uv[face];
+                        bf[face] = {
+                            fluidSurface: true,// disableCullFace: true,
+                            ver: ver.map((v, ind) => ind%3===0? v+blockX: ind%3===1? v+blockY: v+blockZ),
+                            ele: cblock.elements[face],
+                            tex: face[0] === 'y'? uv: cblock.texture.img.texture4array
+                                ? [
+                                    uv[0], 1 - ver[(1 >> 1) * 3 + 1], uv[2],
+                                    uv[3], uv[4], uv[5],
+                                    uv[6], uv[7], uv[8],
+                                    uv[9], 1 - ver[(7 >> 1) * 3 + 1], uv[11],
+                                ]
+                                : [
+                                    uv[0], uv[4] -(uv[4] - uv[1]) * ver[(1 >> 1) * 3 + 1], uv[2],
+                                    uv[3], uv[4], uv[5],
+                                    uv[6], uv[7], uv[8],
+                                    uv[9], uv[7] -(uv[7] - uv[10]) * ver[(7 >> 1) * 3 + 1], uv[11],
+                                ],
+                            col: face[0] !== 'y'
+                                ? [...Array(verNum * 4)].map((_, ind) => ind % 4 === 3? 1.0: 0)
+                                : [...Array(verNum * 4)].map((_, ind) => ind % 4 === 3? 1.0: Math.pow(0.9, 15 - bl)),
+                        };
+                    });
+                    break; }
+                case Block.renderType.CACTUS:
+                case Block.renderType.NORMAL: {
+                    [[1,0,0,"x+"], [-1,0,0,"x-"], [0,1,0,"y+"], [0,-1,0,"y-"], [0,0,1,"z+"], [0,0,-1,"z-"]]
+                    .forEach(([dx, dy, dz, face]) => {
+                        let wx = blockX + dx, wy = blockY + dy, wz = blockZ + dz,
+                            b = world.getBlock(wx, wy, wz);
+                        if (b && b.isOpaque) return;
+                        if (cblock.isGlass && b && b.isGlass) return delete bf[face];
+                        let bl = world.getLight(wx, wy, wz),
+                            verNum = cblock.vertexs[face].length / 3;
+                        if (bl === null) bl = 15;
+                        bf[face] = {
+                            disableCullFace: cblock.renderType === Block.renderType.CACTUS,
+                            ver: cblock.vertexs[face].map((v, ind) => ind%3===0? v+blockX: ind%3===1? v+blockY: v+blockZ),
+                            ele: cblock.elements[face],
+                            tex: cblock.texture.uv[face],
+                            col: [...Array(verNum * 4)].map((_, ind) => ind % 4 === 3? 1.0: Math.pow(0.9, 15 - bl)),
+                        };
+                        if (cblock.isLeaves && !(b && b.isLeaves)) bf[face].disableCullFace = true;
+                    });
+                    break;}
+                case Block.renderType.FLOWER: {
+                    let aroundOpaque = 0;
+                    for (let [dx, dy, dz] of [[1,0,0], [-1,0,0], [0,1,0], [0,-1,0], [0,0,1], [0,0,-1]]) {
+                        let wx = blockX + dx, wy = blockY + dy, wz = blockZ + dz,
+                            b = world.getBlock(wx, wy, wz);
+                        if (b && b.isOpaque) ++aroundOpaque;
+                    }
+                    if (aroundOpaque === 6) break;
+                    let bl = world.getLight(blockX, blockY, blockZ),
+                        verNum = cblock.vertexs.face.length / 3;
+                    bf.face = {
+                        disableCullFace: true,
+                        ver: cblock.vertexs.face.map((v, ind) => ind%3===0? v+blockX: ind%3===1? v+blockY: v+blockZ),
+                        ele: cblock.elements.face,
+                        tex: cblock.texture.uv.face,
+                        col: [...Array(verNum * 4)].map((_, ind) => ind % 4 === 3? 1.0: Math.pow(0.9, 15 - bl)),
+                    };
+                    break;}
+            }
+            let [blockRX, blockRY, blockRZ] = Chunk.getRelativeBlockXYZ(blockX, blockY, blockZ),
+                chunkKey = Chunk.chunkKeyByBlockXYZ(blockX, blockY, blockZ);
+            this.blockFace[chunkKey][rxyz2int(blockRX, blockRY, blockRZ)] = bf;
+            this.needUpdateMeshChunks.add(chunkKey);
+            // handle around block
+            let cbl = world.getLight(blockX, blockY, blockZ);
+            [[1,0,0,"x-"], [-1,0,0,"x+"], [0,1,0,"y-"], [0,-1,0,"y+"], [0,0,1,"z-"], [0,0,-1,"z+"]]
+            .forEach(([dx, dy, dz, inverseFace]) => {
+                let awx = blockX + dx, awy = blockY + dy, awz = blockZ + dz,
+                    ablock = world.getBlock(awx, awy, awz);
+                if (ablock === null || ablock.name === "air") return;
+                let achunkKey = Chunk.chunkKeyByBlockXYZ(awx, awy, awz),
+                    [arx, ary, arz] = Chunk.getRelativeBlockXYZ(awx, awy, awz),
+                    abf = this.blockFace[achunkKey][rxyz2int(arx, ary, arz)];
+                switch (ablock.renderType) {
+                case Block.renderType.FLUID: {
+                    if ((cblock.isOpaque && inverseFace === "y-") || (cblock.isFluid && (
+                        ablock === cblock
+                        || (cblock.name === "water" && ablock.name === "flowing_water")
+                        || (cblock.name === "flowing_water" && ablock.name === "water")
+                        || (cblock.name === "lava" && ablock.name === "flowing_lava")
+                        || (cblock.name === "flowing_lava" && ablock.name === "lava")
+                    ))) {
+                        delete abf[inverseFace];
+                        break;
+                    }
+                    if (cblock.renderType !== Block.renderType.FLUID) break;
+                    let verNum = ablock.vertexs[inverseFace].length / 3;
+                    let fn = v => ({v0: "v1", v1: "v0", v2: "v3", v3: "v2"})[v];
+                    let ver = ablock.vertexs[inverseFace].map(v => typeof v == "string"? yp[fn(v)]: v),
+                        uv = ablock.texture.uv[inverseFace];
+                    abf[inverseFace] = {
+                        fluidSurface: true,// disableCullFace: true,
+                        ver: ver.map((v, ind) => ind%3===0? v+awx: ind%3===1? v+awy: v+awz),
+                        ele: ablock.elements[inverseFace],
+                        tex: inverseFace[0] === 'y'? uv: ablock.texture.img.texture4array
+                            ? [
+                                uv[0], 1 - ver[(1 >> 1) * 3 + 1], uv[2],
+                                uv[3], uv[4], uv[5],
+                                uv[6], uv[7], uv[8],
+                                uv[9], 1 - ver[(7 >> 1) * 3 + 1], uv[11],
+                            ]
+                            : [
+                                uv[0], uv[4] -(uv[4] - uv[1]) * ver[(1 >> 1) * 3 + 1], uv[2],
+                                uv[3], uv[4], uv[5],
+                                uv[6], uv[7], uv[8],
+                                uv[9], uv[7] -(uv[7] - uv[10]) * ver[(7 >> 1) * 3 + 1], uv[11],
+                            ],
+                        col: inverseFace[0] !== 'y'
+                            ? [...Array(verNum * 4)].map((_, ind) => ind % 4 === 3? 1.0: 0)
+                            : [...Array(verNum * 4)].map((_, ind) => ind % 4 === 3? 1.0: Math.pow(0.9, 15 - cbl)),
+                    };
+                    break;}
+                case Block.renderType.CACTUS:
+                case Block.renderType.NORMAL: {
+                    if ((cblock.isGlass && ablock.isGlass) || cblock.isOpaque){
+                        delete abf[inverseFace];
+                        break;
+                    }
+                    let verNum = ablock.vertexs[inverseFace].length / 3;
+                    abf[inverseFace] = {
+                        ver: ablock.vertexs[inverseFace].map((v, ind) => ind%3===0? v+awx: ind%3===1? v+awy: v+awz),
+                        ele: ablock.elements[inverseFace],
+                        tex: ablock.texture.uv[inverseFace],
+                        col: [...Array(verNum * 4)].map((_, ind) => ind % 4 === 3? 1.0: Math.pow(0.9, 15 - cbl)),
+                    };
+                    if ((!cblock.isLeaves) && ablock.isLeaves) abf[inverseFace].disableCullFace = true;
+                    break;}
+                case Block.renderType.FLOWER: break;
+                }
+                this.needUpdateMeshChunks.add(achunkKey);
+            });
+            // 更新斜对角液体顶点?
+        }
+    };
+    updateLight(chunkKey) {
+        let blockFace = this.blockFace[chunkKey];
+        if (!blockFace) return;
+        const world = this.world, chunk = world.getChunkByChunkKey(chunkKey);
+        for (let j = 0; j < Y_SIZE; ++j)
+        for (let k = 0; k < Z_SIZE; ++k)
+        for (let i = 0; i < X_SIZE; ++i) {
+            let cblock = chunk.getBlock(i, j, k);
+            if (cblock.name === "air") continue;
+            let [wx, wy, wz] = chunk.blockRXYZ2BlockXYZ(i, j, k),
+                bf = blockFace[rxyz2int(i, j, k)];
+            switch(cblock.renderType) {
+            case Block.renderType.FLUID:
+            case Block.renderType.CACTUS:
+            case Block.renderType.NORMAL: {
+                [[1,0,0,"x+"], [-1,0,0,"x-"], [0,1,0,"y+"], [0,-1,0,"y-"], [0,0,1,"z+"], [0,0,-1,"z-"]]
+                .forEach(([dx, dy, dz, face]) => {
+                    if (!(face in bf)) return;
+                    let rx = i + dx, ry = j + dy, rz = k + dz,
+                        verNum = cblock.vertexs[face].length / 3,
+                        b = chunk.inOtherChunk(rx, ry, rz)
+                            ? world.getBlock(wx + dx, wy + dy, wz + dz)
+                            : chunk.getBlock(rx, ry, rz),
+                        bl = chunk.inOtherChunk(rx, ry, rz)
+                            ? world.getLight(wx + dx, wy + dy, wz + dz)
+                            : chunk.getLight(rx, ry, rz);
+                    if (bl === null) bl = 15;
+                    if (cblock.isFluid && b && b.isOpaque)
+                        bl = Math.max(0, bl - 1);
+                    bf[face].col = calCol(verNum, bl);
+                });
+                break;}
+            case Block.renderType.FLOWER: {
+                let bl = chunk.getLight(i, j, k),
+                    verNum = cblock.vertexs.face.length / 3;
+                bf.face.col = calCol(verNum, bl);
+                break;}
+            }
+        }
+        this.needUpdateColMeshChunks.add(chunkKey);
+    };
+    updateMeshs() {
+        for (let chunkKey in this.meshs)
+            this.updateMesh(chunkKey);
+    };
+    updateMesh(chunkKey, {
+        ver = this.meshs[chunkKey].ver,
+        col = this.meshs[chunkKey].col,
+        tex = this.meshs[chunkKey].tex,
+        ele = this.meshs[chunkKey].ele,
+        disableCullFace: {
+            ver: dcfVer = this.meshs[chunkKey].disableCullFace.ver,
+            col: dcfCol = this.meshs[chunkKey].disableCullFace.col,
+            tex: dcfTex = this.meshs[chunkKey].disableCullFace.tex,
+            ele: dcfEle = this.meshs[chunkKey].disableCullFace.ele,
+        } = {},
+        fluidSurface: {
+            ver: fluidVer = this.meshs[chunkKey].fluidSurface.ver,
+            col: fluidCol = this.meshs[chunkKey].fluidSurface.col,
+            tex: fluidTex = this.meshs[chunkKey].fluidSurface.tex,
+            ele: fluidEle = this.meshs[chunkKey].fluidSurface.ele,
+        } = {},
+    } = this.meshs[chunkKey]) {
+        let mesh = this.meshs[chunkKey];
+        if (!mesh) mesh = this.meshs[chunkKey] = {};
+        mesh.ver = ver;
+        mesh.tex = tex;
+        mesh.col = col;
+        mesh.ele = ele;
+        mesh.disableCullFace = {
+            ver: dcfVer, tex: dcfTex,
+            col: dcfCol, ele: dcfEle,
+        };
+        mesh.fluidSurface = {
+            ver: fluidVer, tex: fluidTex,
+            col: fluidCol, ele: fluidEle,
+        };
+        const bufferObj = mesh.bo || {}, {renderer} = this;
+        if (!renderer) {
+            if (bufferObj.ver) for (let k of ["ver", "col", "tex", "ele"]) {
+                bufferObj[k] = bufferObj.disableCullFace[k] = null;
+                if (bufferObj.disableCullFace) bufferObj.disableCullFace[k] = null;
+                if (bufferObj.fluidSurface) bufferObj.fluidSurface[k] = null;
+            }
+            return;
+        }
+        if (bufferObj.ver) {
+            renderer.bindBoData(bufferObj.ver, ver);
+            renderer.bindBoData(bufferObj.col, col);
+            renderer.bindBoData(bufferObj.tex, tex);
+            renderer.bindBoData(bufferObj.ele, ele);
+            renderer.bindBoData(bufferObj.disableCullFace.ver, dcfVer);
+            renderer.bindBoData(bufferObj.disableCullFace.col, dcfCol);
+            renderer.bindBoData(bufferObj.disableCullFace.tex, dcfTex);
+            renderer.bindBoData(bufferObj.disableCullFace.ele, dcfEle);
+            renderer.bindBoData(bufferObj.fluidSurface.ver, fluidVer);
+            renderer.bindBoData(bufferObj.fluidSurface.col, fluidCol);
+            renderer.bindBoData(bufferObj.fluidSurface.tex, fluidTex);
+            renderer.bindBoData(bufferObj.fluidSurface.ele, fluidEle);
+        }
+        else {
+            bufferObj.ver = renderer.createVbo(ver);
+            bufferObj.col = renderer.createVbo(col);
+            bufferObj.tex = renderer.createVbo(tex);
+            bufferObj.ele = renderer.createIbo(ele);
+            bufferObj.disableCullFace = {
+                ver: renderer.createVbo(dcfVer),
+                col: renderer.createVbo(dcfCol),
+                tex: renderer.createVbo(dcfTex),
+                ele: renderer.createIbo(dcfEle),
+            };
+            bufferObj.fluidSurface = {
+                ver: renderer.createVbo(fluidVer),
+                col: renderer.createVbo(fluidCol),
+                tex: renderer.createVbo(fluidTex),
+                ele: renderer.createIbo(fluidEle),
+            };
+        }
+        mesh.bo = bufferObj;
+    };
+    update() {
+        if (this.needUpdateTile.length)
+            this.updateTiles(this.needUpdateTile);
+        // rebuild light
+        Object.values(this.world.chunkMap)
+        .filter(chunk => chunk.updatedLightMap)
+        .forEach(chunk => {
+            chunk.updatedLightMap = false;
+            const chunkKey = chunk.chunkKey;
+            this.updateLight(chunkKey);
+            for (let [dx, dy, dz] of [[1,0,0], [-1,0,0], [0,1,0], [0,-1,0], [0,0,1], [0,0,-1]])
+                this.updateLight(Chunk.chunkKeyByChunkXYZ(chunk.x + dx, chunk.y + dy, chunk.z + dz));
+        });
+        if (this.needUpdateColMeshChunks.size) {
+            this.needUpdateColMeshChunks.forEach(chunkKey => {
+                if (this.needUpdateMeshChunks.has(chunkKey)) return;
+                let col = [], dcfCol = [], fluidCol = [];
+                this.blockFace[chunkKey].forEach(bf => {
+                    for (let face in bf) {
+                        let bff = bf[face];
+                        if (bff.disableCullFace)
+                            dcfCol.push(...bff.col);
+                        else if (bff.fluidSurface)
+                            fluidCol.push(...bff.col);
+                        else col.push(...bff.col);
+                    }
+                });
+                this.updateMesh(chunkKey, {
+                    col, disableCullFace: {
+                        col: dcfCol,
+                    }, fluidSurface: {
+                        col: fluidCol,
+                    },
+                });
+            });
+            this.needUpdateColMeshChunks.clear();
+        }
+        if (this.needUpdateMeshChunks.size === 0) return;
+        this.needUpdateMeshChunks.forEach(chunkKey => {
+            if (!(chunkKey in this.blockFace)) return;
+            let ver = [], col = [], ele = [], tex = [], totalVer = 0,
+                dcfVer = [], dcfCol = [], dcfEle = [], dcfTex = [], dcfTotalVer = 0,
+                fluidVer = [], fluidCol = [], fluidEle = [], fluidTex = [], fluidTotalVer = 0;
+            this.blockFace[chunkKey].forEach(bf => {
+                for (let face in bf) {
+                    let bff = bf[face], verNum = bff.ver.length / 3;
+                    if (bff.disableCullFace) {
+                        dcfVer.push(...bff.ver);
+                        dcfTex.push(...bff.tex);
+                        dcfCol.push(...bff.col);
+                        dcfEle.push(...bff.ele.map(v => v + dcfTotalVer));
+                        dcfTotalVer += verNum;
+                        continue;
+                    }
+                    if (bff.fluidSurface) {
+                        fluidVer.push(...bff.ver);
+                        fluidTex.push(...bff.tex);
+                        fluidCol.push(...bff.col);
+                        fluidEle.push(...bff.ele.map(v => v + fluidTotalVer));
+                        fluidTotalVer += verNum;
+                        continue;
+                    }
+                    ver.push(...bff.ver);
+                    tex.push(...bff.tex);
+                    col.push(...bff.col);
+                    ele.push(...bff.ele.map(v => v + totalVer));
+                    totalVer += verNum;
+                }
+            });
+            this.updateMesh(chunkKey, {ver, tex, col, ele, disableCullFace: {
+                ver: dcfVer, tex: dcfTex,
+                col: dcfCol, ele: dcfEle,
+            }, fluidSurface: {
+                ver: fluidVer, tex: fluidTex,
+                col: fluidCol, ele: fluidEle,
+            }, });
+        });
+        this.needUpdateMeshChunks.clear();
+    };
+    draw() {
+        const {renderer} = this, {ctx} = renderer;
+        const prg = renderer.getProgram("showBlock");
+        prg.use().setUni("mvpMatrix", renderer.mainCamera.projview);
+        const mainPlayer = this.world.mainPlayer;
+        let chunk = this.world.getChunkByBlockXYZ(...[...mainPlayer.position].map(n => n < 0? n - 1: n));
+        const meshs = Object.entries(this.meshs).map(([chunkKey, mesh]) => {
+            const chunkPos = Chunk.getChunkXYZByChunkKey(chunkKey);
+            return {
+                chunkKey, mesh, chunkPos,
+                dis: manhattanDis(chunkPos, [chunk.x, chunk.y, chunk.z]),
+            };
+        });
+        meshs.sort((a, b) => b.dis - a.dis);
+        for (const { mesh } of meshs) {
+            const bufferObj = mesh.bo;
+            if (bufferObj.ele.length) {
+                prg.setAtt("position", bufferObj.ver)
+                    .setAtt("color", bufferObj.col)
+                    .setAtt("textureCoord", bufferObj.tex);
+                ctx.bindBuffer(bufferObj.ele.type, bufferObj.ele);
+                // ctx.drawElements(ctx.LINES, bufferObj.ele.length, ctx.UNSIGNED_SHORT, 0);
+                ctx.drawElements(ctx.TRIANGLES, bufferObj.ele.length, ctx.UNSIGNED_SHORT, 0);
+            }
+            if (bufferObj.disableCullFace.ele.length) {
+                prg.setAtt("position", bufferObj.disableCullFace.ver)
+                    .setAtt("color", bufferObj.disableCullFace.col)
+                    .setAtt("textureCoord", bufferObj.disableCullFace.tex);
+                ctx.disable(ctx.CULL_FACE);
+                ctx.enable(ctx.POLYGON_OFFSET_FILL);
+                ctx.polygonOffset(1.0, 1.0);
+                ctx.enable(ctx.BLEND);
+                ctx.blendFunc(ctx.SRC_ALPHA, ctx.ONE_MINUS_SRC_ALPHA);
+                ctx.bindBuffer(bufferObj.disableCullFace.ele.type, bufferObj.disableCullFace.ele);
+                // ctx.drawElements(ctx.LINES, bufferObj.disableCullFace.ele.length, ctx.UNSIGNED_SHORT, 0);
+                ctx.drawElements(ctx.TRIANGLES, bufferObj.disableCullFace.ele.length, ctx.UNSIGNED_SHORT, 0);
+                ctx.blendFunc(ctx.ONE, ctx.ZERO);
+                ctx.disable(ctx.BLEND);
+                ctx.disable(ctx.POLYGON_OFFSET_FILL);
+                ctx.enable(ctx.CULL_FACE);
+            }
+        }
+        for (const { mesh } of meshs) {
+            const bufferObj = mesh.bo;
+            if (bufferObj.fluidSurface.ele.length) {
+                prg.setAtt("position", bufferObj.fluidSurface.ver)
+                    .setAtt("color", bufferObj.fluidSurface.col)
+                    .setAtt("textureCoord", bufferObj.fluidSurface.tex);
+                ctx.depthMask(false);
+                if (mainPlayer.eyeInFluid)
+                    ctx.disable(ctx.CULL_FACE);
+                ctx.disable(ctx.POLYGON_OFFSET_FILL);
+                ctx.polygonOffset(1.0, 1.0);
+                ctx.enable(ctx.BLEND);
+                ctx.blendFunc(ctx.SRC_ALPHA, ctx.ONE_MINUS_SRC_ALPHA);
+                ctx.bindBuffer(bufferObj.fluidSurface.ele.type, bufferObj.fluidSurface.ele);
+                // ctx.drawElements(ctx.LINES, bufferObj.fluidSurface.ele.length, ctx.UNSIGNED_SHORT, 0);
+                ctx.drawElements(ctx.TRIANGLES, bufferObj.fluidSurface.ele.length, ctx.UNSIGNED_SHORT, 0);
+                ctx.blendFunc(ctx.ONE, ctx.ZERO);
+                ctx.disable(ctx.BLEND);
+                ctx.disable(ctx.POLYGON_OFFSET_FILL);
+                if (mainPlayer.eyeInFluid)
+                    ctx.enable(ctx.CULL_FACE);
+                ctx.depthMask(true);
+            }
+        }
+    };
+    dispose() {
+        if (!this.renderer) return;
+        const ctx = this.renderer.ctx;
+        Object.values(this.meshs).forEach(({bo}) => {
+            for (let k of ["ver", "col", "tex", "ele"]) {
+                ctx.deleteBuffer(bo[k]);
+                ctx.deleteBuffer(bo.disableCullFace[k]);
+            }
+        });
+    };
+};
+
+export {
+    ChunksModule,
+    ChunksModule as default,
+};

+ 63 - 0
legacy/WebMC/src/Renderer/WorldRenderer.js

@@ -0,0 +1,63 @@
+import { Render } from "./Render.js";
+import { Camera } from "./Camera.js";
+import * as glsl from "./glsl.js";
+import { Block } from "../World/Block.js";
+import { ChunksModule } from "./WorldChunkModule.js";
+import { HighlightSelectedBlock } from "./HighlightSelectedBlock.js";
+
+class WorldRenderer extends Render {
+    constructor(canvas, world = null) {
+        super(canvas);
+        this.fitScreen();
+        new ResizeObserver(async e => {
+            await new Promise(s => setTimeout(s, 0));
+            this.fitScreen();
+        }).observe(canvas);
+        const {ctx} = this;
+        ctx.clearColor(0.62, 0.81, 1.0, 1.0);
+        ctx.clearDepth(1.0);
+        ctx.clear(ctx.COLOR_BUFFER_BIT | ctx.DEPTH_BUFFER_BIT);
+        ctx.enable(ctx.DEPTH_TEST);
+        ctx.depthFunc(ctx.LEQUAL);
+        ctx.enable(ctx.CULL_FACE);
+        ctx.frontFace(ctx.CCW);
+        this.mainCamera = new Camera(this.aspectRatio, { fovy: 70, pitch: -90 * Math.PI / 180, position: [0, 20, 0] });
+        this.addCamera(this.mainCamera);
+        if (this.isWebGL2)
+            this.createProgram("showBlock", glsl.showBlock_webgl2.vert, glsl.showBlock_webgl2.frag)
+                .use().bindTex("blockTex", this.createTextureArray(Block.defaultBlockTextureImg));
+        else
+            this.createProgram("showBlock", glsl.showBlock.vert, glsl.showBlock.frag)
+                .use().bindTex("blockTex", this.createTexture(Block.defaultBlockTextureImg));
+        this.createProgram("selector", glsl.selector.vert, glsl.selector.frag);
+        if (world !== null) this.setWorld(world);
+    };
+    setWorld(world) {
+        if ((!world) || world === this.world) return;
+        world.setRenderer(this);
+        this.world = world;
+        this.mainCamera.bindEntity(world.mainPlayer);
+        this.chunksModule = new ChunksModule(world, this);
+        this.blockHighlight = new HighlightSelectedBlock(world, this);
+    };
+    onRender(timestamp, dt) {
+        this.world.update(dt);
+        this.chunksModule.update();
+        const {ctx} = this;
+        ctx.clear(ctx.COLOR_BUFFER_BIT | ctx.DEPTH_BUFFER_BIT);
+        this.chunksModule.draw();
+        this.blockHighlight.draw();
+        ctx.flush();
+    };
+    dispose() {
+        super.dispose();
+        if (!this.world) return;
+        this.chunksModule.dispose();
+        this.blockHighlight.dispose();
+    };
+};
+
+export {
+    WorldRenderer,
+    WorldRenderer as dafault
+};

+ 185 - 0
legacy/WebMC/src/Renderer/glsl.js

@@ -0,0 +1,185 @@
+
+export let showBlock_webgl2 = {
+    vert: `#version 300 es
+        precision highp int;
+        precision highp float;
+        in vec3 position;
+        in vec4 color;
+        in vec3 textureCoord;
+        uniform   mat4 mvpMatrix;
+        out   vec4 vColor;
+        out   vec3 vTextureCoord;
+        void main(void) {
+            vColor = color;
+            vTextureCoord  = textureCoord;
+            gl_Position    = mvpMatrix * vec4(position, 1.0);
+        }`,
+    frag: `#version 300 es
+        precision highp int;
+        precision highp float;
+        precision highp sampler2DArray;
+        uniform sampler2DArray blockTex;
+        in vec4      vColor;
+        in vec3      vTextureCoord;
+        out vec4 fragmentColor;
+        void main(void){
+            vec4 smpColor = texture(blockTex, vTextureCoord);
+            if (smpColor.a <= 0.3) discard;
+            fragmentColor  = vColor * smpColor;
+        }`
+};
+
+export let showBlock = {
+    // normal保存顶点的法线信息    invMatrix接受模型变换矩阵的逆矩阵    lightDirection接受光的方向
+    // vec3  invLight = normalize(invMatrix * vec4(lightDirection, 0.0)).xyz;
+    // float diffuse  = clamp(dot(normal, invLight), 0.1, 1.0);
+    // vColor         = color * vec4(vec3(diffuse), 1.0);
+    // 用来计算光系数
+    // normalize是内置函数 作用是将向量标准化【即化为长度为1的向量
+    vert: `
+        attribute vec3 position;
+        attribute vec4 color;
+        attribute vec3 textureCoord;
+        uniform   mat4 mvpMatrix;
+        varying   vec4 vColor;
+        varying   vec3 vTextureCoord;
+        void main(void) {
+            vColor = color;
+            vTextureCoord  = textureCoord;
+            gl_Position    = mvpMatrix * vec4(position, 1.0);
+        }`,
+    // fs中的vColor是vs中传进来的
+    // precision指定精确度 此为精密度中的float
+    frag: `
+        #ifdef GL_FRAGMENT_PRECISION_HIGH
+        precision highp float;
+        #else
+        precision mediump float;
+        #endif
+        uniform sampler2D blockTex;
+        varying vec4      vColor;
+        varying vec3      vTextureCoord;
+        void main(void){
+            vec4 smpColor = texture2D(blockTex, vTextureCoord.xy);
+            if (smpColor.a <= 0.3) discard;
+            gl_FragColor  = vColor * smpColor;
+        }`
+};
+
+export let selector = {
+    vert: `
+        attribute vec3 pos;
+        attribute vec4 col;
+        uniform   mat4 mvp;
+        varying   vec4 vCol;
+        void main(void) {
+            vCol = col;
+            gl_Position = mvp * vec4(pos, 1.0);
+        }`,
+    frag: `
+        precision mediump float;
+        varying vec4 vCol;
+        void main(void) {
+            gl_FragColor = vCol;
+        }`
+};
+
+export let blockInventoryTexure_webgl2 = {
+    vert: `#version 300 es
+        precision highp int;
+        precision highp float;
+        in vec3 position;
+        in vec4 normal;
+        in vec4 color;
+        in vec3 textureCoord;
+        uniform mat4 mvpMatrix;
+        uniform mat4 normalMatrix;
+        uniform vec3 diffuseLightDirection;     // need normalize
+        uniform vec3 diffuseLightColor;
+        uniform vec3 ambientLightColor;
+        out   vec4 vColor;
+        out   vec3 vTextureCoord;
+        void main(void) {
+            gl_Position    = mvpMatrix * vec4(position, 1.0);
+            vTextureCoord  = textureCoord;
+            vec4 nor = normalMatrix * normal;
+            vec3 nor2 = normalize(nor.xyz);
+            // normal dot light direction
+            float nDotL = max(dot(diffuseLightDirection, nor2), 0.0);
+            vec3 diffuse = diffuseLightColor * color.rgb * nDotL;
+            vec3 ambient = ambientLightColor * color.rgb;
+            vColor = vec4(diffuse + ambient, color.a);
+        }`,
+    frag: `#version 300 es
+        precision highp int;
+        precision highp float;
+        precision highp sampler2DArray;
+        uniform sampler2DArray blockTex;
+        in vec4      vColor;
+        in vec3      vTextureCoord;
+        out vec4 fragmentColor;
+
+        void main(void){
+            vec4 smpColor = texture(blockTex, vTextureCoord);
+            if (smpColor.a == 0.0) discard;
+            fragmentColor  = vColor * smpColor;
+        }`
+};
+export let blockInventoryTexure = {
+    vert: `
+        attribute vec3 position;
+        attribute vec4 normal;
+        attribute vec4 color;
+        attribute vec3 textureCoord;
+        uniform mat4 mvpMatrix;
+        uniform mat4 normalMatrix;
+        uniform vec3 diffuseLightDirection;     // need normalize
+        uniform vec3 diffuseLightColor;
+        uniform vec3 ambientLightColor;
+        varying   vec4 vColor;
+        varying   vec3 vTextureCoord;
+        void main(void) {
+            gl_Position    = mvpMatrix * vec4(position, 1.0);
+            vTextureCoord  = textureCoord;
+            vec4 nor = normalMatrix * normal;
+            vec3 nor2 = normalize(nor.xyz);
+            // normal dot light direction
+            float nDotL = max(dot(diffuseLightDirection, nor2), 0.0);
+            vec3 diffuse = diffuseLightColor * color.rgb * nDotL;
+            vec3 ambient = ambientLightColor * color.rgb;
+            vColor = vec4(diffuse + ambient, color.a);
+        }`,
+    frag: `
+        #ifdef GL_FRAGMENT_PRECISION_HIGH
+        precision highp float;
+        #else
+        precision mediump float;
+        #endif
+        uniform sampler2D blockTex;
+        varying vec4      vColor;
+        varying vec3      vTextureCoord;
+
+        void main(void){
+            vec4 smpColor = texture2D(blockTex, vTextureCoord.xy);
+            if (smpColor.a == 0.0) discard;
+            gl_FragColor  = vColor * smpColor;
+        }`
+};
+
+export let welcomePage = {
+    vert: `
+        attribute vec3 aPosition;
+        uniform   mat4 uMvpMatrix;
+        varying   vec3 vNoraml;
+        void main(void){
+            vNoraml = normalize(aPosition);
+            gl_Position = uMvpMatrix * vec4(aPosition, 1.0);
+        }`,
+    frag: `
+        precision lowp float;
+        uniform samplerCube uTexture;
+        varying vec3 vNoraml;
+        void main(void){
+            gl_FragColor = textureCube(uTexture, vNoraml);
+        }`
+};

+ 104 - 0
legacy/WebMC/src/UI/Component.js

@@ -0,0 +1,104 @@
+
+import { asyncLoadResByUrl, setResource, waitResource } from "../utils/loadResources.js";
+const mcComponents = {};
+setResource("MCComponent", mcComponents);
+
+class MCComponent extends HTMLElement {
+    static setBorderAndWaitImg(uri, styleSelector = "." + uri, styleDeclarations = {}) {
+        if (!("preloadStyleRules" in this)) this.preloadStyleRules = [];
+        if (!uri.startsWith("mc-ui-")) uri = "mc-ui-" + uri + "-img";
+        const setIfNotExist = obj => Object.entries(obj).forEach(([property, value]) =>
+            styleDeclarations[property] = styleDeclarations[property] || value);
+        setIfNotExist({
+            "border-color": "transparent",
+            "border-style": "solid",
+            "border-image": `var(--${uri})
+                var(--${uri}-border-top)
+                var(--${uri}-border-right)
+                var(--${uri}-border-bottom)
+                var(--${uri}-border-left)
+                fill stretch`,
+        });
+        let rule = styleSelector + " {\n"
+            + Object.entries(styleDeclarations).map(([property, value]) => `${property}: ${value};`).join("\n")
+            + "\n}";
+        this.preloadStyleRules.push(rule);
+        return waitResource(uri);
+    };
+    static setBackgroundAndWaitImg(uri, styleSelector = "." + uri, styleDeclarations = {}) {
+        if (!("preloadStyleRules" in this)) this.preloadStyleRules = [];
+        if (!uri.startsWith("mc-ui-")) uri = "mc-ui-" + uri + "-img";
+        const setIfNotExist = obj => Object.entries(obj).forEach(([property, value]) =>
+            styleDeclarations[property] = styleDeclarations[property] || value);
+        setIfNotExist({
+            "background-image": `var(--${uri})`,
+            "background-size": "cover",
+            "background-repeat": "no-repeat",
+        });
+        let rule = styleSelector + " {\n"
+            + Object.entries(styleDeclarations).map(([property, value]) => `${property}: ${value};`).join("\n")
+            + "\n}";
+        this.preloadStyleRules.push(rule);
+        return waitResource(uri);
+    };
+    static genTemplate(text, id = text) {
+        if (mcComponents[id]) return mcComponents[id];
+        let template = document.createElement("template");
+        template.innerHTML = (this.preloadStyleRules
+            ? "<style> " + this.preloadStyleRules.join("\n") + "</style>"
+            : "") + text;
+        mcComponents[id] = template;
+        return template;
+    };
+    static asyncLoadTemplateByUrl(url = this.templateUrl) {
+        return asyncLoadResByUrl(url).then(text => {
+            if (typeof text !== "string") return text;
+            let tmp = this.genTemplate(text, url);
+            setResource(url, tmp);
+            return tmp;
+        });
+    };
+    static define(componentName = this.componentName) {
+        if (!componentName) throw "Component registration failed: Missing componentName.";
+        return customElements.define(componentName, this);
+    };
+    static asyncLoadAndDefine() {
+        return this.asyncLoadTemplateByUrl().then(_ => this.define());
+    };
+    static getTemplateByUrl(url = this.templateUrl) {
+        return mcComponents[url];
+    };
+    get template() { return this.constructor.getTemplateByUrl(); };
+    appendTemplate(template = this.template) {
+        return template.content
+            ? this.shadowRoot.appendChild(template.content.cloneNode(true))
+            : null;
+    };
+    constructor() {
+        super();
+        this.attachShadow({mode: 'open'});
+        if (this.template) this.appendTemplate();
+    };
+    async connectedCallback() {};
+    async disconnectedCallback() {};
+    async adoptedCallback() {};
+    dispatchEvent(type, {
+        global = false,
+        data = {}
+    } = {}) {
+        return type instanceof Event
+        ? super.dispatchEvent(type)
+        : super.dispatchEvent(new CustomEvent(type, {
+            ...(global? {
+                bubbles: true,
+                cancelable: true,
+                composed: true,
+            }: {}),
+            detail: data,
+        }));
+    };
+};
+
+export {
+    MCComponent,
+};

+ 65 - 0
legacy/WebMC/src/UI/HowToPlayPage.html

@@ -0,0 +1,65 @@
+
+<!-- mcpage-how-to-play -->
+<style>
+    :host {
+        text-align: center;
+    }
+    div.mc-background {
+        height: 100vh;
+        background-image: var(--mc-ui-background-img);
+        background-repeat: repeat;
+        background-size: auto;
+    }
+    div.mc-background[darken] {
+        background-image: var(--mc-ui-background-darken-img);
+    }
+    div.mc-background > div {
+        position: relative;
+        top: 50%;
+        transform: translateY(-50%);
+    }
+    h1 {
+        margin: 0;
+        padding: 20px 0 0;
+    }
+    table {
+        margin: 20px auto;
+        background-color: rgba(0, 0, 0, .5);
+    }
+    tr {
+        font-size: 1rem;
+        vertical-align: top;
+        text-align: left;
+    }
+    tr:first-child {
+        vertical-align: top;
+    }
+    tr b {
+        float: right;
+        text-align: end;
+    }
+    td:first-child { padding-right: 10px; }
+    td:not(:first-child) {
+        padding-bottom: 5px;
+        padding-left: 10px;
+        width: 50%;
+    }
+    mc-button {
+        padding: 1.2vh 0;
+        width: 50%;
+        max-width: 400px;
+    }
+</style>
+<div class="mc-background" darken><div>
+    <h1>How to play</h1>
+    <table>
+        <tr><td><b>WASD</b></td>                   <td>Move; Double-click W to Sprint</td></tr>
+        <tr><td><b>Spacebar</b></td>               <td>Jump; Double-click to switch Fly Mode</td></tr>
+        <tr><td><b>Shift/X</b></td>                <td>When Fly Mode is on, causes the player to lose altitude (descend)</td></tr>
+        <tr><td><b>ESC</b></td>                    <td>Gives back cursor control</td></tr>
+        <tr><td><b>E</b></td>                      <td>Open inventory</td></tr>
+        <tr><td><b>Left/Right button</b></td>      <td>Destroy/Place blocks</td></tr>
+        <tr><td><b>Scroll</b></td>                 <td>Scrolls through the Hotbar</td></tr>
+    </table>
+    <mc-button gotoPage="*pop">Back</mc-button>
+</div></div>

+ 15 - 0
legacy/WebMC/src/UI/HowToPlayPage.js

@@ -0,0 +1,15 @@
+
+import { Page } from "./Page.js";
+
+class HowToPlayPage extends Page {
+    static get shortPageID() { return "how-to-play"; };
+    static get templateUrl() { return "src/UI/HowToPlayPage.html"; };
+    onHistoryBack() { this.close(); };
+}
+
+HowToPlayPage.asyncLoadAndDefine();
+
+
+export {
+    HowToPlayPage,
+};

+ 25 - 0
legacy/WebMC/src/UI/LoadTerrainPage.html

@@ -0,0 +1,25 @@
+
+<!-- mcpage-load-terrain -->
+<style>
+    :host {
+        text-align: center;
+        z-index: 100;
+    }
+    div.mc-background {
+        height: 100vh;
+        background-image: var(--mc-ui-background-img);
+        background-repeat: repeat;
+        background-size: auto;
+    }
+    div.mc-background[darken] {
+        background-image: var(--mc-ui-background-darken-img);
+    }
+    p {
+        margin: 0;
+        padding: 45vh 0 20px;
+    }
+</style>
+<div class="mc-background" darken>
+    <p id="gen-out"></p>
+    <progress></progress>
+</div>

+ 32 - 0
legacy/WebMC/src/UI/LoadTerrainPage.js

@@ -0,0 +1,32 @@
+
+import { Page, pm } from "./Page.js";
+
+import { World } from "../World/World.js";
+import { WorldRenderer } from "../Renderer/WorldRenderer.js";
+
+const sleep = ms => new Promise(s => window.setTimeout(s, ms));
+
+class LoadTerrainPage extends Page {
+    static get shortPageID() { return "load-terrain"; };
+    static get templateUrl() { return "src/UI/LoadTerrainPage.html"; };
+    async connectedCallback() {
+        await super.connectedCallback();
+        let p = this.shadowRoot.getElementById("gen-out");
+        p.innerHTML = "Generating terrain...";
+        await sleep(70);
+        let world = new World();
+        p.innerHTML = "Ready to render...";
+        await sleep(70);
+        let canvas = pm.getPageByID("play").mainCanvas;
+        let renderer = new WorldRenderer(canvas, world);
+        pm.dispatchEvent("load-terrain.loaded", {world, renderer});
+        this.close();
+    };
+}
+
+LoadTerrainPage.asyncLoadAndDefine();
+
+
+export {
+    LoadTerrainPage,
+};

+ 26 - 0
legacy/WebMC/src/UI/MCButton.html

@@ -0,0 +1,26 @@
+
+<!-- mc-button -->
+<style>
+    :host {
+        display: inline-block;
+        text-align: center;
+        color: #E0E0E0;
+        cursor: pointer;
+        text-shadow: 0.13rem 0.13rem 0 #383838;
+        padding: 0.5em;
+        --mc-ui-button-border-width: calc(3px * var(--mc-ui-button-img-border-top));
+        border-width: var(--mc-ui-button-border-width);
+    }
+    :host(:hover) {
+        color: #FFFFA0;
+        text-shadow: 0.13rem 0.13rem 0 #3F3F28;
+    }
+    :host(:active) {
+        color: #979797;
+    }
+    :host([disabled]) {
+        color: #979797;
+        cursor: not-allowed;
+    }
+</style>
+<slot></slot>

+ 39 - 0
legacy/WebMC/src/UI/MCButton.js

@@ -0,0 +1,39 @@
+
+import { MCComponent } from "./Component.js";
+
+import { pm } from "./Page.js";
+
+class MCButton extends MCComponent {
+    static get componentName() { return "mc-button"; };
+    static get templateUrl() { return "src/UI/MCButton.html" };
+    constructor() {
+        super();
+        this.addEventListener("click", e => {
+            if (this.disabled || !this.hasAttribute("gotoPage")) return false;
+            let pageID = this.getAttribute("gotoPage");
+            pm.openPageByID(pageID);
+        });
+    };
+    static get observedAttributes() { return ["disabled", "value"]; };
+    attributeChangedCallback(name, oldValue, newValue) {
+        if (name == "value")
+            this.shadowRoot.querySelector("slot").innerHTML = newValue;
+    };
+    get disabled() { return this.hasAttribute("disabled"); };
+    set disabled(val) {
+        if (val) this.setAttribute("disabled", "");
+        else this.removeAttribute("disabled");
+    };
+};
+
+MCButton.setBorderAndWaitImg("button", ":host");
+MCButton.setBorderAndWaitImg("button-hover", ":host(:hover)");
+MCButton.setBorderAndWaitImg("button-active", ":host(:active)");
+MCButton.setBorderAndWaitImg("button-disabled", ":host([disabled])");
+
+MCButton.asyncLoadAndDefine();
+
+
+export {
+    MCButton,
+};

+ 31 - 0
legacy/WebMC/src/UI/MCCrosshairs.js

@@ -0,0 +1,31 @@
+
+import { MCComponent } from "./Component.js";
+
+class MCCrosshairs extends MCComponent {
+    static get componentName() { return "mc-crosshairs"; };
+    get template() {
+        return MCComponent.genTemplate(`
+            <style>
+                :host {
+                    background-image: url(texture/icons.png);
+                    background-size: 100% 100%;
+                    background-repeat: no-repeat;
+                    pointer-events: none;
+                    display: block;
+                    width: 32px; height: 32px;
+                    margin: auto auto;
+                    position: absolute;
+                    left: 0; top: 0;
+                    bottom: 0; right: 0;
+                }
+            </style>
+        `);
+    };
+};
+
+MCCrosshairs.define();
+
+export {
+    MCCrosshairs,
+};
+

+ 71 - 0
legacy/WebMC/src/UI/MCFullScreenBtn.js

@@ -0,0 +1,71 @@
+
+import { MCButton } from "./MCButton.js";
+import { pm } from "./Page.js";
+
+const requestFullscreen = document.body.requestFullscreen || document.body.mozRequestFullScreen || document.body.webkitRequestFullScreen || document.body.msRequestFullscreen;
+const isFullscreen = () => document.body === (document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement);
+document.body.onfullscreenchange =
+document.body.onmozfullscreenchange =
+document.body.onwebkitfullscreenchange =
+document.body.MSFullscreenChange = function(e) {
+    if (e.target === null) return;
+    const isFull = isFullscreen();
+    pm.dispatchEvent("onfullscreenchange", isFull);
+};
+
+class MCFullScreenButton extends MCButton {
+    static get componentName() { return "mc-full-screen-button"; };
+    constructor() {
+        super();
+        this.onfullscreenchange = this.onfullscreenchange.bind(this);
+        this.onclick = this.onclick.bind(this);
+        pm.addEventListener("onfullscreenchange", this.onfullscreenchange);
+        this.addEventListener("click", this.onclick);
+    };
+    onfullscreenchange(isFull) {
+        this.style.display = isFull? "none": "";
+    };
+    async connectedCallback() {
+        await super.connectedCallback();
+        this.innerHTML = "full screen";
+        this.style.position = "absolute";
+        this.style.top = this.style.right = "10px";
+        this.style.display = window.isTouchDevice && !isFullscreen()? "": "none";
+    };
+    async disconnectedCallback() {
+        await super.disconnectedCallback();
+        pm.removeEventListener("onfullscreenchange", this.onfullscreenchange);
+        this.removeEventListener("click", this.onclick);
+    };
+    onclick() {
+        requestFullscreen.call(document.body).then(async _ => {
+            try {
+                await screen.orientation.lock("landscape");
+            } catch (e) {
+                console.warn(e);
+                let lockOrientation = screen.lockOrientation || screen.mozLockOrientation || screen.msLockOrientation;
+                if (lockOrientation) lockOrientation.call(screen, "landscape");
+            }
+            await new Promise(res => setTimeout(res, 200));
+            if (["landscape-primary", "landscape-secondary", "landscape"].includes(screen.orientation.type))
+                return;
+            // cannot lock screen orientation to landscape
+            // remove all callback hooks and exit fullscreen
+            document.documentElement.onfullscreenchange =
+            document.documentElement.onmozfullscreenchange =
+            document.documentElement.onwebkitfullscreenchange =
+            document.documentElement.MSFullscreenChange = null;
+            const exitFullscreen = document.exitFullscreen || document.webkitExitFullscreen || document.mozCancelFullScreen || document.msExitFullscreen;
+            exitFullscreen.call(document);
+            this.style.display = "none";
+        }, err => {
+            console.warn(err);
+        });
+    };
+}
+
+customElements.whenDefined(MCButton.componentName).then(_ => MCFullScreenButton.define());
+
+export {
+    MCFullScreenButton,
+};

+ 119 - 0
legacy/WebMC/src/UI/MCHotbar.html

@@ -0,0 +1,119 @@
+
+<!-- mc-hotbar -->
+<style>
+    :host {
+        --hotbar-width: 45vw;
+        --mc-ui-hotbar-scale-factor-per-pixel: calc(var(--hotbar-width) / var(--mc-ui-hotbar-background-img-width));
+
+        --bg-img-width: var(--mc-ui-hotbar-background-img-width);
+        --bg-img-height: var(--mc-ui-hotbar-background-img-height);
+        --selector-bg-img-width: var(--mc-ui-hotbar-selector-background-img-width);
+        --selector-bg-img-height: var(--mc-ui-hotbar-selector-background-img-height);
+        --offset: 0;
+
+        position: fixed;
+        bottom: 0; left: 0;
+        margin-left: calc((100vw - var(--hotbar-width)) / 2);
+    }
+    .showname {
+        pointer-events: none;
+        position: absolute;
+        left: 0; right: 0;
+        top: -2.5em;
+        text-align: center;
+        color: #fff;
+        text-shadow: 1px 1px 1px #000;
+        opacity: 1;
+    }
+    .showname.fadeout {
+        transition: opacity 0.5s 2s;
+        opacity: 0;
+    }
+    .hotbar-background {
+        opacity: 0.75;
+        width: var(--hotbar-width);
+        height: calc(var(--mc-ui-hotbar-scale-factor-per-pixel) * var(--bg-img-height));
+    }
+    .selector-background {
+        position: absolute;
+        --width-one-pixel: var(--mc-ui-hotbar-scale-factor-per-pixel);
+        --height-one-pixel: calc(100% / var(--bg-img-height));
+        width: calc(var(--width-one-pixel) * var(--selector-bg-img-width));
+        height: calc(var(--width-one-pixel) * var(--selector-bg-img-height));
+        top: calc(-1 * var(--height-one-pixel));
+
+        left: calc(var(--offset) * var(--mc-ui-hotbar-item-cell-width) * var(--width-one-pixel) - var(--width-one-pixel));
+    }
+    #items {
+        position: absolute;
+        top: 0;
+        width: var(--hotbar-width);
+        height: 100%;
+    }
+    #items img {
+        -webkit-user-drag: none;
+        user-drag: none;
+        width: calc(15 * var(--mc-ui-hotbar-scale-factor-per-pixel));
+        height: auto;
+        cursor: pointer;
+        padding:
+            calc(4 * var(--mc-ui-hotbar-scale-factor-per-pixel))
+            calc(2 * var(--mc-ui-hotbar-scale-factor-per-pixel))
+            calc(3 * var(--mc-ui-hotbar-scale-factor-per-pixel))
+            calc(3 * var(--mc-ui-hotbar-scale-factor-per-pixel));
+    }
+    #items img:first-child {
+        margin-left: calc(1 * var(--mc-ui-hotbar-scale-factor-per-pixel));
+    }
+    #items img:hover {
+        filter: brightness(1.5);
+    }
+    #items img:active {
+        filter: brightness(0.7);
+    }
+    #items img:not([src]) {
+        height: 100%;
+        padding-top: 0; padding-bottom: 0;
+        opacity: 0;
+    }
+    :host, #items { z-index: 5; }
+    .selector-background { z-index: 4; }
+
+    .inventory-btn {
+        --width-one-pixel: var(--mc-ui-hotbar-scale-factor-per-pixel);
+        z-index: 4;
+        position: absolute;
+        top: 0;
+        width: calc(var(--width-one-pixel) * (var(--mc-ui-hotbar-background-img-height) - 1));
+        left: calc(9 * var(--mc-ui-hotbar-item-cell-width) * var(--width-one-pixel) + var(--width-one-pixel));
+    }
+    .inventory-btn.hotbar-background {
+        background-position: 100%;
+    }
+    .inventory-btn > * {
+        width: calc(16 * var(--mc-ui-hotbar-scale-factor-per-pixel));
+        height: calc(16 * var(--mc-ui-hotbar-scale-factor-per-pixel));
+        position: absolute;
+        top: calc(3 * var(--mc-ui-hotbar-scale-factor-per-pixel));
+        left: calc(2 * var(--mc-ui-hotbar-scale-factor-per-pixel));
+    }
+    .inventory-btn > .background {
+        width: calc(14 * var(--mc-ui-hotbar-scale-factor-per-pixel));
+        height: calc(14 * var(--mc-ui-hotbar-scale-factor-per-pixel));
+        border-width: var(--mc-ui-hotbar-scale-factor-per-pixel);
+    }
+    .inventory-btn > .foreground {
+        z-index: 5;
+        cursor: pointer;
+        background-size: 75%;
+        background-position: center;
+    }
+</style>
+<div class="showname"></div>
+<div class="hotbar-background"></div>
+<div id="items"></div>
+<div class="inventory-btn hotbar-background" style="display: none;">
+    <div class="background"></div>
+    <div class="foreground"></div>
+</div>
+<div class="selector-background"></div>

+ 109 - 0
legacy/WebMC/src/UI/MCHotbar.js

@@ -0,0 +1,109 @@
+
+import { MCComponent } from "./Component.js";
+// import { Block } from "../World/Block.js";
+
+const normIndex = (i, len) => (i + len) % len;
+
+class MCHotbar extends MCComponent {
+    static get componentName() { return "mc-hotbar"; };
+    static get templateUrl() { return "src/UI/MCHotbar.html"; };
+    constructor() {
+        super();
+        this.itemsDOM = this.shadowRoot.getElementById("items");
+        this.shownameBox = this.shadowRoot.querySelector(".showname");
+        this.inventoryBtn = this.shadowRoot.querySelector(".inventory-btn");
+        this.inventoryBtn.addEventListener("click", e => {
+            e.preventDefault();
+            this.dispatchEvent(new Event("inventoryBtnClick"));
+            return false;
+        });
+        this.length = 9;
+        this.blocks = [];
+        for (let i = 0; i < this.length; ++i) {
+            let img = document.createElement("img");
+            img.onclick = e => this.updateSelector(i);
+            img.draggable = false;
+            this.itemsDOM.append(img);
+        }
+    };
+    get selectOn() { return 1 * getComputedStyle(this).getPropertyValue("--offset"); };
+    async connectedCallback() {
+        await super.connectedCallback();
+        let showInventoryBtn = this.hasAttribute("showInventoryBtn") || window.isTouchDevice;
+        this.inventoryBtn.style.display = showInventoryBtn? "": "none";
+    };
+    static get observedAttributes() { return ["offset", "inventory_btn_active"]; };
+    attributeChangedCallback(name, oldValue, newValue) {
+        switch (name) {
+        case "inventory_btn_active": {
+            this.activeInventoryBtn(this.hasAttribute(name));
+            break; }
+        case "offset": {
+            this.style.setProperty("--offset", newValue);
+            break; }
+        }
+    };
+    activeInventoryBtn(bool) {
+        if (bool) {
+            this.inventoryBtn.setAttribute("active", "");
+            if (!this.hasAttribute("inventory_btn_active")) this.setAttribute("inventory_btn_active", "");
+        }
+        else {
+            this.inventoryBtn.removeAttribute("active");
+            if (this.hasAttribute("inventory_btn_active")) this.removeAttribute("inventory_btn_active");
+        }
+    };
+    updateSelector(i = this.selectOn) {
+        i = normIndex(i, this.length);
+        this.style.setProperty("--offset", i);
+        this.showName();
+        this.dispatchEvent("selectBlock", { global: true, data: this.blocks[i], });
+    };
+    setItem(block, index = this.selectOn) {
+        index = normIndex(index, this.length);
+        const { blocks, itemsDOM } = this;
+        blocks[index] = block;
+
+        itemsDOM.innerHTML = "";
+        for (let i = 0; i < this.length; ++i) {
+            let b = blocks[i];
+            let img = b
+                ? b.texture.inventory.cloneNode()
+                : document.createElement("img");
+            img.draggable = false;
+            img.onclick = e => this.updateSelector(i);
+            itemsDOM.append(img);
+        }
+        this.updateSelector();
+    };
+    getSelectedItem(index = this.selectOn) {
+        return this.blocks[index];
+    };
+    selectPrev() {
+        this.updateSelector(this.selectOn + 1);
+    };
+    selectNext() {
+        this.updateSelector(this.selectOn - 1);
+    };
+    showName(index = this.selectOn) {
+        const block = this.blocks[index];
+        if (!block || block.name == "air") return;
+        const shownameBox = this.shownameBox;
+        shownameBox.innerHTML = block.showName;
+        shownameBox.classList.remove("fadeout");
+        setTimeout(() => {shownameBox.classList.add("fadeout");}, 10);
+    };
+};
+
+MCHotbar.setBackgroundAndWaitImg("hotbar-background", ".hotbar-background");
+MCHotbar.setBackgroundAndWaitImg("hotbar-selector-background", ".selector-background");
+MCHotbar.setBackgroundAndWaitImg("hotbar-inventory-btn-foreground", ".inventory-btn > .foreground");
+MCHotbar.setBorderAndWaitImg("hotbar-inventory-btn-bg", ".inventory-btn > .background");
+MCHotbar.setBorderAndWaitImg("hotbar-inventory-btn-bg-active", ".inventory-btn[active] > .background, .inventory-btn > .background[active]");
+
+MCHotbar.asyncLoadAndDefine();
+
+
+export {
+    MCHotbar,
+};

+ 91 - 0
legacy/WebMC/src/UI/MCInventory.html

@@ -0,0 +1,91 @@
+
+<!-- mc-incentory -->
+<style>
+    :host {
+        overflow: hidden;
+        position: absolute;
+        background-color: rgba(0, 0, 0, 0.5);
+        width: 100%; height: 100vh;
+        font-size: 0;
+        text-align: center;
+        display: table;
+    }
+    :host > div {
+        display: table-cell;
+        vertical-align: middle;
+    }
+    [class*="mc-inventory-tabs"],
+    .mc-inventory-items {
+        display: inline-block;
+        vertical-align: top;
+        position: relative;
+    }
+    .mc-inventory-items {
+        width: 50%; max-height: 65vh;
+        overflow-y: overlay;
+        text-align: left;
+        padding: 10px 30px;
+    }
+    .mc-inventory-item-background {
+        display: inline-block;
+        width: 11.111111%;
+        padding-top: 11.111111%;
+        /* width: 64px; height: 64px; */
+        position: relative;
+    }
+    .mc-inventory-item-background:hover {
+        filter: brightness(1.5);
+    }
+    .mc-inventory-item-background:active {
+        filter: brightness(0.7);
+    }
+    .mc-inventory-item-background img {
+        -webkit-user-drag: none;
+        user-drag: none;
+        width: 75%; height: 75%;
+        position: absolute;
+        top: 0; bottom: 0;
+        left: 0; right: 0;
+        margin: auto;
+    }
+    .mc-inventory-tab {
+        position: relative;
+    }
+    .mc-inventory-tab-item {
+        position: absolute;
+        top: 0;
+        width: 100%; height: 100%;
+    }
+    [class*="mc-inventory-tab-background"] {
+        width: 64px; height: 64px;
+        position: relative;
+    }
+    [class*="mc-inventory-tab-background"],
+    .mc-inventory-items { border-width: calc(4px * var(--mc-ui-inventory-items-img-border-top)); }
+    [class*="mc-inventory-tabs"] { width: calc(64px + 4px * var(--mc-ui-inventory-items-img-border-top) * 2); }
+    /* Overlap 2 pixels */
+    .mc-inventory-tabs-left  { left: calc(4px * 2); }
+    .mc-inventory-tabs-right { right: calc(4px * 2); }
+    .mc-inventory-tab-item * {
+        position: absolute;
+        top: 0; bottom: 0; left: 0; right: 0;
+        width: 60%; height: 60%;
+        margin: auto;
+    }
+
+    :host { z-index: 5; }
+    .mc-inventory-items { z-index: 6; }
+    [class*="mc-inventory-tab-background"] { z-index: 5; }
+    [class*="mc-inventory-tab-background"][active] { z-index: 7; }
+    .mc-inventory-tab-item>* { z-index: 8; }
+</style>
+<div>
+    <div class="mc-inventory-tabs-left"></div>
+    <div class="mc-inventory-items"></div>
+    <div class="mc-inventory-tabs-right">
+        <div class="mc-inventory-tab">
+            <div class="mc-inventory-tab-background-right" active></div>
+            <div class="mc-inventory-tab-item"><div class="mc-close-btn"></div></div>
+        </div>
+    </div>
+</div>

+ 45 - 0
legacy/WebMC/src/UI/MCInventory.js

@@ -0,0 +1,45 @@
+
+import { MCComponent } from "./Component.js";
+
+class MCInventory extends MCComponent {
+    static get componentName() { return "mc-inventory"; };
+    static get templateUrl() { return "src/UI/MCInventory.html"; };
+    constructor() {
+        super();
+        this.closeBtn = this.shadowRoot.querySelector(".mc-close-btn");
+        this.closeBtn.addEventListener("click", e => {
+            e.preventDefault();
+            this.dispatchEvent(new Event("closeBtnClick"));
+            return false;
+        });
+        this.itemList = this.shadowRoot.querySelector(".mc-inventory-items");
+    };
+    async connectedCallback() {
+        await super.connectedCallback();
+    };
+    appendItem(block) {
+        let div = document.createElement("div");
+        div.classList = "mc-inventory-item-background";
+        div.appendChild(block.texture.inventory.cloneNode()).draggable = false;
+        div.data = block;
+        div.onclick = e => {
+            e.preventDefault();
+            this.dispatchEvent("inventoryItemClick", { global: true, data: block });
+            return false;
+        };
+        this.itemList.append(div);
+    };
+};
+
+MCInventory.setBorderAndWaitImg("inventory-items", ".mc-inventory-items");
+MCInventory.setBorderAndWaitImg("inventory-tab-background-right", ".mc-inventory-tab-background-right");
+MCInventory.setBackgroundAndWaitImg("close-btn", ".mc-close-btn");
+MCInventory.setBackgroundAndWaitImg("close-btn-active", ".mc-close-btn:active");
+MCInventory.setBackgroundAndWaitImg("inventory-item-background", ".mc-inventory-item-background");
+
+MCInventory.asyncLoadAndDefine();
+
+
+export {
+    MCInventory,
+};

+ 56 - 0
legacy/WebMC/src/UI/MCMoveBtns.html

@@ -0,0 +1,56 @@
+
+<!-- mc-move-buttons -->
+<style>
+    :host {
+        --slice: 2.5;
+        --size: calc(1px * var(--slice) * var(--mc-ui-move-btn-up-img-width));
+        position: fixed;
+        bottom: 0;
+        width: calc(3 * var(--size));
+        height: calc(3 * var(--size));
+        opacity: 0.75;
+        display: block;
+    }
+    div {
+        width: var(--size); height: var(--size);
+        position: absolute;
+    }
+    #upleft {
+        left: calc(0 * var(--size));
+        top: calc(0 * var(--size));
+    }
+    #up, #flyup {
+        left: calc(1 * var(--size));
+        top: calc(0 * var(--size));
+    }
+    #upright {
+        left: calc(2 * var(--size));
+        top: calc(0 * var(--size));
+    }
+    #left {
+        left: calc(0 * var(--size));
+        top: calc(1 * var(--size));
+    }
+    #jump, #fly {
+        left: calc(1 * var(--size));
+        top: calc(1 * var(--size));
+    }
+    #right {
+        left: calc(2 * var(--size));
+        top: calc(1 * var(--size));
+    }
+    #down, #flydown {
+        left: calc(1 * var(--size));
+        top: calc(2 * var(--size));
+    }
+</style>
+<div id="upleft" style="display: none;"></div>
+<div id="up"></div>
+<div id="flyup" style="display: none;"></div>
+<div id="upright" style="display: none;"></div>
+<div id="left"></div>
+<div id="jump"></div>
+<div id="fly" style="display: none;"></div>
+<div id="right"></div>
+<div id="down"></div>
+<div id="flydown" style="display: none;"></div>

+ 117 - 0
legacy/WebMC/src/UI/MCMoveBtns.js

@@ -0,0 +1,117 @@
+
+import { MCComponent } from "./Component.js";
+
+class MCMoveButtons extends MCComponent {
+    static get componentName() { return "mc-move-buttons"; };
+    static get templateUrl() { return "src/UI/MCMoveBtns.html"; };
+    constructor() {
+        super();
+        this.lastBtnPressTime = {};
+        this.isPress = {};
+        this.btns = [];
+        for (let id of "up,left,down,right,jump,upleft,upright,flyup,flydown,fly,sneak".split(",")) {
+            this[id] = this.btns[id] = this.shadowRoot.getElementById(id);
+            this.lastBtnPressTime[id] = 0;
+            this.isPress[id] = false;
+            this.btns.push(this[id]);
+        }
+        this.onTouchMove = this.onTouchMove.bind(this);
+        this.onTouchEnd = this.onTouchEnd.bind(this);
+        // if use { passive: true }, mobile browser will vibration,
+        // else will get a warning
+        this.addEventListener("touchstart", this.onTouchMove);
+        this.addEventListener("touchend", this.onTouchEnd);
+        this.addEventListener("touchcancel", this.onTouchEnd);
+        this.addEventListener("touchmove", this.onTouchMove);
+    };
+    get size() { return 1 * getComputedStyle(this).getPropertyValue("--slice"); };
+    set size(v) { return this.style.setProperty("--slice", v); };
+    activeFlyBtn(bool) {
+        if (bool) {
+            if (this.jump.hasAttribute("active")) this.inactiveMoveBtn(this.jump);
+            this.jump.style.display = "none";
+            this.fly.style.display = "";
+            this.activeMoveBtn(this.fly, false);
+        }
+        else {
+            if (this.fly.hasAttribute("active")) this.inactiveMoveBtn(this.fly);
+            this.fly.style.display = "none";
+            this.jump.style.display = "";
+            this.activeMoveBtn(this.jump, false);
+        }
+    };
+    onTouchMove(e) {
+        if (e.cancelable) e.preventDefault();
+        const pressedBtns = Object.entries(this.isPress).filter(([id, isPress]) => isPress).map(([id]) => this[id]);
+        const targetBtns = Array.from(e.touches)
+            .map(touch => this.shadowRoot.elementFromPoint(touch.clientX, touch.clientY))
+            .filter(ele => this.btns.includes(ele));
+        targetBtns.forEach(btn => this.activeMoveBtn(btn));
+        pressedBtns.forEach(btn => {
+            if (!targetBtns.includes(btn))
+                this.inactiveMoveBtn(btn);
+        });
+    };
+    onTouchEnd(e) {
+        if (e.cancelable) e.preventDefault();
+        const targetBtns = Array.from(e.changedTouches)
+            .map(touch => this.shadowRoot.elementFromPoint(touch.clientX, touch.clientY))
+            .filter(ele => this.btns.includes(ele));
+        targetBtns.forEach(btn => this.inactiveMoveBtn(btn));
+    };
+    activeMoveBtn(btn, fireEvent = true) {
+        if (!btn || this.isPress[btn.id]) return;
+        btn.setAttribute("active", "");
+        switch (btn) {
+        case this.up: {
+            this.upleft.style.display = this.upright.style.display = "";
+            break; }
+        case this.fly: {
+            this.up.style.display = this.down.style.display = "none";
+            this.flyup.style.display = this.flydown.style.display = "";
+            break; }
+        }
+        this.isPress[btn.id] = true;
+        if (!fireEvent) return;
+        this.dispatchEvent(btn.id + "BtnPress", { global: true, data: btn, });
+        if (this.lastBtnPressTime[btn.id] === 0)
+            this.lastBtnPressTime[btn.id] = new Date();
+        else if ((new Date()) - this.lastBtnPressTime[btn.id] < 250) {
+            this.dispatchEvent(btn.id + "BtnDblPress", { global: true, data: btn, });
+            this.lastBtnPressTime[btn.id] = 0;
+        }
+        else this.lastBtnPressTime[btn.id] = new Date();
+    };
+    inactiveMoveBtn(btn, fireEvent = true) {
+        if (!btn || !this.isPress[btn.id]) return;
+        btn.removeAttribute("active");
+        switch (btn) {
+        case this.up: case this.upleft: case this.upright: {
+            if (this.up.hasAttribute("active") || this.upleft.hasAttribute("active") || this.upright.hasAttribute("active"))
+                break;
+            this.upleft.style.display = this.upright.style.display = "none";
+            break; }
+        case this.fly: case this.flyup: case this.flydown: {
+            if (this.fly.hasAttribute("active") || this.flyup.hasAttribute("active") || this.flydown.hasAttribute("active"))
+                break;
+            this.flyup.style.display = this.flydown.style.display = "none";
+            this.up.style.display = this.down.style.display = "";
+            break; }
+        }
+        this.isPress[btn.id] = false;
+        if (!fireEvent) return;
+        this.dispatchEvent(btn.id + "BtnUp", { global: true, data: btn });
+    };
+};
+
+for (let name of "up,left,down,right,jump,upleft,upright,flyup,flydown,fly,sneak".split(",")) {
+    MCMoveButtons.setBackgroundAndWaitImg(`mc-ui-move-btn-${name}-img`, `#${name}`);
+    MCMoveButtons.setBackgroundAndWaitImg(`mc-ui-move-btn-${name}-active-img`, `#${name}[active]`);
+}
+
+MCMoveButtons.asyncLoadAndDefine();
+
+
+export {
+    MCMoveButtons,
+};

+ 135 - 0
legacy/WebMC/src/UI/Page.js

@@ -0,0 +1,135 @@
+
+import { MCComponent } from "./Component.js";
+import { edm } from "../utils/EventDispatcher.js";
+
+import { FSM } from "../utils/FiniteStateMachine.js";
+
+edm.getOrNewEventDispatcher("mc.preload")
+.addEventListener("done", () => {
+    history.pushState(null, document.title);
+    window.addEventListener("popstate", e => {
+        history.pushState(null, document.title);
+        window.dispatchEvent(new Event("back"));
+    }, false);
+    window.addEventListener("exit", e => {
+        history.go(-2);
+    });
+}, { once: true, });
+
+class PageManager extends FSM {
+    constructor() {
+        super({
+            id: "pageManager",
+            initial: "preload",
+            transitions: [
+                { from: "preload", to: "welcome", },
+                { from: "welcome", to: "play", },
+                { from: "welcome", to: "how-to-play", },
+                { from: "welcome", to: "setting", },
+                { from: "play", to: "load-terrain", },
+                { from: "play", to: "pause", },
+                { from: "load-terrain", to: "play", },
+                { from: "pause", to: "play", },
+                { from: "pause", to: "welcome", },
+                { from: "pause", to: "setting", },
+                { from: "setting", to: "welcome", },
+                { from: "setting", to: "pause", },
+                { from: "how-to-play", to: "welcome", },
+            ],
+        });
+        edm.addEventDispatcher("mc.page", this);
+    };
+    getCurrentPage() { return document.body.lastChild; };
+    getPageByID(pageID) {
+        pageID = "mcpage-" + pageID;
+        let page = [...document.body.childNodes].reverse().find(page => page.pageID == pageID);
+        return page || null;
+    };
+    openPageByID(pageID) {
+        if (pageID === "*pop") return this.closeCurrentPage();
+        let currentPage = this.getCurrentPage();
+        let page = document.createElement("mcpage-" + pageID);
+        document.body.appendChild(page);
+        this.dispatchEvent("open", page);
+        this.transition(pageID, currentPage, page);
+        return page;
+    };
+    closePage(pageID) {
+        if (pageID === "*pop") return this.closeCurrentPage();
+        let page = this.getPageByID(pageID);
+        if (!page) return null;
+        if (page === this.getCurrentPage()) return this.closeCurrentPage();
+        document.body.removeChild(page);
+        this.dispatchEvent("close", page);
+        return page;
+    };
+    closeCurrentPage() {
+        let page = this.getCurrentPage();
+        document.body.removeChild(page);
+        this.dispatchEvent("close", page);
+        let nowPage = this.getCurrentPage();
+        this.transition(nowPage.shortPageID, page, nowPage);
+        return page;
+    };
+};
+
+const pageManager = new PageManager();
+
+let pageStyle = document.createElement("style");
+pageStyle.innerHTML = `
+    :host {
+        width: 100vw;
+        height: 100vh;
+        position: absolute;
+        top: 0; left: 0;
+    }
+`;
+pageStyle.id = "pageStyle";
+class Page extends MCComponent {
+    static get pageID() { return "mcpage-" + this.shortPageID; };
+    static get componentName() { return this.pageID; };
+    get shortPageID() { return this.constructor.shortPageID; };
+    get pageID() { return this.constructor.pageID; };
+    constructor() {
+        super();
+        this.onHistoryBack = this.onHistoryBack.bind(this);
+        this._transitionedCallbackID =
+        pageManager.addEventListener("transitioned", (from, to, en, [fromPage, toPage]) => {
+            if (fromPage === this) {
+                this.onTransitionedFromThis(to, en, toPage);
+                window.removeEventListener("back", this.onHistoryBack);
+            }
+            else if (toPage === this) {
+                this.onTransitionedToThis(from, en, fromPage);
+                window.addEventListener("back", this.onHistoryBack);
+            }
+        });
+    };
+    onTransitionedFromThis(to, en, toPage) {};
+    onTransitionedToThis(from, en, fromPage) {};
+    onHistoryBack() {};
+    async disconnectedCallback() {
+        await super.disconnectedCallback();
+        pageManager.removeEventListenerByID(this._transitionedCallbackID);
+        window.removeEventListener("back", this.onHistoryBack);
+    };
+    appendTemplate(template = this.template) {
+        let tmp = super.appendTemplate(template);
+        if (!tmp) return;
+        this.shadowRoot.prepend(pageStyle.cloneNode(true));
+        customElements.whenDefined("mc-full-screen-button").then(_ => {
+            this.shadowRoot.append(document.createElement("mc-full-screen-button"));
+        });
+        return tmp;
+    };
+    close() {
+        // pageManager.dispatchEvent("close", this);
+        // this.parentElement.removeChild(this);
+        pageManager.closePage(this.shortPageID);
+    };
+};
+
+export {
+    Page,
+    pageManager, pageManager as pm,
+};

+ 28 - 0
legacy/WebMC/src/UI/PausePage.html

@@ -0,0 +1,28 @@
+
+<!-- mcpage-pause (old: stop_game_page) -->
+<style>
+    :host {
+        position: absolute;
+        width: 100%; height: 100vh;
+        background-color: rgba(0, 0, 0, 0.5);
+        z-index: 100;
+    }
+    div {
+        position: relative;
+        top: 50%;
+        transform: translateY(-50%);
+    }
+    mc-button {
+        text-align: center;
+        display: block;
+        padding: 1.2vh 0;
+        width: 50%;
+        max-width: 400px;
+        margin: 0 auto 10px;
+    }
+    </style>
+<div>
+    <mc-button gotoPage="*pop">Resume Game</mc-button>
+    <mc-button gotoPage="setting">Settings</mc-button>
+    <mc-button gotoPage="welcome">Quit</mc-button>
+</div>

+ 21 - 0
legacy/WebMC/src/UI/PausePage.js

@@ -0,0 +1,21 @@
+
+import { Page, pm } from "./Page.js";
+
+class PausePage extends Page {
+    static get shortPageID() { return "pause"; };
+    static get templateUrl() { return "src/UI/PausePage.html"; };
+    onHistoryBack() { this.close(); };
+};
+
+pm.addEventListener("pause=>welcome", (pause, welcome) => {
+    let play = pm.getPageByID("play");
+    play && play.close();
+    pause.close();
+});
+
+PausePage.asyncLoadAndDefine();
+
+
+export {
+    PausePage,
+};

+ 24 - 0
legacy/WebMC/src/UI/PlayPage.html

@@ -0,0 +1,24 @@
+
+<!-- mcpage-play (old: play_game_page) -->
+<style>
+    #mainCanvas {
+        position: absolute;
+        width: 100vw;
+        height: 100vh;
+    }
+    #mc-f3-out {
+        position: absolute;
+        z-index: 10;
+        text-shadow: 0px 0px 4px #000;
+        white-space: pre;
+        color: #fff;
+        margin: 0 10px;
+        pointer-events: none;
+    }
+</style>
+<canvas id="mainCanvas">Your browser does not support canvas. Please upgrade your browser.</canvas>
+<mc-crosshairs></mc-crosshairs>
+<mc-move-buttons></mc-move-buttons>
+<mc-inventory style="display: none;"></mc-inventory>
+<mc-hotbar></mc-hotbar>
+<div id="mc-f3-out"></div>

+ 90 - 0
legacy/WebMC/src/UI/PlayPage.js

@@ -0,0 +1,90 @@
+
+import { Page, pm } from "./Page.js";
+import { PlayerLocalController } from "../Entity/PlayerLocalController.js";
+let worldRenderer = null, world = null;
+
+pm.addEventListener("load-terrain.loaded", ({world: w, renderer}) => {
+    world = w;
+    worldRenderer = renderer;
+    pm.getPageByID("play").playerLocalController.setEntity(world.mainPlayer);
+});
+
+class PlayPage extends Page {
+    static get shortPageID() { return "play"; };
+    static get templateUrl() { return "src/UI/PlayPage.html"; };
+    constructor() {
+        super();
+        this.moveButtons = this.shadowRoot.querySelector("mc-move-buttons");
+        this.inventory = this.shadowRoot.querySelector("mc-inventory");
+        this.hotbar = this.shadowRoot.querySelector("mc-hotbar");
+        this.mainCanvas = this.shadowRoot.getElementById("mainCanvas");
+        this.debugOutput = this.shadowRoot.getElementById("mc-f3-out");
+    };
+    async connectedCallback() {
+        await super.connectedCallback();
+        this.hotbar.addEventListener("inventoryBtnClick", e => {
+            if (this.isShownInventory) this.closeInventory();
+            else this.showInventory();
+        });
+        this.inventory.addEventListener("closeBtnClick", e => {
+            this.closeInventory();
+        });
+        this.inventory.addEventListener("inventoryItemClick", e => {
+            this.hotbar.setItem(e.detail);
+            this.hotbar.showName();
+        });
+        if (!window.isTouchDevice) {
+            this.moveButtons.style.display = "none";
+        }
+        else {
+            this.hotbar.setAttribute("showInventoryBtn", "");
+        }
+        this.playerLocalController = new PlayerLocalController(null, { playPage: this, });
+        if (worldRenderer === null) pm.openPageByID("load-terrain");
+    };
+    async disconnectedCallback() {
+        await super.disconnectedCallback();
+        if (!worldRenderer) return;
+        worldRenderer.dispose();
+        this.playerLocalController.dispose();
+        worldRenderer = world = null;
+    };
+    get isShownInventory() { return this.inventory.style.display !== "none"; };
+    showInventory() {
+        this.inventory.style.display = "";
+        this.hotbar.activeInventoryBtn(true);
+        this.dispatchEvent(new Event("showInventory"));
+    };
+    closeInventory() {
+        this.inventory.style.display = "none";
+        this.hotbar.activeInventoryBtn(false);
+        this.dispatchEvent(new Event("closeInventory"));
+    };
+};
+
+const onHistoryBack = e => pm.openPageByID("pause");
+pm.addEventListener("onfullscreenchange", isFull => {
+    if (window.isTouchDevice && !isFull && pm.getCurrentPage().pageID === PlayPage.pageID) {
+        window.removeEventListener("back", onHistoryBack);
+        pm.openPageByID("pause");
+    }
+});
+
+pm.addEventListener("load-terrain=>play", () => {
+    worldRenderer.play();
+    pm.openPageByID("pause");
+});
+pm.addEventListener("pause=>play", (pause, play) => {
+    window.addEventListener("back", onHistoryBack, {once: true});
+    worldRenderer.play();
+});
+pm.addEventListener("play=>pause", (play, pause) => {
+    worldRenderer.stop();
+});
+
+PlayPage.asyncLoadAndDefine();
+
+
+export {
+    PlayPage,
+};

+ 93 - 0
legacy/WebMC/src/UI/PreloadPage.js

@@ -0,0 +1,93 @@
+
+import { Page, pm } from "./Page.js";
+
+import { edm } from "../utils/EventDispatcher.js";
+import { RESOURCES } from "../utils/loadResources.js";
+
+let preloadIsDone = false,
+    numOfResLoaded = 0,
+    totalNumOfResNeedBeLoaded = 0;
+const resCount = {};
+
+const sleep = ms => new Promise(s => setTimeout(s, ms));
+edm.getOrNewEventDispatcher("mc.load")
+.addEventListener("newAwaitRes", function onNewAwaitRes(url, promise) {
+    let page = document.getElementsByTagName("mcpage-preload")[0],
+        loadingList = page.querySelector("[slot=loading]"),
+        loadedList = page.querySelector("[slot=loaded]"),
+        li;
+    if (!(url in resCount)) {
+        ++totalNumOfResNeedBeLoaded;
+        page.setProgress(numOfResLoaded, totalNumOfResNeedBeLoaded);
+        li = document.createElement("li");
+        loadingList.appendChild(li);
+    }
+    else li = [...loadingList.childNodes].find(l => l.innerHTML.startsWith(url));
+    resCount[url] = (resCount[url] || 0) + 1;
+    li.innerHTML = url + " (" + resCount[url] + ")";
+    promise.then(async _ => {
+        await sleep(600);
+        li.innerHTML = url + " (" + --resCount[url] + ")";
+        if (resCount[url] == 0) {
+            ++numOfResLoaded;
+            page.setProgress(numOfResLoaded, totalNumOfResNeedBeLoaded);
+            loadedList.appendChild(li);
+        }
+        if (preloadIsDone) return;
+        for (let url in resCount) if (resCount[url]) return;
+        preloadIsDone = true;
+        edm.getOrNewEventDispatcher("mc.preload").dispatchEvent("done", RESOURCES);
+        edm.getOrNewEventDispatcher("mc.load").removeEventListener("newAwaitRes", onNewAwaitRes);
+    }, err => {
+        console.error("preload failed: " + url, err);
+    });
+});
+
+edm.getOrNewEventDispatcher("mc.preload")
+.addEventListener("done", async () => {
+    await sleep(777);
+    pm.openPageByID("welcome");
+    const preloadPage = document.querySelector("mcpage-preload");
+    preloadPage.style.opacity = 0;
+    await sleep(1100);
+    pm.closePage("preload");
+}, { once: true, });
+
+// To display preload page as soon as possible,
+// this page is a special case of moving elements to template after display.
+const template = document.createElement("template");
+let firstTimeOpen = true;
+class PreloadPage extends Page {
+    static get shortPageID() { return "preload"; };
+    get template() { return firstTimeOpen? null: template; };
+    async connectedCallback() {
+        await super.connectedCallback();
+        if (!firstTimeOpen) return;
+        firstTimeOpen = false;
+        let style = document.getElementById(this.pageID);
+        let reg = new RegExp(this.pageID, "g");
+        style.innerHTML = style.innerHTML.replace(reg, ":host");
+        style.removeAttribute("id");
+        style.parentElement.removeChild(style);
+        template.content.appendChild(style);
+        [...this.querySelectorAll("mcpage-preload > :not([slot])")]
+            .forEach(dom => template.content.appendChild(dom));
+        template.id = this.pageID;
+        this.appendTemplate(document.head.appendChild(template));
+    };
+    async setProgress(value, max) {
+        const root = firstTimeOpen? this: this.shadowRoot;
+        let progress = root.querySelector("progress");
+        progress.value = value;
+        progress.max = max;
+        root.querySelector(".loadingCount").innerHTML = value;
+        root.querySelector(".loadedCount").innerHTML = max;
+    };
+}
+
+PreloadPage.define();
+
+
+export {
+    PreloadPage,
+};

+ 34 - 0
legacy/WebMC/src/UI/SettingPage.html

@@ -0,0 +1,34 @@
+
+<!-- mcpage-setting (old: options_page) -->
+<style>
+    :host {
+        text-align: center;
+        z-index: 200;
+    }
+    div.mc-background {
+        height: 100vh;
+        background-image: var(--mc-ui-background-img);
+        background-repeat: repeat;
+        background-size: auto;
+    }
+    div.mc-background[darken] {
+        background-image: var(--mc-ui-background-darken-img);
+    }
+    h1 {
+        margin: 0;
+        padding: 20px 0 0;
+    }
+    mc-button {
+        padding: 1.2vh 0;
+        width: 50%;
+        max-width: 400px;
+        margin-bottom: 10px;
+    }
+</style>
+<div class="mc-background" darken>
+    <h1>Options</h1>
+    <h2>Terrain type:</h2>
+    <mc-button class="world-terrain">flat</mc-button><br />
+    <mc-button class="world-terrain">pre-classic</mc-button><br />
+    <mc-button gotoPage="*pop">Back</mc-button>
+</div>

+ 30 - 0
legacy/WebMC/src/UI/SettingPage.js

@@ -0,0 +1,30 @@
+
+import { Page } from "./Page.js";
+import { World } from "../World/World.js";
+
+class SettingPage extends Page {
+    static get shortPageID() { return "setting"; };
+    static get templateUrl() { return "src/UI/SettingPage.html"; };
+    constructor() {
+        super();
+        this.worldTerrainBtns = this.shadowRoot.querySelectorAll(".world-terrain");
+        for (let btn of this.worldTerrainBtns) {
+            if (btn.innerHTML == World.config.terrain)
+                btn.setAttribute("disabled", "true");
+            btn.onclick = (e) => {
+                btn.setAttribute("disabled", "");
+                for (let b of this.worldTerrainBtns)
+                    if (b !== btn) b.removeAttribute("disabled");
+                World.config.terrain = btn.innerHTML;
+            };
+        }
+    };
+    onHistoryBack() { this.close(); };
+};
+
+SettingPage.asyncLoadAndDefine();
+
+
+export {
+    SettingPage,
+};

+ 60 - 0
legacy/WebMC/src/UI/WelcomePage.html

@@ -0,0 +1,60 @@
+
+<!-- mcpage-welcome (old: start_game_page) -->
+<style>
+    canvas {
+        position: absolute;
+        z-index: -1;
+        height: 100vh;
+        width: 100vw;
+        filter: blur(calc(3.5px / var(--device-pixel-ratio)));
+    }
+    img {
+        width: 100%;
+        height: auto;
+        margin: 10vh 0 10vh;
+    }
+    ul {
+        list-style-type: none;
+        padding: 0;
+        margin: 0 30%;
+    }
+    ul li {
+        margin: 0 0 2vh;
+        width: 100%;
+        background-color: #888;
+        text-align:center;
+    }
+    ul li mc-button {
+        width: calc(100% - 2 * var(--mc-ui-button-border-width));
+        display: inline-block;
+        padding: 2vh 0;
+    }
+    .mc-bouncing-text {
+        color: yellow;
+        font-size: 1.2rem;
+        text-shadow: 0.16rem 0.16rem 0 #404000;
+        position: absolute;
+        right: 24vw;
+        top: calc(100vw / 512 * 50 + 10vh);
+        transform: rotate(-25deg);
+        animation: mc-bouncing-text-bounce 0.4s infinite;
+    }
+    a.mc-bouncing-text {
+        text-decoration: underline;
+    }
+    @media (min-width: 767px) { .mc-bouncing-text { font-size: 1.5rem; text-shadow: 0.2rem 0.2rem 0 #404000; } }
+    @media (min-width: 1024px) { .mc-bouncing-text { font-size: 2rem; text-shadow: 0.26rem 0.26rem 0 #404000; } }
+    @media (min-width: 1440px) { .mc-bouncing-text { font-size: 2.5rem; text-shadow: 0.33rem 0.33rem 0 #404000; } }
+    @media (min-width: 2560px) { .mc-bouncing-text { font-size: 3rem; text-shadow: 0.4rem 0.4rem 0 #404000; } }
+    @keyframes mc-bouncing-text-bounce {
+        50% { transform: rotate(-25deg) scale(1.1); }
+    }
+</style>
+<canvas id="background-canvas">Your browser does not support canvas. Please upgrade your browser.</canvas>
+<img width="512" height="64" src="./texture/title.png" />
+<a class="mc-bouncing-text" href="https://github.com/jerrychan7/WebMC">View in Github</a>
+<ul>
+    <li><mc-button gotoPage="play">Start</mc-button></li>
+    <li><mc-button gotoPage="how-to-play">How to play</mc-button></li>
+    <li><mc-button gotoPage="setting">Setting</mc-button></li>
+</ul>

+ 47 - 0
legacy/WebMC/src/UI/WelcomePage.js

@@ -0,0 +1,47 @@
+
+import { Page } from "./Page.js";
+
+import { WelcomeRenderer } from "../Renderer/WelcomePageRenderer.js";
+
+class WelcomePage extends Page {
+    static get shortPageID() { return "welcome"; };
+    static get templateUrl() { return "src/UI/WelcomePage.html"; };
+    constructor() {
+        super();
+        this.bgCanvas = this.shadowRoot.getElementById("background-canvas");
+        this.renderer = null;
+    };
+    async connectedCallback() {
+        await super.connectedCallback();
+        this.renderer = new WelcomeRenderer(this.bgCanvas);
+        this.renderer.play();
+    };
+    async disconnectedCallback() {
+        await super.disconnectedCallback();
+        this.renderer.dispose();
+        this.renderer = null;
+    };
+    onTransitionedFromThis(to) {
+        switch(to) {
+        case "play": {
+            this.close();
+            break; }
+        case "how-to-play": case "setting": {
+            this.renderer.stop();
+            break; }
+        }
+    };
+    onTransitionedToThis() {
+        if (this.renderer) this.renderer.play();
+    };
+    onHistoryBack() {
+        window.dispatchEvent(new Event("exit"));
+    };
+};
+
+WelcomePage.asyncLoadAndDefine();
+
+
+export {
+    WelcomePage,
+};

+ 17 - 0
legacy/WebMC/src/UI/index.js

@@ -0,0 +1,17 @@
+
+// pages
+export * from "./PreloadPage.js";
+export * from "./WelcomePage.js";
+export * from "./HowToPlayPage.js";
+export * from "./SettingPage.js";
+export * from "./LoadTerrainPage.js";
+export * from "./PausePage.js";
+export * from "./PlayPage.js";
+
+// mc components
+export * from "./MCButton.js";
+export * from "./MCHotbar.js";
+export * from "./MCCrosshairs.js";
+export * from "./MCFullScreenBtn.js";
+export * from "./MCInventory.js";
+export * from "./MCMoveBtns.js";

+ 237 - 0
legacy/WebMC/src/World/Block.js

@@ -0,0 +1,237 @@
+import { asyncLoadResByUrl } from "../utils/loadResources.js";
+import { textureMipmapByTile, prepareTextureAarray, blockInventoryTexture } from "../processingPictures.js";
+
+class LongID extends Number {
+    constructor(id = 0, bd = 0) {
+        super(bd << 16 | id);
+    };
+    get id() { return this & 0xFFFF; };
+    get bd() { return this >>> 16 };
+};
+
+let defaultBlockTextureImg = null, blocksCfg = null;
+
+const BlockRenderType = {
+    NORMAL: Symbol("block render type: normal"),
+    FLOWER: Symbol("block render type: flower"),
+    CACTUS: Symbol("block render type: cactus"),
+    FLUID: Symbol("block render type: fluid"),
+};
+// BLOCKS: block name -> block      blockIDs: block id -> [db] -> block
+const BLOCKS = {}, blockIDs = new Map();
+
+class Block {
+    constructor(blockName, {
+        opacity = 15,
+        luminance = 0,
+        renderType = Block.renderType.NORMAL,
+        stackable = 64,
+        textureImg = defaultBlockTextureImg,
+        texture: textureCoord = [[16, 32]],
+        friction = 1,
+        id = blockIDs.size,
+        bd = 0,
+        showName = blockName.toLowerCase().replace(/_/g, " ").replace(/^\w|\s\w/g, w => w.toUpperCase()),
+        isLeaves = blockName.endsWith("leaves"),
+        isGlass = blockName.endsWith("glass"),
+        isFluid = renderType == Block.renderType.FLUID,
+        maxLevel = 8,
+        ...others
+    } = {}) {
+        this.name = blockName;
+        this.isFluid = isFluid;
+        if (isFluid) this.maxLevel = maxLevel - 1;
+        // if (renderType == Block.renderType.FLUID)
+        //     renderType = Block.renderType.NORMAL;
+        this.renderType = renderType;
+        this.vertexs = Block.getVertexsByRenderType(renderType);
+        this.elements = Block.getElementsByRenderType(renderType);
+        this.texture = { img: textureImg, uv: {} };
+        this.initTexUV(textureCoord);
+        this.opacity = opacity;
+        this.luminance = luminance;
+        this.stackable = stackable;
+        this.friction = friction;
+        this.id = id;
+        this.bd = bd;
+        this.longID = new LongID(id, bd);
+        if (blockIDs.has(id)) blockIDs.get(id)[bd] = this;
+        else {
+            let t = []; t[bd] = this;
+            blockIDs.set(id, t);
+        }
+        BLOCKS[blockName] = this;
+        this.texture.inventory = blockInventoryTexture(this);
+        this.showName = showName;
+        this.isLeaves = isLeaves; this.isGlass = isGlass;
+        for (let k in others) this[k] = others[k];
+    };
+
+    get isOpaque() { return this.opacity === 15; };
+
+    initTexUV(texCoord = this.texture.textureCoord) {
+        for (let texture of texCoord) {
+            let [x, y] = texture;
+            texture[0] = y-1; texture[1] = x-1;
+        }
+        this.texture.coordinate = texCoord;
+        let {texture: {img: texImg, uv, coordinate}} = this;
+        let xsize = 1 / 32, ysize = 1 / 16,
+            calculateOffset = i => texImg.mipmap? i / 4: 0,
+            dx = texImg.texture4array? texImg.texture4array.tileCount[0]: calculateOffset(xsize),
+            dy = texImg.texture4array? texImg.texture4array.tileCount[1]: calculateOffset(ysize),
+            cr2uv = texImg.texture4array? ([x, y]) => [
+                0, 0, (x + y * dx),
+                0, 1, (x + y * dx),
+                1, 1, (x + y * dx),
+                1, 0, (x + y * dx),
+            ]: ([x, y]) => [
+                x*xsize+dx,     y*ysize+dy, 0,
+                x*xsize+dx,     (y+1)*ysize-dy, 0,
+                (x+1)*xsize-dx, (y+1)*ysize-dy, 0,
+                (x+1)*xsize-dx, y*ysize+dy, 0,
+            ];
+        switch (this.renderType) {
+            case BlockRenderType.CACTUS:
+            case BlockRenderType.NORMAL: {
+                if (coordinate.length === 1) {
+                    let uvw = cr2uv(coordinate[0]);
+                    "x+,x-,y+,y-,z+,z-".split(",").map(k => uv[k] = uvw);
+                }
+                else if (coordinate.length === 2) {
+                    uv["y+"] = uv["y-"] = cr2uv(coordinate[0]);
+                    let uvw = cr2uv(coordinate[1]);
+                    "x+,x-,z+,z-".split(",").forEach(k => uv[k] = uvw);
+                }
+                else if (coordinate.length === 3) {
+                    uv["y+"] = cr2uv(coordinate[0]);
+                    uv["y-"] = cr2uv(coordinate[1]);
+                    let uvw = cr2uv(coordinate[2]);
+                    "x+,x-,z+,z-".split(",").forEach(k => uv[k] = uvw);
+                }
+                else if (coordinate.length === 4) {
+                    uv["y+"] = cr2uv(coordinate[0]);
+                    uv["y-"] = cr2uv(coordinate[1]);
+                    uv["x+"] = uv["x-"] = cr2uv(coordinate[2]);
+                    uv["z+"] = uv["z-"] = cr2uv(coordinate[3]);
+                }
+                else if (coordinate.length === 6) {
+                    "x+,x-,y+,y-,z+,z-".split(",").forEach((k, i) => uv[k] = cr2uv(coordinate[i]));
+                }
+                else throw this.name + " texture translate error: array length";
+                break;
+            }
+            case BlockRenderType.FLOWER: {
+                if (coordinate.length > 2)
+                    throw this.name + " texture translate error: array length";
+                let uvw = cr2uv(coordinate[0]);
+                uv["face"] = [...uvw, ...uvw];
+                break;
+            }
+            case BlockRenderType.FLUID: {
+                if (coordinate.length > 2)
+                    throw this.name + " texture translate error: array length";
+                let uvw = cr2uv(coordinate[0]);
+                "x+,x-,y+,y-,z+,z-".split(",").forEach(k => uv[k] = uvw);
+            }
+        }
+    };
+
+    static get renderType() {
+        return BlockRenderType;
+    };
+    static getVertexsByRenderType(renderType) {
+        return blocksCfg.vertexs[renderType] || {};
+    };
+    static getElementsByRenderType(renderType) {
+        return Object.entries(this.getVertexsByRenderType(renderType)).map(([face, vs]) => ({[face]: (len => {
+            if (!len) return [];
+            let base = [0,1,2, 0,2,3], out = [];
+            for(let i=0,j=0; i<len; j=++i*4)
+                out.push(...base.map(x => x+j));
+            return out;
+        })(vs.length/12)})).reduce((ac, o) => ({...ac, ...o}), {});
+    };
+    static getBlockByBlockName(blockName) {
+        return BLOCKS[blockName] || null;
+    };
+    static getBlockByBlockIDandData(id, bd = 0) {
+        let blocks = blockIDs.get(id);
+        return blocks
+            ? bd < blocks.length? blocks[bd]: blocks[0]
+            : null;
+    };
+    static getBlockByBlockLongID(longID) {
+        longID = longID instanceof LongID? longID: new LongID(longID);
+        return this.getBlockByBlockIDandData(longID.id, longID.bd);
+    };
+    static listBlocks() {
+        return Object.values(BLOCKS);
+    };
+    static get defaultBlockTextureImg() {
+        return defaultBlockTextureImg;
+    };
+};
+
+Block.preloaded = Promise.all([
+asyncLoadResByUrl("texture/terrain-atlas.png").then(img => {
+    defaultBlockTextureImg = img;
+    if (isSupportWebGL2)
+        prepareTextureAarray(img);
+    else textureMipmapByTile(img);
+}),
+asyncLoadResByUrl("src/World/blocks.json").then(obj => {
+    // index_renderType = [index -> BlockRenderType[render type]]
+    let index_renderType = obj.index_renderType = [];
+    Object.entries(obj.block_renderType_index).forEach(([type, i]) => {
+        index_renderType[i] = BlockRenderType[type.toUpperCase()];
+    });
+    // blocksCfg.blocks.renderType = BlockRenderType[render type]
+    Object.entries(obj.blocks).forEach(([, block]) => {
+        if ("renderType" in block)
+            block.renderType = index_renderType[block.renderType];
+    });
+    let brtv = obj.block_renderType_vertex;
+    obj.vertexs = {
+        [BlockRenderType.NORMAL]:
+            ("x+:2763,x-:0541,y+:0123,y-:4567,z+:1472,z-:3650")
+            .split(",").map(s => s.split(":"))
+            .map(([face, vs]) => {
+                return ({[face]: [...vs].map(i => brtv.normal[i]).reduce((ac, d) => {ac.push(...d); return ac;},[])});
+            })
+            .reduce((ac, o) => ({...ac, ...o}), {}),
+        [BlockRenderType.FLOWER]:
+            ("face:14630572").split(",").map(s => s.split(":"))
+            .map(([face, vs]) => {
+                return ({[face]: [...vs].map(i => brtv.flower[i]).reduce((ac, d) => {ac.push(...d); return ac;},[])});
+            })
+            .reduce((ac, o) => ({...ac, ...o}), {}),
+        [BlockRenderType.CACTUS]:
+            ("x+:12 13 14 15,x-:20 21 22 23,y+:0 1 2 3,y-:4 5 6 7,z+:8 9 10 11,z-:16 17 18 19")
+            .split(",").map(s => s.split(":"))
+            .map(([face, vs]) => {
+                return ({[face]: vs.split(" ").map(i => brtv.cactus[i]).reduce((ac, d) => {ac.push(...d); return ac;},[])});
+            })
+            .reduce((ac, o) => ({...ac, ...o}), {}),
+        [BlockRenderType.FLUID]:
+            ("x+:2763,x-:0541,y+:0123,y-:4567,z+:1472,z-:3650")
+            .split(",").map(s => s.split(":"))
+            .map(([face, vs]) => {
+                return ({[face]: [...vs].map(i => brtv.fluid[i]).reduce((ac, d) => ac.concat(d), [])});
+            })
+            .reduce((ac, o) => ({...ac, ...o}), {}),
+    };
+    blocksCfg = obj;
+}),
+]).then(() => {
+    Object.entries(blocksCfg.blocks).forEach(([blockName, cfg]) => {
+        new Block(blockName, cfg);
+    });
+});
+
+
+export {
+    Block,
+    Block as default,
+    LongID,
+};

+ 224 - 0
legacy/WebMC/src/World/Block.txt

@@ -0,0 +1,224 @@
+这个文件是定义各个方块的
+应该用名字当索引而不是用id 这样为未来发展做准备 会大量减少id冲突
+大致构想是 服务器端按名字自动分配好id 然后将地图加载方式和方块列表发给客户端这样
+
+
+id bd 方块名 透明度 硬度 抗暴 最大堆叠上限 亮度 掉落 渲染类型
+定义一个方块
+"方块名":{
+    id: 方块id(数字 缺省时会根据注册时自动分配),
+    bd: 方块数据值(blockData)(缺省0),
+    renderType: 渲染模型 详见下(缺省0),
+    opacity: 不透明度 亮度衰减(缺省15,范围[0-15]),
+    luminance: 亮度(缺省0),
+    //blastResistance: 抗爆性,
+    //drops:掉落物,
+    stackable: 可堆叠数量(缺省64),
+    textureImg: url 材质图片 缺省使用默认材质,
+    texture:[] 材质数组 详见下(除空气外 其他缺省为丢失材质),
+    friction: 摩擦力(缺省1),
+    showName: 方块的显示名称(缺省时,名字的_被替换成空格,单词首字母大写),
+    isLeaves: 是否是树叶(缺省时,名字以leaves结尾时true,否则为false),
+    isGlass: 是否是玻璃(缺省时,名字以glass结尾时true,否则为false),
+    isFluid: 是否为流体(缺省时,渲染模型为fluid时true,否则false),
+    maxLevel: 当isFluid=true时有效,液体最远能传播多长(1~8,缺省8),
+}
+
+"normal":0,         普通方块
+"flower":1,         花,交叉状
+"torch":2,          火把
+"fluid":3,          流体
+"crop":4,           作物,井字形
+"door":5,           门
+"steps":6,          台阶,1*0.5*1
+"stairs":7,         楼梯,上字性
+"fence":8,          栏杆 栅栏
+"cactus":9,         仙人掌
+"bed":10,           床
+"glass_panes":11,   玻璃片
+"stem":12,          茎
+"fence_gate":13     栅栏门
+
+最后计算方块当前亮度的时候 :max(0,min(15,天光-方块不透明度+方块亮度)) 宽搜处理光照渲染
+两个不透明的方块紧挨着时 将不会渲染这两个挨着的面
+方块可以透明 但又同时有亮度衰减的效果 例如树叶和蜘蛛网
+楼梯和玻璃一样
+萤石是透明的 但遮光
+有不透明但又渲染旁边方块的例子吗? 没有 不透明指的就是不会渲染旁边方块
+存在亮度有衰减的透明方块,但有没有亮度衰减为15 但透明的方块【目前未发现 基于这点
+    将透明属性和不透明属性合并 不透明度为15既为不透明方块 其他的都视为透明方块(需要渲染相邻方块)
+不透明度为0 就是全透明 当空气处理
+
+
+方块的材质声明:
+指定材质的时候 用该材质相对于整个材质图片来说的坐标
+比如石头的材质在材质图片中是第1行第20列 那相对坐标就是1,20
+然后各个渲染模型对应的材质声明方式如下:
+
+渲染采用右手坐标系(z朝屏外)
+normal:
+          Y                          
+          ^                          v0: 0, 1, 0
+          |                          v1: 0, 1, 1
+          v0-------------v3          v2: 1, 1, 1
+         /¦             /|           v3: 1, 1, 0
+        / ¦            / |           v4: 0, 0, 1
+       v1-------------v2 |           v5: 0, 0, 0
+       |  ¦           |  |           v6: 1, 0, 0
+       |  ¦ /         |  |           v7: 1, 0, 1
+       |  ¦/          |  |           x+: v2763
+=======|==v5 - - - - -|- v6======> X x-: v0541
+       | '|           | /            y+: v0123
+       |' |           |/             y-: v4567
+       v4-------------v7             z+: v1472
+      /   |                          z-: v3650
+     L    |                          ibo = 0,1,2, 0,2,3
+    Z                                
+声明:
+    长度为1时六个面都是一种材质 如石头:[[1,20]]
+    长度为2时为[[y], [xz]] 如橡木:[[3,30],[3,29]]
+    长度为3时为[[y+], [y-], [xz]] 如草方块:[[1,3],[2,12],[1,4]]
+    长度为4时为[[y+],[y-], [x+-], [z+-]] 如工作台:[[5,6],[2,15],[5,7],[5,8]]
+    长度为6时对应 [[x+],[x-], [y+],[y-], [z+],[z-]]
+
+flower:
+          Y                          
+          ^                          
+          |                          v0: 0, 1, 0
+          v0._       _.~'v3          v1: 0, 1, 1
+          |   `_.~'^`    |           v2: 1, 1, 1
+         _.~'^` : `~._   |           v3: 1, 1, 0
+       v1 ¦     :     v2 |           v4: 0, 0, 1
+       |  ¦     :     |  |           v5: 0, 0, 0
+       |  ¦ /   :     |  |           v6: 1, 0, 0
+       |  ¦/    :     |  |           v7: 1, 0, 1
+-------|--v5._--:----_.~"v6------> X 
+       | /|   `_.-'^` |              face: v1463 v0572
+       |/_.~"^`   `~._|              
+       v4 |           v7             
+      /   |                          ibo = 0,1,2, 0,2,3
+     L                               
+    Z                                
+声明:
+    只有一个材质 如树苗[[3, 23]]
+
+cactus:
+v0: 0, 1, 0
+v1: 0, 1, 1
+v2: 1, 1, 1
+v3: 1, 1, 0
+v4: 0, 0, 1
+v5: 0, 0, 0
+v6: 1, 0, 0
+v7: 1, 0, 1
+v8:  0, 1, 15/16
+v9:  0, 0, 15/16
+v10: 1, 0, 15/16
+v11: 1, 1, 15/16
+v12: 15/16, 1, 1
+v13: 15/16, 0, 1
+v14: 15/16, 0, 0
+v15: 15/16, 1, 0
+v16: 1, 1, 1/16
+v17: 1, 0, 1/16
+v18: 0, 0, 1/16
+v19: 0, 1, 1/16
+v20: 1/16, 1, 0
+v21: 1/16, 0, 0
+v22: 1/16, 0, 1
+v23: 1/16, 1, 1
+x+: v12 13 14 15
+x-: v20 21 22 23
+y+: v0 1 2 3
+y-: v4 5 6 7
+z+: v8 9 10 11
+z-: v16 17 18 19
+ibo = 0,1,2, 0,2,3
+声明:
+    长度为1时六个面都是一种材质
+    长度为3时为[[y+], [y-], [xz]] 如仙人掌:[[6,9],[6,11],[6,10]]
+    长度为4时为[[y+],[y-], [x+z+], [x-z-]]
+    长度为6时对应 [[x+],[x-], [y+],[y-], [z+],[z-]]
+
+fluid:
+normal:
+          Y                          y指需要经过计算
+          ^                          v0: 0, y0, 0
+          |                          v1: 0, y1, 1
+          v0-------------v3          v2: 1, y2, 1
+         /:             /|           v3: 1, y3, 0
+        / :            / |           v4: 0, 0, 1
+       v1-------------v2 |           v5: 0, 0, 0
+       |  :           |  |           v6: 1, 0, 0
+       |  : /         |  |           v7: 1, 0, 1
+       |  :/          |  |           x+: v2763
+=======|--v5 - - - - -|- v6======> X x-: v0541
+       | '¦           | /            y+: v0123
+       |' ¦           |/             y-: v4567
+       v4-------------v7             z+: v1472
+      /   |                          z-: v3650
+     L    |                          ibo = 0,1,2, 0,2,3
+    Z                                
+声明:
+    只有一个材质 如水[[12, 1]]
+
+
+
+
+
+
+
+
+
+
+
+------------------------------------------------------------------------------------------
+
+下面?是给没能力的自己看的,明明NDS里是左手的,可是实现不了【数学基础过差,满足不了自己的欲望
+【算了 反正mc里是右手的【试图安慰自己【惨白无力
+渲染采用左手坐标系(z朝屏内)
+normal:
+   Y            Z                
+   ^           7                 v0: 0, 0, 0
+   |          /                  v1: 1, 0, 0
+   |  v6-------------v7          v2: 0, 1, 0
+   | /¦     /       /|           v3: 1, 1, 0
+   |/ ¦    /       / |           v4: 0, 0, 1
+   v2-------------v3 |           v5: 1, 0, 1
+   |  ¦  /        |  |           v6: 0, 1, 1
+   |  ¦ /         |  |           v7: 1, 1, 1
+   |  ¦/          |  |           x+: v3157
+   |  v4 - - - - -|- v5          x-: v6402
+   | '            | /            y+: v6237
+   |'             |/             y-: v0451
+===v0-------------v1======> X    z+: v7546
+  /|                             z-: v2013
+ / |                             ibo = 0,1,2, 0,2,3
+/  |                             
+声明:
+    长度为1时六个面都是一种材质 如石头:[[1,20]]
+    长度为3时为[[y+], [xz], [y-]] 如草方块:[[1,3],[2,12],[1,4]]
+    长度为6时对应 [[x+],[x-], [y+],[y-], [z+],[z-]]
+
+
+flower:
+   Y            Z                
+   ^           7                 
+   |          /                  v0: 0, 0, 0
+   |  v6._   /   _.~'v7          v1: 1, 0, 0
+   |  |   `_.~'^`    |           v2: 0, 1, 0
+   | _.~'^`/: `~._   |           v3: 1, 1, 0
+   v2 ¦   / :     v3 |           v4: 0, 0, 1
+   |  ¦  /  :     |  |           v5: 1, 0, 1
+   |  ¦ /   :     |  |           v6: 0, 1, 1
+   |  ¦/    :     |  |           v7: 1, 1, 1
+   |  v4._  :    _.~"v5          
+   | /    `_.-'^` |              face: v2057 v6413
+   |/_.~"^`   `~._|              
+---v0-------------v1------> X    
+  /|                             ibo = 0,1,2, 0,2,3
+ / |                             
+/  |                             
+声明:
+    只有一个材质 如树苗[[3, 23]]
+

+ 101 - 0
legacy/WebMC/src/World/Chunk.js

@@ -0,0 +1,101 @@
+import { Block, LongID } from "./Block.js";
+import { LightMap } from "./WorldLight.js";
+import { EventDispatcher } from "../utils/EventDispatcher.js";
+
+const SHIFT_X = 4, SHIFT_Y = 4, SHIFT_Z = 4,
+      X_SIZE = 1 << SHIFT_X,
+      Y_SIZE = 1 << SHIFT_Y,
+      Z_SIZE = 1 << SHIFT_Z;
+
+class Chunk extends EventDispatcher {
+    static get X_SIZE() { return X_SIZE; };
+    static get Y_SIZE() { return Y_SIZE; };
+    static get Z_SIZE() { return Z_SIZE; };
+    static getChunkXYZByBlockXYZ(blockX, blockY, blockZ) { return [blockX >> SHIFT_X, blockY >> SHIFT_Y, blockZ >> SHIFT_Z]; };
+    static chunkKeyByChunkXYZ(chunkX, chunkY, chunkZ) { return chunkX + "," + chunkY + "," + chunkZ; };
+    static chunkKeyByBlockXYZ(blockX, blockY, blockZ) { return (blockX >> SHIFT_X) + "," + (blockY >> SHIFT_Y) + "," + (blockZ >> SHIFT_Z); };
+    static getRelativeBlockXYZ(blockX, blockY, blockZ) {
+        const mod = (n, m) => (m + (n % m)) % m;
+        return [mod(blockX, X_SIZE), mod(blockY, Y_SIZE), mod(blockZ, Z_SIZE)];
+    };
+    // YZX = y * Y_SIZE * Z_SIZE + z * Z_SIZE + x
+    static getLinearBlockIndex(blockRX, blockRY, blockRZ) { return (((blockRY << SHIFT_Y) + blockRZ) << SHIFT_Z) + blockRX; };
+    // x = index % X_SIZE; z = ((index - x) / Z_SIZE) % Z_SIZE; y = (index - x - z * Z_size) / (Y_SIZE * Z_SIZE);
+    static getBlockRXYZBytLinearBlockIndex(index) {
+        let blockX = index & (X_SIZE - 1);
+        let blockZ = (index >> SHIFT_Z) & (Z_SIZE - 1);
+        let blockY = (index >> SHIFT_Y >> SHIFT_Z) & (Y_SIZE - 1);
+        return [blockX, blockY, blockZ];
+    };
+    static getChunkXYZByChunkKey(chunkKey) {
+        return chunkKey.split(",").map(Number);
+    };
+
+    constructor(world, chunkX, chunkY, chunkZ, renderer = world.renderer, generator = world.generator) {
+        super();
+        this.world = world;
+        this.x = chunkX; this.y = chunkY; this.z = chunkZ;
+        this.chunkKey = Chunk.chunkKeyByChunkXYZ(chunkX, chunkY, chunkZ);
+        this.tileMap = new Uint32Array(Y_SIZE * Z_SIZE * X_SIZE);
+        this.lightMap = new LightMap();
+        this.generator = generator;
+        generator(chunkX, chunkY, chunkZ, this.tileMap);
+        this.setRenderer(renderer);
+    };
+    setRenderer(renderer = null) {
+        if (!renderer) return;
+        this.renderer = renderer;
+    };
+    getTile(blockRX, blockRY, blockRZ) {
+        return new LongID(this.tileMap[Chunk.getLinearBlockIndex(blockRX, blockRY, blockRZ)]);
+    };
+    getBlock(blockRX, blockRY, blockRZ) {
+        return Block.getBlockByBlockLongID(this.getTile(blockRX, blockRY, blockRZ));
+    };
+    setTileByBlock(blockRX, blockRY, blockRZ, block, id = block?.id, bd = block?.bd) {
+        if (!block) return block;
+        let lbi = Chunk.getLinearBlockIndex(blockRX, blockRY, blockRZ);
+        let oldLongID = new LongID(this.tileMap[lbi]), oldBlock = Block.getBlockByBlockLongID(oldLongID);
+        let newLongID = new LongID(id, bd);
+        this.tileMap[lbi] = newLongID;
+        this.dispatchEvent("onTileChanges", blockRX, blockRY, blockRZ, {
+            longID: newLongID, block,
+        }, {
+            longID: oldLongID, block: oldBlock,
+        });
+        return block;
+    };
+    setTile(blockRX, blockRY, blockRZ, id, bd) {
+        return this.setTileByBlock(blockRX, blockRY, blockRZ, Block.getBlockByBlockIDandData(id, bd), id, bd);
+    };
+    setBlock(blockRX, blockRY, blockRZ, blockName) {
+        return this.setTileByBlock(blockRX, blockRY, blockRZ, Block.getBlockByBlockName(blockName));
+    };
+    getLight(blockRX, blockRY, blockRZ) {
+        return this.lightMap.getMax(blockRX, blockRY, blockRZ);
+    };
+    getSkylight(blockRX, blockRY, blockRZ) {
+        return this.lightMap.getSkylight(blockRX, blockRY, blockRZ);
+    };
+    getTorchlight(blockRX, blockRY, blockRZ) {
+        return this.lightMap.getTorchlight(blockRX, blockRY, blockRZ);
+    };
+    static inOtherChunk(blockRX, blockRY, blockRZ) {
+        return blockRX < 0 || blockRX >= X_SIZE || blockRZ < 0 || blockRZ >= Z_SIZE || blockRY < 0 || blockRY >= Y_SIZE;
+    };
+    inOtherChunk(blockRX, blockRY, blockRZ) {
+        return blockRX < 0 || blockRX >= X_SIZE || blockRZ < 0 || blockRZ >= Z_SIZE || blockRY < 0 || blockRY >= Y_SIZE;
+    };
+    blockRXYZ2BlockXYZ(blockRX, blockRY, blockRZ) {
+        return [blockRX + this.x * X_SIZE, blockRY + this.y * Y_SIZE, blockRZ + this.z * Z_SIZE];
+    };
+    update() {};
+};
+
+export {
+    Chunk,
+    Chunk as default,
+    X_SIZE as CHUNK_X_SIZE,
+    Y_SIZE as CHUNK_Y_SIZE,
+    Z_SIZE as CHUNK_Z_SIZE,
+};

+ 374 - 0
legacy/WebMC/src/World/World.js

@@ -0,0 +1,374 @@
+import Chunk from "./Chunk.js";
+import Block from "./Block.js";
+import Player from "../Entity/Player.js";
+import { vec3, radian2degree } from "../utils/gmath.js";
+import { PerlinNoise } from "./noise.js";
+import { FluidCalculator } from "./WorldFluidCal.js";
+import { ChunksLightCalculation } from "./WorldLight.js";
+import { asyncLoadResByUrl } from "../utils/loadResources.js";
+import { EventDispatcher } from "../utils/EventDispatcher.js";
+
+let worldDefaultConfig = {};
+asyncLoadResByUrl("src/World/worldDefaultConfig.json")
+.then(cfg => {
+    worldDefaultConfig = cfg;
+});
+
+class World extends EventDispatcher {
+    static get config() { return worldDefaultConfig; };
+
+    constructor({
+        worldName = "My World",
+        worldType = World.config.terrain,
+        renderer = null,
+        seed = Date.now(),
+    } = {}) {
+        super();
+        this.name = worldName;
+        this.type = worldType;
+        this.chunkMap = {};
+        this.callbacks = {};
+        this.mainPlayer = new Player(this);
+        this.entitys = [this.mainPlayer];
+        this.renderer = renderer;
+        this.seed = seed;
+        this.noise = new PerlinNoise(seed);
+        this.generator = this.generator.bind(this);
+        for (let x = -2; x <= 2; ++x)
+        for (let z = -2; z <= 2; ++z)
+        for (let y = 2; y >= -2; --y)
+            this.loadChunk(x, y, z);
+        this.fluidCalculator = new FluidCalculator(this);
+        this.lightingCalculator = new ChunksLightCalculation(this);
+        this.setRenderer(renderer);
+    };
+    generator(chunkX, chunkY, chunkZ, tileMap) {
+        switch(this.type) {
+        case "flat":
+            let block = Block.getBlockByBlockName(chunkY >= 0? "air":
+                chunkX%2? chunkZ%2? "grass": "stone"
+                : chunkZ%2? "stone": "grass");
+            for (let i = 0; i < tileMap.length; ++i)
+                tileMap[i] = block.longID;
+            break;
+        case "pre-classic": {
+            const {X_SIZE, Y_SIZE, Z_SIZE} = Chunk;
+            const noise = this.noise, fn = noise.gen2d.bind(noise);
+            let air = Block.getBlockByBlockName("air"),
+                stone = Block.getBlockByBlockName(
+                    chunkY%2
+                    ? chunkX%2
+                        ? chunkZ%2? "grass": "stone"
+                        : chunkZ%2? "stone": "grass"
+                    : chunkX%2
+                        ? chunkZ%2? "stone": "grass"
+                        : chunkZ%2? "grass": "stone"
+                );
+            let fn3 = noise.gen3d.bind(noise);
+            let elevations = [];
+            for (let x = 0; x < X_SIZE; ++x)
+            for (let z = 0; z < Z_SIZE; ++z) {
+                let i = chunkX * X_SIZE + x, k = chunkZ * Z_SIZE + z;
+                let elevation = (fn(i/200,k/200)+fn(i/50,k/50)/2+fn(i/10,k/10)/64)/2;
+                elevation = Math.floor(elevation * 128);
+                elevations[x] = elevations[x] || [];
+                elevations[x][z] = elevation;
+                for (let y = 0; y < Y_SIZE; ++y) {
+                    let j = chunkY * Y_SIZE + y;
+                    if (j < elevation) {
+                        let n3 = (fn3(i / 16, j / 16, k / 16));
+                        let n33 = (fn3(i/256,j/256,k/256) +fn3(i/128,j/128,k/128)/2 + fn3(i/64, j/64, k/64) / 4+fn3(i/25,j/25,k/25)/8);
+                        let vein = (fn3(i / 5, j / 5, k / 5) + 1) / 2;
+                        tileMap[Chunk.getLinearBlockIndex(x, y, z)] =
+                            (vein < 0.18? air: n33 > 0 && n3 < -0.1? air: stone).longID;
+                    }
+                    else {
+                        elevations.haveSurface = elevations.haveSurface || j === elevation;
+                        tileMap[Chunk.getLinearBlockIndex(x, y, z)] = air.longID;
+                    }
+                }
+            }
+            let treeNoise = [], R = 5;
+            for (let x = -R - 2; x < X_SIZE + R + 2; ++x)
+            for (let z = -R - 2; z < Z_SIZE + R + 2; ++z) {
+                let i = chunkX * X_SIZE + x, k = chunkZ * Z_SIZE + z;
+                treeNoise[x] = treeNoise[x] || [];
+                treeNoise[x][z] = (fn(k/10, i/10)+1)/2 + fn(k/5, i/5)/2;
+            }
+            let treePlacement = [], haveTreeAround = [];
+            for (let x = -2; x < X_SIZE + 2; ++x)
+            for (let z = -2; z < Z_SIZE + 2; ++z) {
+                treePlacement[x] = treePlacement[x] || [];
+                let max = -10;
+                for (let a = -R; a <= R; ++a)
+                for (let b = -R; b <= R; ++b) {
+                    let t = treeNoise[x + a][z + b];
+                    if (t > max) max = t;
+                }
+                treePlacement[x][z] = treeNoise[x][z] === max;
+            }
+            let afterEle = [];
+            for (let x = 0; x < X_SIZE; ++x)
+            for (let z = 0, y; z < Z_SIZE; ++z) {
+                afterEle[x] = afterEle[x] || [];
+                if (elevations[x][z] >= chunkY * Y_SIZE && elevations[x][z] < (chunkY + 1) * Y_SIZE) {
+                    for (y = Chunk.getRelativeBlockXYZ(0, elevations[x][z], 0)[1]; y > 0; --y)
+                        if (Block.getBlockByBlockLongID(tileMap[Chunk.getLinearBlockIndex(x, y - 1, z)]).name !== "air") {
+                            elevations[x][z] = chunkY * Y_SIZE + y;
+                            break;
+                        }
+                    if (y == -1) elevations[x][z] = chunkY * Y_SIZE;
+                }
+            }
+            for (let x = 0; x < X_SIZE; ++x)
+            for (let z = 0; z < Z_SIZE; ++z) {
+                let f = true;
+                for (let a = -2; a <= 2 && f; ++a)
+                for (let b = -2; b <= 2 && f; ++b) {
+                    if (treePlacement[x + a][z + b]) f = false;
+                }
+                haveTreeAround[x] = haveTreeAround[x] || [];
+                haveTreeAround[x][z] = !f;
+            }
+            for (let x = 0; x < X_SIZE; ++x)
+            for (let z = 0; z < Z_SIZE; ++z)
+            for (let y = Y_SIZE - 1; y >= 0; --y) {
+                if (Block.getBlockByBlockLongID(tileMap[Chunk.getLinearBlockIndex(x, y, z)]).name !== "air")
+                    break;
+                let elevation = elevations[x][z];
+                let j = chunkY * Y_SIZE + y;
+                let flowerPlacement = treeNoise[x][z] < 0.15;
+                if (treePlacement[x][z] && j - elevation < 5 && elevations.haveSurface)
+                    tileMap[Chunk.getLinearBlockIndex(x, y, z)] = Block.getBlockByBlockName("oak_log").longID;
+                else if (j - elevation < 7 && j - elevation > 3 && haveTreeAround[x][z] !== false)
+                    tileMap[Chunk.getLinearBlockIndex(x, y, z)] = Block.getBlockByBlockName("oak_leaves").longID;
+                else if (flowerPlacement && j <= elevation && y > 0 && elevations.haveSurface
+                && Block.getBlockByBlockLongID(tileMap[Chunk.getLinearBlockIndex(x, y - 1, z)]).name !== "air")
+                    tileMap[Chunk.getLinearBlockIndex(x, y, z)] = Block.getBlockByBlockName("dandelion").longID;
+            }
+            break;}
+        }
+    };
+    setRenderer(renderer = null) {
+        if (!renderer) return;
+        this.renderer = renderer;
+        for (let ck in this.chunkMap) {
+            this.chunkMap[ck].setRenderer(renderer);
+        }
+    };
+    getChunkByChunkKey(chunkKey) {
+        return this.chunkMap[chunkKey] || null;
+    };
+    getChunkByChunkXYZ(chunkX, chunkY, chunkZ) {
+        return this.getChunkByChunkKey(Chunk.chunkKeyByChunkXYZ(chunkX, chunkY, chunkZ));
+    };
+    getChunkByBlockXYZ(blockX, blockY, blockZ) {
+        return this.getChunkByChunkKey(Chunk.chunkKeyByBlockXYZ(blockX, blockY, blockZ));
+    };
+    loadChunk(chunkX, chunkY, chunkZ) {
+        let ck = Chunk.chunkKeyByChunkXYZ(chunkX, chunkY, chunkZ),
+            chunk = this.chunkMap[ck];
+        if (chunk) return chunk;
+        chunk = this.chunkMap[ck] = new Chunk(this, chunkX, chunkY, chunkZ);
+        this.dispatchEvent("onChunkLoad", chunk);
+        return chunk;
+    };
+
+    getTile(blockX, blockY, blockZ) {
+        [blockX, blockY, blockZ] = [blockX, blockY, blockZ].map(Math.floor);
+        let c = this.chunkMap[Chunk.chunkKeyByBlockXYZ(blockX, blockY, blockZ)];
+        if (c) return c.getTile(...Chunk.getRelativeBlockXYZ(blockX, blockY, blockZ));
+        return null;
+    };
+    setTile(blockX, blockY, blockZ, id, bd) {
+        [blockX, blockY, blockZ] = [blockX, blockY, blockZ].map(Math.floor);
+        let c = this.chunkMap[Chunk.chunkKeyByBlockXYZ(blockX, blockY, blockZ)];
+        if (c) {
+            let t = c.setTile(...Chunk.getRelativeBlockXYZ(blockX, blockY, blockZ), id, bd);
+            this.dispatchEvent("onTileChanges", blockX, blockY, blockZ);
+            return t;
+        }
+        return null;
+    };
+    getBlock(blockX, blockY, blockZ) {
+        [blockX, blockY, blockZ] = [blockX, blockY, blockZ].map(Math.floor);
+        let c = this.chunkMap[Chunk.chunkKeyByBlockXYZ(blockX, blockY, blockZ)];
+        if (c) return c.getBlock(...Chunk.getRelativeBlockXYZ(blockX, blockY, blockZ));
+        return null;
+    };
+    setBlock(blockX, blockY, blockZ, blockName) {
+        [blockX, blockY, blockZ] = [blockX, blockY, blockZ].map(Math.floor);
+        let c = this.chunkMap[Chunk.chunkKeyByBlockXYZ(blockX, blockY, blockZ)];
+        if (c) {
+            let t = c.setBlock(...Chunk.getRelativeBlockXYZ(blockX, blockY, blockZ), blockName);
+            this.dispatchEvent("onTileChanges", blockX, blockY, blockZ);
+            return t;
+        }
+        return null;
+    };
+    getLight(blockX, blockY, blockZ) {
+        [blockX, blockY, blockZ] = [blockX, blockY, blockZ].map(Math.floor);
+        let c = this.chunkMap[Chunk.chunkKeyByBlockXYZ(blockX, blockY, blockZ)];
+        if (c) return c.getLight(...Chunk.getRelativeBlockXYZ(blockX, blockY, blockZ));
+        return null;
+    };
+    getSkylight(blockX, blockY, blockZ) {
+        [blockX, blockY, blockZ] = [blockX, blockY, blockZ].map(Math.floor);
+        let c = this.chunkMap[Chunk.chunkKeyByBlockXYZ(blockX, blockY, blockZ)];
+        if (c) return c.getSkylight(...Chunk.getRelativeBlockXYZ(blockX, blockY, blockZ));
+        return null;
+    };
+    getTorchlight(blockX, blockY, blockZ) {
+        [blockX, blockY, blockZ] = [blockX, blockY, blockZ].map(Math.floor);
+        let c = this.chunkMap[Chunk.chunkKeyByBlockXYZ(blockX, blockY, blockZ)];
+        if (c) return c.getTorchlight(...Chunk.getRelativeBlockXYZ(blockX, blockY, blockZ));
+        return null;
+    };
+    update(dt) {
+        for (let ck in this.chunkMap) {
+            this.chunkMap[ck].update(dt);
+        }
+        this.fluidCalculator.update(dt);
+        this.lightingCalculator.update(dt);
+        this.entitys.forEach(e => e.update(dt));
+
+        const {mainPlayer} = this;
+
+        let start = mainPlayer.getEyePosition(),
+            end = mainPlayer.getDirection(20);
+        vec3.add(start, end, end);
+        let hit = this.rayTraceBlock(start, end, (x, y, z) => {
+            let b = this.getBlock(x, y, z);
+            return b && b.name !== "air";
+        });
+        let block = hit? this.getBlock(...hit.blockPos): null;
+        let longID = hit? this.getTile(...hit.blockPos): null;
+        let chunk = this.getChunkByBlockXYZ(...[...mainPlayer.position].map(n => n < 0? n - 1: n));
+        if (!this.fpss) this.fpss = [];
+        this.fpss.push(dt);
+        if (this.fpss.length > 15) this.fpss.shift();
+        document.getElementsByTagName("mcpage-play")[0].debugOutput.innerHTML = Object.entries({
+            "FPS: ": (1000 / (this.fpss.reduce((n, i) => n + i, 0) / this.fpss.length)).toFixed(2),
+            "Player:": [
+                "XYZ: " + [...mainPlayer.position].map(n => n.toFixed(1)).join(", "),
+                `Pitch: ${radian2degree(mainPlayer.pitch).toFixed(2)}°, Yaw: ${Math.abs(radian2degree(mainPlayer.yaw) * 100 % 36000 / 100).toFixed(2)}°`,
+                `Chunk: ${chunk? Chunk.getRelativeBlockXYZ(...mainPlayer.position).map(n => ~~n).join(" ") + " in " + [chunk.x, chunk.y, chunk.z].join(" "): "null"}`,
+                `Light: ${this.getLight(...mainPlayer.position)} (${this.getSkylight(...mainPlayer.position)} sky, ${this.getTorchlight(...mainPlayer.position)} block)`,
+            ],
+            "Crosshairs:": [
+                "XYZ: " + (hit? hit.blockPos.join(", "): "null") + " (" + (hit? hit.axis? hit.axis: "in block": "null") + ")",
+                `Block: ${block? block.name: "null"} (${longID?.id ?? "null"}, ${longID?.bd ?? "null"}, ${longID? longID: "null"})`,
+            ],
+        }).map(([k, v]) => `<p>${k}${
+            Array.isArray(v)
+            ? v.map(str => `<p>\t${str}</p>`).join("")
+            : v
+        }</p>`).join("");
+
+        let cxyz = Chunk.getChunkXYZByBlockXYZ(...mainPlayer.position),
+            [cx, cy, cz] = cxyz;
+        if (vec3.exactEquals(cxyz, mainPlayer.lastChunk || []))
+            for (let dx = -1; dx <= 1; ++dx)
+            for (let dz = -1; dz <= 1; ++dz)
+            for (let dy = 1; dy >= -1; --dy)
+                this.loadChunk(cx + dx, cy + dy, cz + dz);
+        mainPlayer.lastChunk = cxyz;
+    };
+    // return null->uncollision    else -> { axis->("x+-y+-z+-": collision face, "": in block, b)lockPos}
+    rayTraceBlock(start, end, chunkFn) {
+        if (start.some(Number.isNaN) || end.some(Number.isNaN) || vec3.equals(start, end))
+            return null;
+        if (chunkFn(...start.map(Math.floor))) return {
+            axis: "", blockPos: start.map(Math.floor)
+        };
+        let vec = vec3.subtract(end, start),
+            len = vec3.length(vec),
+            delta = vec3.create(),
+            axisStepDir = vec3.create(),
+            blockPos = vec3.create(),
+            nextWay = vec3.create();
+        vec.forEach((dir, axis) => {
+            let d = delta[axis] = len / Math.abs(dir);
+            axisStepDir[axis] = dir < 0? -1: 1;
+            blockPos[axis] = dir > 0? Math.ceil(start[axis]) - 1: Math.floor(start[axis]);
+            nextWay[axis] = d === Infinity? Infinity :d * (dir > 0
+                ? Math.ceil(start[axis]) - start[axis]
+                : start[axis] - Math.floor(start[axis]));
+        });
+        for (let way = 0, axis; way <= len;) {
+            axis = nextWay[0] < nextWay[1] && nextWay[0] < nextWay[2]
+                ? 0 : nextWay[1] < nextWay[2]? 1: 2;
+            way = nextWay[axis];
+            if (way > len) break;
+            blockPos[axis] += axisStepDir[axis];
+            if (chunkFn(...blockPos))
+                return {
+                    axis: "xyz"[axis] + (axisStepDir[axis] > 0? '-': '+'),
+                    blockPos
+                };
+            nextWay[axis] += delta[axis];
+        }
+        return null;
+    };
+    // 向大佬低头
+    // from: https://github.com/guckstift/BlockWeb/blob/master/src/boxcast.js
+    hitboxesCollision(box, vec, chunkFn) {
+        let len      = vec3.length(vec);
+        if(len === 0) return null;
+        let boxmin   = box.min, boxmax = box.max,
+            lead     = vec3.create(),
+            leadvox  = vec3.create(),
+            trailvox = vec3.create(),
+            step     = vec.map(n => n > 0? 1: -1),
+            waydelta = vec.map(n => len / Math.abs(n)),
+            waynext  = vec3.create();
+        for(let k = 0, distnext, trail; k < 3; k ++) {
+            if(vec[k] > 0) {
+                lead[k]     = boxmax[k];
+                trail       = boxmin[k];
+                trailvox[k] = Math.floor(trail);
+                leadvox[k]  = Math.ceil(lead[k]) - 1;
+                distnext    = Math.ceil(lead[k]) - lead[k];
+            }
+            else {
+                lead[k]     = boxmin[k];
+                trail       = boxmax[k];
+                trailvox[k] = Math.ceil(trail) - 1;
+                leadvox[k]  = Math.floor(lead[k]);
+                distnext    = lead[k] - Math.floor(lead[k]);
+            }
+            waynext[k] = waydelta[k] === Infinity? Infinity: waydelta[k] * distnext;
+        }
+        for (let way = 0, axis; way <= len; ) {
+            axis = waynext[1] < waynext[0] && waynext[1] < waynext[2]
+                ? 1: waynext[0] < waynext[2]? 0: 2;
+            // axis = waynext[0] < waynext[1] && waynext[0] < waynext[2]
+            //     ? 0: waynext[1] < waynext[2]? 1: 2;
+            way = waynext[axis];
+            if (way > len) break;
+            waynext[axis]  += waydelta[axis];
+            leadvox[axis]  += step[axis];
+            trailvox[axis] += step[axis];
+            let [stepx, stepy, stepz] = step,
+                xs = axis === 0? leadvox[0]: trailvox[0],
+                ys = axis === 1? leadvox[1]: trailvox[1],
+                zs = axis === 2? leadvox[2]: trailvox[2],
+                [xe, ye, ze] = vec3.add(leadvox, step),
+                x, y, z;
+            for(x = xs; x !== xe; x += stepx)
+            for(y = ys; y !== ye; y += stepy)
+            for(z = zs; z !== ze; z += stepz)
+                if(chunkFn(x, y, z)) return {
+                    axis: axis,
+                    step: step[axis],
+                    pos:  lead[axis] + way * (vec[axis] / len),
+                };
+        }
+        return null;
+    };
+};
+
+export {
+    World,
+    World as default
+};

+ 238 - 0
legacy/WebMC/src/World/WorldFluidCal.js

@@ -0,0 +1,238 @@
+// Calculate fluid level
+import {
+    Chunk,
+    CHUNK_X_SIZE as X_SIZE,
+    CHUNK_Y_SIZE as Y_SIZE,
+    CHUNK_Z_SIZE as Z_SIZE,
+} from "./Chunk.js";
+import { Block } from "./Block.js";
+
+class FluidCalculator {
+    constructor(world) {
+        // wx, wy, wz
+        this.spreadQueue = [];
+        this.removalQueue = [];
+        this.updatingTile = false;
+        this.waterID = Block.getBlockByBlockName("water").id;
+        this.flowingWaterID = Block.getBlockByBlockName("flowing_water").id;
+        this.lavaID = Block.getBlockByBlockName("lava").id;
+        this.flowingLavaID = Block.getBlockByBlockName("flowing_lava").id;
+        this.timeCount = 0;
+        this.setWorld(world);
+    };
+    setWorld(world) {
+        this.world = world;
+        const getChunkFlowingFluid = (chunk) => {
+            for (let rx = 0; rx < X_SIZE; ++rx)
+            for (let ry = 0; ry < Y_SIZE; ++ry)
+            for (let rz = 0; rz < Z_SIZE; ++rz) {
+                let {id, bd} = chunk.getBlock(rx, ry, rz);
+                if (id == this.flowingWaterID || id == this.flowingLavaID) {
+                    this.spreadQueue.push(chunk.blockRXYZ2BlockXYZ(rx, ry, rz));
+                }
+            }
+            chunk.addEventListener("onTileChanges", (blockRX, blockRY, blockRZ, newInfo, oldInfo) => {
+                if (this.updatingTile || (!oldInfo.block.isFluid && !newInfo.block.isFluid)) return;
+                if (newInfo.block.isFluid)
+                    this.spreadQueue.push(chunk.blockRXYZ2BlockXYZ(blockRX, blockRY, blockRZ));
+                else this.removalQueue.push([...chunk.blockRXYZ2BlockXYZ(blockRX, blockRY, blockRZ), oldInfo.longID.id, oldInfo.longID.bd]);
+            });
+        };
+        Object.values(world.chunkMap).forEach(getChunkFlowingFluid);
+        this.spreadFluid();
+        world.addEventListener("onChunkLoad", (chunk) => {
+            getChunkFlowingFluid(chunk);
+            this.spreadFluid();
+        });
+    };
+    spreadFluid(depth = Infinity, spreadQueue = this.spreadQueue) {
+        const {world, waterID, lavaID, flowingWaterID, flowingLavaID} = this;
+        while (depth-- > 0 && spreadQueue.length) {
+            let nextQueue = [];
+            const push2queue = (x, y, z) => {
+                const k = x + "," + y + "," + z;
+                if (nextQueue[k]) return;
+                nextQueue.push([x, y, z]);
+                nextQueue[k] = true;
+            };
+            while (spreadQueue.length) {
+                let [cwx, cwy, cwz] = spreadQueue.shift();
+                let chunk = world.getChunkByBlockXYZ(cwx, cwy, cwz);
+                // 如果区块还未加载
+                if (chunk == null) {
+                    push2queue(cwx, cwy, cwz);
+                    // nextQueue.push([cwx, cwy, cwz]);
+                    continue;
+                }
+                // chunk.updatedFluidLevel = true;
+                let longID = world.getTile(cwx, cwy, cwz), cid = longID.id, cbd = longID.bd;
+                // console.log(longID, cid, cbd)
+                let cblock = Block.getBlockByBlockIDandData(cid, cbd);
+                const fluidID =
+                    cid == waterID || cid == flowingWaterID? waterID:
+                    cid == lavaID || cid == flowingLavaID? lavaID: cid;
+                const flowingID =
+                    cid == waterID || cid == flowingWaterID? flowingWaterID:
+                    cid == lavaID || cid == flowingLavaID? flowingLavaID: cid;
+                if (cid != fluidID) {
+                    world.setTile(cwx, cwy, cwz, fluidID, cbd);
+                    cid = fluidID;
+                }
+                let downBlock = world.getBlock(cwx, cwy - 1, cwz);
+                longID = world.getTile(cwx, cwy - 1, cwz);
+                let downBlockId = longID?.id, downBlockBd = longID?.bd;
+                // 如果下面的区块还未加载
+                // if (downBlock == null) nextQueue.push([cwx, cwy, cwz]);
+                if (downBlock == null) push2queue(cwx, cwy, cwz);
+                // 如果是向下传播的液体方块
+                if (cbd >= 8 && downBlock != null) {
+                    // 如果能向下传播
+                    if (downBlock.name == "air" || downBlockId == fluidID || downBlockId == flowingID) {
+                        if (downBlock.name == "air" || downBlockBd) world.setTile(cwx, cwy - 1, cwz, flowingID, cbd);
+                        else world.setTile(cwx, cwy - 1, cwz, flowingID, downBlockBd);
+                        // nextQueue.push([cwx, cwy - 1, cwz]);
+                        push2queue(cwx, cwy - 1, cwz);
+                        continue;
+                    }
+                    // 否则作为水源向外传播
+                    else {
+                        cbd = 0;
+                    }
+                }
+                // 若等级到最小且无法向下方流动时终止传播
+                if (cbd == cblock.maxLevel && (downBlock == null || downBlock.name != "air"))
+                    continue;
+                // 非水源方块且下面方块为空 直接向下传播
+                if (downBlock && (downBlock.name == "air" || downBlockId == fluidID || downBlockId == flowingID)) {
+                    world.setTile(cwx, cwy - 1, cwz, flowingID, cbd + 8);
+                    // nextQueue.push([cwx, cwy - 1, cwz]);
+                    push2queue(cwx, cwy - 1, cwz);
+                    if (cbd != 0) continue;
+                }
+                // 找到最近的坑
+                const blockKey = (x, z) => x + "," + z;
+                let holes = [], level = cbd;
+                let queue = [[[1,0], [-1,0], [0,1], [0,-1]].map(
+                    ([dx, dz]) => [cwx + dx, cwz + dz])];
+                while (queue.length && level <= cblock.maxLevel) {
+                    ++level;
+                    let q = queue.shift(), nextQ = [];
+                    queue.push(nextQ);
+                    while (q.length) {
+                        let [x, z] = q.shift();
+                        let b = world.getBlock(x, cwy - 1, z);
+                        if (b && (b.name == "air" || b.id == fluidID || b.id == flowingID)) {
+                            queue.length = 0;
+                            holes.push([x, z]);
+                        }
+                        else for (let [dx, dz] of [[1,0], [-1,0], [0,1], [0,-1]]) {
+                            if (queue[blockKey(x + dx, z + dz)] != true) {
+                                nextQ.push([x + dx, z + dz]);
+                                queue[blockKey(x + dx, z + dz)] = true;
+                            }
+                        }
+                    }
+                }
+                // console.log(holes);
+                // 若有坑 向坑内传播 否则向四周传播
+                let sp = holes.length
+                    ? [[1,0], [-1,0], [0,1], [0,-1]].reduce((ans, [dx, dz]) => {
+                        let x = cwx + dx, z = cwz + dz;
+                        const {abs} = Math;
+                        for (let [i, k] of holes) {
+                            if (abs(x - i) + abs(z - k) == level - 1 - cbd) {
+                                ans.push([dx, dz]);
+                                break;
+                            }
+                        }
+                        return ans;
+                    }, [])
+                    : [[1,0], [-1,0], [0,1], [0,-1]];
+                for (let [dx, dz] of sp) {
+                    let awx = cwx + dx, awy = cwy, awz = cwz + dz;
+                    let ablock = world.getBlock(awx, awy, awz);
+                    if (ablock == null) {
+                        // nextQueue.push([awx, awy, awz]);
+                        push2queue(awx, awy, awz);
+                        continue;
+                    }
+                    if (ablock.name == "air") {
+                        world.setTile(awx, awy, awz, flowingID, cbd + 1);
+                        // nextQueue.push([awx, awy, awz]);
+                        push2queue(awx, awy, awz);
+                    }
+                    else if (ablock.id == fluidID || ablock.id == flowingID) {
+                        let abd = world.getTile(awx, awy, awz).bd;
+                        if (abd >= 8) abd = 0;
+                        // 向旁边传播
+                        if (abd > cbd) {
+                            world.setTile(awx, awy, awz, flowingID, cbd + 1);
+                            // nextQueue.push([awx, awy, awz]);
+                            push2queue(awx, awy, awz);
+                        }
+                        // 向中间传播
+                        else if (abd < cbd - 1) {
+                            world.setTile(awx, awy, awz, flowingID, abd + 1);
+                            // nextQueue.push([awx, awy, awz]);
+                            push2queue(awx, awy, awz);
+                        }
+                    }
+                }
+                // 无限水源
+            }
+            this.spreadQueue = spreadQueue = nextQueue;
+            // console.log(nextQueue.map(t => t.join(",")))
+        }
+    };
+    // removalQueue = [[wx, wy, wz, center block id, center block fluid level]]
+    removeFluid(depth = Infinity, removalQueue = this.removalQueue) {
+        const {world, waterID, lavaID, flowingWaterID, flowingLavaID} = this;
+        while (depth-- > 0 && removalQueue.length) {
+            let nextQueue = [];
+            const push2queue = (x, y, z) => {
+                const k = x + "," + y + "," + z;
+                if (nextQueue[k]) return;
+                nextQueue.push([x, y, z]);
+                nextQueue[k] = true;
+            };
+            while (removalQueue.length) {
+                let [cwx, cwy, cwz, cid, cbd] = removalQueue.shift();
+                let chunk = world.getChunkByBlockXYZ(cwx, cwy, cwz);
+                // 如果区块还未加载
+                if (chunk == null) {
+                    push2queue(cwx, cwy, cwz);
+                    continue;
+                }
+                let cblock = Block.getBlockByBlockIDandData(cid, cbd);
+                const fluidID =
+                    cid == waterID || cid == flowingWaterID? waterID:
+                    cid == lavaID || cid == flowingLavaID? lavaID: cid;
+                const flowingID =
+                    cid == waterID || cid == flowingWaterID? flowingWaterID:
+                    cid == lavaID || cid == flowingLavaID? flowingLavaID: cid;
+                
+            }
+            this.removalQueue = removalQueue = nextQueue;
+        }
+    };
+    updateTile(blockX, blockY, blockZ) {
+        if (this.updatingTile) return;
+        let b = this.world.getBlock(blockX, blockY, blockZ);
+        if (!b.isFluid) return;
+        this.spreadQueue.push([blockX, blockY, blockZ]);
+    };
+    update(dt) {
+        if (this.timeCount >= 250) {
+            this.updatingTile = true;
+            this.spreadFluid(1);
+            this.updatingTile = false;
+            this.timeCount = 0;
+        }
+        this.timeCount += dt;
+    };
+}
+
+export {
+    FluidCalculator,
+    FluidCalculator as default
+};

+ 313 - 0
legacy/WebMC/src/World/WorldLight.js

@@ -0,0 +1,313 @@
+// 计算光照
+import { 
+    Chunk,
+    CHUNK_X_SIZE as X_SIZE,
+    CHUNK_Y_SIZE as Y_SIZE,
+    CHUNK_Z_SIZE as Z_SIZE,
+} from "./Chunk.js";
+
+// low 4 bit save sky light, and hight 4 bit save torch light
+// https://www.seedofandromeda.com/blogs/29-fast-flood-fill-lighting-in-a-blocky-voxel-game-pt-1
+// https://www.reddit.com/r/gamedev/comments/2k7gxt/fast_flood_fill_lighting_in_a_blocky_voxel_game/
+class LightMap extends Uint8Array {
+    constructor() { super(Y_SIZE * Z_SIZE * X_SIZE); };
+    get(x, y, z) { return this[Chunk.getLinearBlockIndex(x, y, z)]; };
+    set(x, y, z, l) { return this[Chunk.getLinearBlockIndex(x, y, z)] = l; };
+    getSkylight(x, y, z) { return this[Chunk.getLinearBlockIndex(x, y, z)] & 0xF; };
+    getTorchlight(x, y, z) { return (this[Chunk.getLinearBlockIndex(x, y, z)] >> 4) & 0xF; };
+    getMax(x, y, z) {
+        let l = this[Chunk.getLinearBlockIndex(x, y, z)];
+        return Math.max(l & 0xF, (l >> 4) & 0xF);
+    };
+    setSkylight(x, y, z, l) {
+        let i = Chunk.getLinearBlockIndex(x, y, z);
+        this[i] = (this[i] & 0xF0) | l;
+        return l;
+    };
+    setTorchlight(x, y, z, l) {
+        let i = Chunk.getLinearBlockIndex(x, y, z);
+        this[i] = (this[i] & 0xF) | (l << 4);
+        return l;
+    };
+};
+
+class ChunksLightCalculation {
+    constructor(world) {
+        this.setWorld(world);
+    };
+    setWorld(world) {
+        this.world = world;
+        let chunks = Object.values(world.chunkMap);
+        let topChunks = chunks.sort(({y: y1, y: y2}) => y2 - y1).reduce((arr, chunk) => {
+            if (chunk.y === chunks[0].y) arr.push(chunk);
+            return arr;
+        }, []);
+        let queue = [];
+        topChunks.forEach(chunk => {
+            for (let rx = 0; rx < X_SIZE; ++rx)
+            for (let rz = 0; rz < Z_SIZE; ++rz) {
+                let cblock = chunk.getBlock(rx, Y_SIZE - 1, rz);
+                if (cblock.isOpaque) continue;
+                let abl = 15;
+                let cbl = cblock.opacity === 0 && abl === 15? 15: abl - cblock.opacity - 1;
+                chunk.lightMap.setSkylight(rx, Y_SIZE - 1, rz, cbl);
+                if (cbl > 1) queue.push([rx, Y_SIZE - 1, rz, chunk]);
+            }
+        });
+        this.spreadSkylight(queue);
+        chunks.forEach(chunk => {
+            for (let rx = 0; rx < X_SIZE; ++rx)
+            for (let ry = 0; ry < Y_SIZE; ++ry)
+            for (let rz = 0; rz < Z_SIZE; ++rz) {
+                let b = chunk.getBlock(rx, ry, rz);
+                if (b.luminance) {
+                    chunk.lightMap.setTorchlight(rx, ry, rz, b.luminance);
+                    queue.push([rx, ry, rz, chunk]);
+                }
+            }
+        });
+        this.spreadTorchlight(queue);
+        world.addEventListener("onTileChanges", this.updateTile.bind(this));
+        world.addEventListener("onChunkLoad", chunk => {
+            this.buildChunkLight(chunk.chunkKey);
+            for (let [dx, dy, dz] of [[1,0,0], [-1,0,0], [0,1,0], [0,-1,0], [0,0,1], [0,0,-1]]) {
+                this.buildChunkLight(world.getChunkByChunkXYZ(chunk.x + dx, chunk.y + dy, chunk.z + dz));
+            }
+        });
+    };
+    // queue = [[rx, ry, rz, chunk]]
+    spreadSkylight(queue) {
+        const world = this.world, {max} = Math;
+        while (queue.length) {
+            let [crx, cry, crz, chunk] = queue.shift(),
+                csl = chunk.lightMap.getSkylight(crx, cry, crz),
+                cblock = chunk.getBlock(crx, cry, crz);
+            chunk.updatedLightMap = true;
+            for (let [dx, dy, dz] of [[1,0,0], [-1,0,0], [0,1,0], [0,-1,0], [0,0,1], [0,0,-1]]) {
+                let arx = crx + dx, ary = cry + dy, arz = crz + dz, achunk = chunk;
+                if (chunk.inOtherChunk(arx, ary, arz)) {
+                    [arx, ary, arz] = Chunk.getRelativeBlockXYZ(crx + dx, cry + dy, crz + dz);
+                    achunk = world.getChunkByChunkXYZ(chunk.x + dx, chunk.y + dy, chunk.z + dz);
+                    if (achunk === null) continue;
+                }
+                let ablock = achunk.getBlock(arx, ary, arz);
+                if (ablock.isOpaque) continue;
+                // 向下无衰减传播
+                if (csl === 15 && dy === -1 && ablock.opacity === 0) {
+                    achunk.lightMap.setSkylight(arx, ary, arz, 15);
+                    queue.push([arx, ary, arz, achunk]);
+                    continue;
+                }
+                let asl = achunk.lightMap.getSkylight(arx, ary, arz);
+                // 中间比旁边亮 向旁边传播
+                if (csl - ablock.opacity - 1 > asl) {
+                    achunk.lightMap.setSkylight(arx, ary, arz, max(0, csl - ablock.opacity - 1));
+                    queue.push([arx, ary, arz, achunk]);
+                }
+                // 旁边比中间亮 向中间传播
+                else if (asl - cblock.opacity - 1 > csl) {
+                    chunk.lightMap.setSkylight(crx, cry, crz, asl - cblock.opacity - 1);
+                    queue.push([crx, cry, crz, chunk]);
+                }
+            }
+        }
+    };
+    spreadTorchlight(queue) {
+        const world = this.world;
+        while (queue.length) {
+            let [crx, cry, crz, chunk] = queue.shift(),
+                ctl = chunk.lightMap.getTorchlight(crx, cry, crz),
+                cblock = chunk.getBlock(crx, cry, crz);
+            chunk.updatedLightMap = true;
+            if (ctl <= 1) continue;
+            for (let [dx, dy, dz] of [[1,0,0], [-1,0,0], [0,1,0], [0,-1,0], [0,0,1], [0,0,-1]]) {
+                let arx = crx + dx, ary = cry + dy, arz = crz + dz, achunk = chunk;
+                if (chunk.inOtherChunk(arx, ary, arz)) {
+                    [arx, ary, arz] = Chunk.getRelativeBlockXYZ(crx + dx, cry + dy, crz + dz);
+                    achunk = world.getChunkByChunkXYZ(chunk.x + dx, chunk.y + dy, chunk.z + dz);
+                    if (achunk === null) continue;
+                }
+                let ablock = achunk.getBlock(arx, ary, arz);
+                if (ablock.isOpaque) continue;
+                let atl = achunk.lightMap.getTorchlight(arx, ary, arz);
+                // 中间比旁边亮 向旁边传播
+                if (ctl - ablock.opacity - 1 > atl) {
+                    achunk.lightMap.setTorchlight(arx, ary, arz, ctl - ablock.opacity - 1);
+                    queue.push([arx, ary, arz, achunk]);
+                    continue;
+                }
+                // 旁边比中间亮 向中间传播
+                else if (atl - cblock.opacity - 1 > ctl) {
+                    chunk.lightMap.setTorchlight(crx, cry, crz, atl - cblock.opacity - 1);
+                    queue.push([crx, cry, crz, chunk]);
+                    continue;
+                }
+            }
+        }
+    };
+    // removalLightQueue = [[rx, ry, rz, center block sky/torch light, chunk]]
+    removeSkylight(removalLightQueue) {
+        const world = this.world, spreadLightQueue = [];
+        while (removalLightQueue.length) {
+            let [crx, cry, crz, csl, chunk] = removalLightQueue.shift();
+            chunk.updatedLightMap = true;
+            for (let [dx, dy, dz] of [[1,0,0], [-1,0,0], [0,1,0], [0,-1,0], [0,0,1], [0,0,-1]]) {
+                let arx = crx + dx, ary = cry + dy, arz = crz + dz, achunk = chunk;
+                if (chunk.inOtherChunk(arx, ary, arz)) {
+                    [arx, ary, arz] = Chunk.getRelativeBlockXYZ(crx + dx, cry + dy, crz + dz);
+                    achunk = world.getChunkByChunkXYZ(chunk.x + dx, chunk.y + dy, chunk.z + dz);
+                    if (achunk === null) continue;
+                }
+                let ablock = achunk.getBlock(arx, ary, arz),
+                    asl = achunk.lightMap.getSkylight(arx, ary, arz);
+                if (asl === 15 && dy === -1 && ablock.opacity === 0) {
+                    achunk.lightMap.setSkylight(arx, ary, arz, 0);
+                    removalLightQueue.push([arx, ary, arz, asl, achunk]);
+                    continue;
+                }
+                else if (asl !== 0 && asl < csl) {
+                    achunk.lightMap.setSkylight(arx, ary, arz, 0);
+                    removalLightQueue.push([arx, ary, arz, asl, achunk]);
+                    continue;
+                }
+                else if (asl >= csl) {
+                    spreadLightQueue.push([arx, ary, arz, achunk]);
+                    continue;
+                }
+            }
+        }
+        return spreadLightQueue;
+    };
+    removeTorchlight(removalLightQueue) {
+        const world = this.world, spreadLightQueue = [];
+        while (removalLightQueue.length) {
+            let [crx, cry, crz, ctl, chunk] = removalLightQueue.shift();
+            chunk.updatedLightMap = true;
+            for (let [dx, dy, dz] of [[1,0,0], [-1,0,0], [0,1,0], [0,-1,0], [0,0,1], [0,0,-1]]) {
+                let arx = crx + dx, ary = cry + dy, arz = crz + dz, achunk = chunk;
+                if (chunk.inOtherChunk(arx, ary, arz)) {
+                    [arx, ary, arz] = Chunk.getRelativeBlockXYZ(crx + dx, cry + dy, crz + dz);
+                    achunk = world.getChunkByChunkXYZ(chunk.x + dx, chunk.y + dy, chunk.z + dz);
+                    if (achunk === null) continue;
+                }
+                let ablock = achunk.getBlock(arx, ary, arz),
+                    atl = achunk.lightMap.getTorchlight(arx, ary, arz);
+                if (atl !== 0 && atl < ctl && ablock.luminance === 0) {
+                    achunk.lightMap.setTorchlight(arx, ary, arz, 0);
+                    removalLightQueue.push([arx, ary, arz, atl, achunk]);
+                }
+                else if (atl >= ctl || ablock.luminance) {
+                    spreadLightQueue.push([arx, ary, arz, achunk]);
+                }
+            }
+        }
+        return spreadLightQueue;
+    };
+    buildChunkLight(chunkKey) {
+        const {world} = this, chunk = world.getChunkByChunkKey(chunkKey);
+        if (!chunk) return;
+        // build torch light
+        let queue = [], lightMap = chunk.lightMap;
+        for (let j = 0; j < Y_SIZE; ++j)
+        for (let k = 0; k < Z_SIZE; ++k)
+        for (let i = 0; i < X_SIZE; ++i) {
+            let b = chunk.getBlock(i, j, k);
+            lightMap.setSkylight(i, j, k, 0);
+            let tl = b.luminance;
+            lightMap.setTorchlight(i, j, k, tl);
+            if (tl) queue.push([i, j, k, chunk]);
+        }
+        this.spreadTorchlight(queue);
+        // build sky light
+        [   [0,X_SIZE-1, 0,0, 0,Z_SIZE-1], [0,X_SIZE-1, Y_SIZE-1,Y_SIZE-1, 0,Z_SIZE-1],
+            [0,0, 0,Y_SIZE-1, 0,Z_SIZE-1], [X_SIZE-1,X_SIZE-1, 0,Y_SIZE-1, 0,Z_SIZE-1],
+            [0,X_SIZE-1, 0,Y_SIZE-1, 0,0], [0,X_SIZE-1, 0,Y_SIZE-1, Z_SIZE-1,Z_SIZE-1],
+        ].forEach(([sx, ex, sy, ey, sz, ez]) => {
+            let dx = sx === ex? sx? 1: -1: 0,
+                dy = sy === ey? sy? 1: -1: 0,
+                dz = sz === ez? sz? 1: -1: 0;
+            for (let rx = sx; rx <= ex; ++rx)
+            for (let ry = sy; ry <= ey; ++ry)
+            for (let rz = sz; rz <= ez; ++rz) {
+                let b = chunk.getBlock(rx, ry, rz);
+                if (b.isOpaque) continue;
+                let asl = world.getSkylight(...chunk.blockRXYZ2BlockXYZ(rx + dx, ry + dy, rz + dz));
+                if (asl === null && dy !== 1) return;
+                let csl = chunk.lightMap.getSkylight(rx, ry, rz),
+                    l = Math.max(csl, dy == 1 && (asl === null || asl === 15)? 15: asl - b.opacity - 1);
+                if (l > 1) {
+                    lightMap.setSkylight(rx, ry, rz, l);
+                    queue.push([rx, ry, rz, chunk]);
+                }
+            }
+        });
+        this.spreadSkylight(queue);
+    };
+    updateTile(blockX, blockY, blockZ) {
+        const world = this.world;
+        const cchunk = world.getChunkByBlockXYZ(blockX, blockY, blockZ);
+        if (cchunk === null) return;
+        const cblock = world.getBlock(blockX, blockY, blockZ);
+        // calculate sky light
+        let obstructed = blockY, oblock = null, queue = [];
+        const setSkylight = (x, y, z, l) => {
+            let c = world.getChunkByBlockXYZ(x, y, z);
+            if (c === null) return;
+            let rxyz = Chunk.getRelativeBlockXYZ(x, y, z);
+            c.lightMap.setSkylight(...rxyz, l);
+            queue.push([...rxyz, c]);
+        };
+        while (oblock = world.getBlock(blockX, ++obstructed, blockZ)) if (oblock.opacity === 0) break;
+        for (let y = obstructed - 1, b; true; --y) {
+            b = world.getBlock(blockX, y, blockZ); if (b === null || b.opacity === 0) break;
+            if (oblock !== null)
+                setSkylight(blockX, y, blockZ, 0);
+            // If there is no obstruction directly above
+            else if (world.getSkylight(blockX, y, blockZ) !== 15)
+                setSkylight(blockX, y, blockZ, 15);
+        }
+        // remove below sky light
+        let removalLightQueue = [];
+        removalLightQueue.push([
+            ...Chunk.getRelativeBlockXYZ(blockX, blockY, blockZ),
+            world.getSkylight(blockX, blockY, blockZ),
+            cchunk]);
+        setSkylight(blockX, blockY, blockZ, oblock === null? 15 - cblock.opacity: 0);
+        queue.push(...this.removeSkylight(removalLightQueue));
+        this.spreadSkylight(queue);
+
+        // calculate torch light
+        const setTorchlight = (x, y, z, l) => {
+            let c = world.getChunkByBlockXYZ(x, y, z);
+            if (c === null) return;
+            let rxyz = Chunk.getRelativeBlockXYZ(x, y, z);
+            c.lightMap.setTorchlight(...rxyz, l);
+            queue.push([...rxyz, c]);
+        };
+        let oldLight = world.getTorchlight(blockX, blockY, blockZ);
+        if (oldLight > cblock.luminance) {
+            // remove light
+            let removalLightQueue = [[...Chunk.getRelativeBlockXYZ(blockX, blockY, blockZ), oldLight, cchunk]];
+            setTorchlight(blockX, blockY, blockZ, 0);
+            queue.push(...this.removeTorchlight(removalLightQueue));
+        }
+        setTorchlight(blockX, blockY, blockZ, cblock.luminance);
+        if (!cblock.luminance) {
+            [[1,0,0], [-1,0,0], [0,1,0], [0,-1,0], [0,0,1], [0,0,-1]]
+            .forEach(([dx, dy, dz]) => {
+                let wx = blockX + dx, wy = blockY + dy, wz = blockZ + dz,
+                    l = world.getTorchlight(wx, wy, wz);
+                if (l !== null && l !== 0) {
+                    queue.push([...Chunk.getRelativeBlockXYZ(wx, wy, wz), world.getChunkByBlockXYZ(wx, wy, wz)]);
+                }
+            });
+        }
+        this.spreadTorchlight(queue);
+    };
+    update() {};
+}
+
+export {
+    LightMap,
+    ChunksLightCalculation,
+};

+ 94 - 0
legacy/WebMC/src/World/blocks.json

@@ -0,0 +1,94 @@
+{
+"block_renderType_index":{
+    "normal":0,
+    "flower":1,
+    "torch":2,
+    "fluid":3,
+    "crop":4,
+    "door":5,
+    "steps":6,
+    "stairs":7,
+    "fence":8,
+    "cactus":9,
+    "bed":10,
+    "glass_panes":11,
+    "stem":12,
+    "fence_gate":13
+},
+"block_renderType_vertex":{
+    "normal":[
+        [0, 1, 0], [0, 1, 1], [1, 1, 1], [1, 1, 0],
+        [0, 0, 1], [0, 0, 0], [1, 0, 0], [1, 0, 1]
+    ],
+    "flower":[
+        [0, 1, 0], [0, 1, 1], [1, 1, 1], [1, 1, 0],
+        [0, 0, 1], [0, 0, 0], [1, 0, 0], [1, 0, 1]
+    ],
+    "cactus": [
+        [0, 1, 0], [0, 1, 1], [1, 1, 1], [1, 1, 0],
+        [0, 0, 1], [0, 0, 0], [1, 0, 0], [1, 0, 1],
+        [0, 1, 0.9375], [0, 0, 0.9375], [1, 0, 0.9375], [1, 1, 0.9375],
+        [0.9375, 1, 1], [0.9375, 0, 1], [0.9375, 0, 0], [0.9375, 1, 0],
+        [1, 1, 0.0625], [1, 0, 0.0625], [0, 0, 0.0625], [0, 1, 0.0625],
+        [0.0625, 1, 0], [0.0625, 0, 0], [0.0625, 0, 1], [0.0625, 1, 1]
+    ],
+    "fluid": [
+        [0, "y0", 0], [0, "y1", 1], [1, "y2", 1], [1, "y3", 0],
+        [0, 0, 1], [0, 0, 0], [1, 0, 0], [1, 0, 1]
+    ]
+},
+"blocks":{
+    "air":{"id":0,"opacity":0},
+    "stone":            {"id":1,"blastResistance":30,"texture":[[1,20]]},
+    "granite":          {"id":1,"bd":1,"blastResistance":30,"texture":[[1,21]]},
+    "polished_granite": {"id":1,"bd":2,"blastResistance":30,"texture":[[1,22]]},
+    "diorite":          {"id":1,"bd":3,"blastResistance":30,"texture":[[1,23]]},
+    "polished_diorite": {"id":1,"bd":4,"blastResistance":30,"texture":[[1,24]]},
+    "andesite":         {"id":1,"bd":5,"blastResistance":30,"texture":[[1,25]]},
+    "polished_andesite":{"id":1,"bd":6,"blastResistance":30,"texture":[[1,26]]},
+    "grass":       {"id":2,"blastResistance":3,"texture":[[1,3],[2,12],[1,4]]},
+    "dirt":        {"id":3,"blastResistance":2.5,"texture":[[2,12]]},
+    "cobblestone": {"id":4,"blastResistance":30,"texture":[[1,27]]},
+    "oak_wood_planks":      {"id":5,"blastResistance":15,"texture":[[2,15]]},
+    "spruce_wood_planks":   {"id":5,"bd":1,"blastResistance":15,"texture":[[2,16]]},
+    "birch_wood_planks":    {"id":5,"bd":2,"blastResistance":15,"texture":[[2,17]]},
+    "jungle_wood_planks":   {"id":5,"bd":3,"blastResistance":15,"texture":[[2,18]]},
+    "acacia_wood_planks":   {"id":5,"bd":4,"blastResistance":15,"texture":[[2,19]]},
+    "dark_oak_wood_planks": {"id":5,"bd":5,"blastResistance":15,"texture":[[2,20]]},
+    "oak_sapling":      {"id":6,"renderType":1,"opacity":0,"blastResistance":0,"texture":[[3,23]]},
+    "spruce_sapling":   {"id":6,"bd":1,"renderType":1,"opacity":0,"blastResistance":0,"texture":[[3,24]]},
+    "birch_sapling":    {"id":6,"bd":2,"renderType":1,"opacity":0,"blastResistance":0,"texture":[[3,25]]},
+    "jungle_sapling":   {"id":6,"bd":3,"renderType":1,"opacity":0,"blastResistance":0,"texture":[[3,26]]},
+    "acacia_sapling":   {"id":6,"bd":4,"renderType":1,"opacity":0,"blastResistance":0,"texture":[[3,27]]},
+    "dark_oak_sapling": {"id":6,"bd":5,"renderType":1,"opacity":0,"blastResistance":0,"texture":[[3,28]]},
+    "bedrock":      {"id":7,"blastResistance":18000000,"texture":[[2,1]]},
+    "water":        {"id":8,"renderType":3,"opacity":1,"blastResistance":500,"stackable":null,"texture":[[12,1]]},
+    "flowing_water":{"id":9,"renderType":3,"opacity":1,"blastResistance":500,"stackable":null,"texture":[[12,1]]},
+    "lava":         {"id":10,"renderType":3,"opacity":14,"luminance":15,"maxLevel":4,"blastResistance":500,"stackable":null,"texture":[[12,3]]},
+    "flowing_lava": {"id":11,"renderType":3,"opacity":14,"luminance":15,"maxLevel":4,"blastResistance":500,"stackable":null,"texture":[[12,3]]},
+    "oak_log":    {"id":17,"texture":[[3,30],[3,29]]},
+    "spruce_log": {"id":17,"bd":1,"texture":[[3,32],[3,31]]},
+    "birch_log":  {"id":17,"bd":2,"texture":[[4,2],[4,1]]},
+    "jungle_log": {"id":17,"bd":3,"texture":[[4,4],[4,3]]},
+    "oak_leaves":    {"id":18,"opacity":1,"texture":[[5,23]]},
+    "spruce_leaves": {"id":18,"bd":1,"opacity":1,"texture":[[5,24]]},
+    "birch_leaves":  {"id":18,"bd":2,"opacity":1,"texture":[[5,25]]},
+    "jungle_leaves": {"id":18,"bd":3,"opacity":1,"texture":[[5,26]]},
+    "glass": {"id":20,"opacity":0,"texture":[[5,18]]},
+    "acacia_leaves":   {"id":161,"opacity":1,"texture":[[6,1]]},
+    "dark_oak_leaves": {"id":161,"bd":1,"opacity":1,"texture":[[6,2]]},
+    "dandelion":   {"id":37,"renderType":1,"opacity":0,"texture":[[2, 29]]},
+    "poppy":       {"id":38,"renderType":1,"opacity":0,"texture":[[2, 30]]},
+    "blue_orchid": {"id":38,"bd":1,"renderType":1,"opacity":0,"texture":[[2, 31]]},
+    "allium":      {"id":38,"bd":2,"renderType":1,"opacity":0,"texture":[[2, 32]]},
+    "azure_bluet": {"id":38,"bd":3,"renderType":1,"opacity":0,"texture":[[3, 1]]},
+    "red_tulip":   {"id":38,"bd":4,"renderType":1,"opacity":0,"texture":[[3, 2]]},
+    "orange_tulip":{"id":38,"bd":5,"renderType":1,"opacity":0,"texture":[[3, 3]]},
+    "white_tulip": {"id":38,"bd":6,"renderType":1,"opacity":0,"texture":[[3, 4]]},
+    "pink_tulip":  {"id":38,"bd":7,"renderType":1,"opacity":0,"texture":[[3, 5]]},
+    "oxeye_daisy": {"id":38,"bd":8,"renderType":1,"opacity":0,"texture":[[3, 6]]},
+    "crafting_table": {"id":58,"texture":[[5,6],[2,15],[5,7],[5,8]]},
+    "cactus": {"id":81,"renderType":9,"opacity":0,"texture":[[6,9],[6,11],[6,10]]},
+    "glowstone": {"id":89,"luminance":15,"blastResistance":0.3,"texture":[[7,18]]}
+}
+}

+ 180 - 0
legacy/WebMC/src/World/noise.js

@@ -0,0 +1,180 @@
+import { vec3, vec2 } from "../utils/gmath.js";
+
+function lerp(a, b, t) {
+    return (1 - t) * a + t * b;
+}
+
+// ease curves 3t^2 - 2t^3
+function fade(t) {
+    return t * t * ((2 * t) - 3);
+}
+// 6t^5 - 15t^4 + 10t^3
+function fade2(t) {
+    return t * t * t * (t * (t * 6 - 15) + 10);
+}
+
+// return (-1.0, 1.0]
+function fnoise(int32) {
+    int32 = (int32 << 13) ^ int32;
+    return (1.0 - ((int32 * (int32 * int32 * 15731 + 789221) + 1376312589) & 0x7fffffff) / 1073741824.0);
+}
+
+// 在JS中,按照IEEE 754-2008标准的定义,所有数字都以双精度64位浮点格式表示。
+// 在此标准下,无法精确表示的非常大的整数将自动四舍五入。
+// 确切地说,JS 中的Number类型只能安全地表示-9007199254740991(-(2^53-1))和9007199254740991(2^53-1)之间的整数,
+// 任何超出此范围的整数值都可能失去精度。
+
+// 基于这点 种子的范围[-(2^53-1), 2^53-1],一共2^54-1个。
+// 最接近2^53的质数 = 9007199254740881 = 1FFFFFFFFFFF91
+// Number.MAX_SAFE_INTEGER (2^53-1) > max prime number = 9007199254740881 = 1FFFFFFFFFFF91
+const MAX_SAFE_PRIME = 0x1FFFFFFFFFFF91;
+
+function toSeed(seed) {
+    let s = Number(seed);
+    if (!Number.isNaN(s)) seed = s;
+    if (typeof seed === "number") {
+        if (Number.isInteger(seed)) {
+            if (Number.isSafeInteger(seed)) s = seed;
+            else s = seed % MAX_SAFE_PRIME;
+        }
+        else {
+            seed *= Math.PI;
+            s = Number(seed.toString().replace(".", "").slice(0, 16)) % MAX_SAFE_PRIME;
+        }
+    }
+    else if (typeof seed === "string") {
+        let i = 0, t = "";
+        for (; i < seed.length; ++i) {
+            let j = seed.charCodeAt(i);
+            if ((t + j).length > 53) break;
+            t += j;
+        }
+        s = Number(t);
+        for (; i < seed.length; ++i) {
+            s = ((s + seed.charCodeAt(i)) * 48271 + 57) % MAX_SAFE_PRIME;
+        }
+    }
+    return s;
+}
+
+class RandomGen {
+    constructor(seed = Date.now()) {
+        this.seed = toSeed(seed);
+    };
+}
+
+class LCG extends RandomGen {
+    constructor(seed = Date.now()) {
+        super(seed);
+        this.x = seed;
+    };
+    nextInt(n) {
+        this.x = (48271 * this.x + 57) % MAX_SAFE_PRIME;
+        if (n) return this.x % n;
+        return this.x;
+    };
+}
+
+class Noise {
+    constructor(seed = Date.now()) {
+        this.setSeed(seed);
+        let random = this.random = new LCG(this.seed);
+        let perm = [...Array(256)].map((_, i) => i);
+        for (let i = 0, j, t; i < 256; ++i) {
+            j = random.nextInt(256 - i) + i;
+            t = perm[i];
+            perm[i] = perm[i + 256] = perm[j];
+            perm[j] = t;
+        }
+        this.permutations = perm;
+    };
+    setSeed(seed) {
+        this.originalSeed = seed;
+        this.seed = toSeed(seed);
+    };
+    getSeed() { return this.seed; };
+}
+
+// perlin noise
+class PerlinNoise extends Noise {
+    constructor(seed) {
+        super(seed);
+        let grad3 = [
+            [1,1,0],[-1,1,0],[1,-1,0],[-1,-1,0],
+            [1,0,1],[-1,0,1],[1,0,-1],[-1,0,-1],
+            [0,1,1],[0,-1,1],[0,1,-1],[0,-1,-1],
+        ].map(([x, y, z]) => vec3.create(x, y, z));
+        let gradP = this.gradP = [];
+        this.permutations.forEach(i => gradP.push(grad3[i % 12]));
+        // console.log(this.gradP.join("|"))
+    };
+    gen(x, y, z) {
+        if (y == 1) return this.gen2d(x, z);
+        return this.gen3d(x, y, z);
+    };
+    // return [-1, 1];
+    gen2d(x, y) {
+        const {floor} = Math, perm = this.permutations, gradP = this.gradP;
+        let fx = floor(x), fy = floor(y),
+            rx = x - fx, ry = y - fy;
+        fx &= 255; fy &= 255; // %= 256
+        let n00 = vec2.dot(gradP[fx + perm[fy]        ], [    rx,     ry]),
+            n01 = vec2.dot(gradP[fx + perm[fy + 1]    ], [    rx, ry - 1]),
+            n10 = vec2.dot(gradP[fx + perm[fy]     + 1], [rx - 1,     ry]),
+            n11 = vec2.dot(gradP[fx + perm[fy + 1] + 1], [rx - 1, ry - 1]),
+            u = fade2(rx);
+        return lerp(
+            lerp(n00, n10, u),
+            lerp(n01, n11, u),
+            fade2(ry)
+        );
+    };
+    // return [-1, 1];
+    gen3d(x, y, z) {
+        const {floor} = Math,
+              {gradP, permutations: perm} = this;
+        // Find unit grid cell containing point
+        let X = floor(x), Y = floor(y), Z = floor(z);
+        // Get relative xyz coordinates of point within that cell
+        x -= X; y -= Y; z -= Z;
+        X &= 255; Y &= 255; Z &= 255; // %= 256
+
+        // Calculate noise contributions from each of the eight corners
+        let n000 = vec3.dot(gradP[X+  perm[Y+  perm[Z  ]]], [  x,   y,   z]),
+            n001 = vec3.dot(gradP[X+  perm[Y+  perm[Z+1]]], [  x,   y, z-1]),
+            n010 = vec3.dot(gradP[X+  perm[Y+1+perm[Z  ]]], [  x, y-1,   z]),
+            n011 = vec3.dot(gradP[X+  perm[Y+1+perm[Z+1]]], [  x, y-1, z-1]),
+            n100 = vec3.dot(gradP[X+1+perm[Y+  perm[Z  ]]], [x-1,   y,   z]),
+            n101 = vec3.dot(gradP[X+1+perm[Y+  perm[Z+1]]], [x-1,   y, z-1]),
+            n110 = vec3.dot(gradP[X+1+perm[Y+1+perm[Z  ]]], [x-1, y-1,   z]),
+            n111 = vec3.dot(gradP[X+1+perm[Y+1+perm[Z+1]]], [x-1, y-1, z-1]);
+
+        // Compute the fade curve value for x, y, z
+        // let [u, v, w] = [x, y, z].map(fade);
+        let u = fade2(x), v = fade2(y), w = fade2(z);
+
+        // Interpolate
+        return lerp(
+            lerp(
+                lerp(n000, n100, u),
+                lerp(n001, n101, u), w),
+            lerp(
+                lerp(n010, n110, u),
+                lerp(n011, n111, u), w), v);
+    };
+}
+
+// simplex noise
+class SimplexNoise extends Noise {}
+
+// Fractal Brownian Motion (fbm) 分形叠加
+// freq frequency 频率
+// amp amplitude 振幅
+// octave 倍频
+function fbm(noise, x, freq, amp, octave, normalized) {
+
+}
+
+export {
+    PerlinNoise
+}

+ 3 - 0
legacy/WebMC/src/World/worldDefaultConfig.json

@@ -0,0 +1,3 @@
+{
+    "terrain": "pre-classic"
+}

+ 21 - 0
legacy/WebMC/src/globalVeriable.js

@@ -0,0 +1,21 @@
+
+let sUserAgent = navigator.userAgent.toLowerCase();
+window.isTouchDevice = [
+    "android", "ucweb",
+    "iphone os", "ipad", "ipod",
+    "windows phone os", "windows ce", "windows mobile",
+    "midp", "symbianos", "blackberry", "hpwos", "rv:1.2.3.4",
+].some(s => sUserAgent.includes(s));
+// for iPadOS
+if (!isTouchDevice && sUserAgent.includes("safari") && sUserAgent.includes("version")) {
+    if (!sUserAgent.includes("iphone") && "ontouchend" in document)
+        window.isTouchDevice = true;
+}
+
+import { isWebGL2Context } from "./utils/isWebGL2Context.js";
+window.isSupportWebGL2 = (() => {
+    let ctx = null;
+    try { ctx = document.createElement("canvas").getContext("webgl2"); }
+    catch(e) { ctx = null; }
+    return isWebGL2Context(ctx);
+})();

+ 16 - 0
legacy/WebMC/src/main.js

@@ -0,0 +1,16 @@
+
+import "./globalVeriable.js";
+
+window.addEventListener("contextmenu", e => { if (e.cancelable) e.preventDefault(); }, true);
+
+const updatePixelRatio = () => {
+    let dpr = window.devicePixelRatio;
+    document.documentElement.style.setProperty("--device-pixel-ratio", dpr);
+    window.dispatchEvent(new Event("dprchange"));
+    matchMedia(`(resolution: ${dpr}dppx)`).addEventListener("change", updatePixelRatio, { once: true });
+};
+updatePixelRatio();
+
+
+import {} from "./UI/index.js";
+import {} from "./processingPictures.js";

+ 380 - 0
legacy/WebMC/src/processingPictures.js

@@ -0,0 +1,380 @@
+
+class Canvas2D {
+    constructor(width = 0, height = 0) {
+        this.canvas = document.createElement("canvas");
+        this.ctx = this.canvas.getContext("2d");
+        if (width > 0 && height > 0)
+            this.setSize(width, height);
+        return this.wrapper = new Proxy(this, {
+            get(tar, key) {
+                if (key in tar) return tar[key];
+                if (key in tar.canvas)
+                    return typeof tar.canvas[key] === "function"
+                        ? tar.canvas[key].bind(tar.canvas)
+                        : tar.canvas[key];
+                if (key in tar.ctx)
+                    return typeof tar.ctx[key] === "function"
+                        ? tar.ctx[key].bind(tar.ctx)
+                        : tar.ctx[key];
+            },
+            set(tar, key, value) {
+                if (key in tar) tar[key] = value;
+                if (key in tar.canvas) tar.canvas[key] = value;
+                if (key in tar.ctx) tar.ctx[key] = value;
+                return true;
+            }
+        });
+    };
+    setSize(w, h, smoothing = false, smoothingQuality = 2) {
+        this.canvas.width = w;
+        this.canvas.height = h;
+        this.setImgSmoothingEnabled(smoothing);
+        if (smoothing)
+            this.setImgSmoothingQuality(smoothingQuality);
+        return this.wrapper;
+    };
+    setImgSmoothingEnabled(tf) {
+        this.ctx.mozImageSmoothingEnabled    = tf;
+        this.ctx.webkitImageSmoothingEnabled = tf;
+        this.ctx.msImageSmoothingEnabled     = tf;
+        this.ctx.imageSmoothingEnabled       = tf;
+        this.ctx.oImageSmoothingEnabled      = tf;
+        return this.wrapper;
+    };
+    setImgSmoothingQuality(level = 2) {
+        this.ctx.imageSmoothingQuality = (["low", "medium", "high"])[level];
+        return this.wrapper;
+    };
+    toImage(onload = function() {}, onerror = function() {}) {
+        let img = new Image(this.canvas.width, this.canvas.height);
+        img.onload = onload;
+        img.onerror = onerror;
+        img.src = this.canvas.toDataURL();
+        return img;
+    };
+    clear() {
+        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+        return this.wrapper;
+    };
+    cropAndZoom(img, sx, sy, sw, sh, coordZoomFactor = 1, {
+        finalZoom = 1,
+        canvasW = sw * coordZoomFactor * finalZoom,
+        canvasH = sh * coordZoomFactor * finalZoom,
+    } = {}) {
+        sx *= coordZoomFactor; sy *= coordZoomFactor;
+        sw *= coordZoomFactor; sh *= coordZoomFactor;
+        this.setSize(canvasW, canvasH);
+        this.ctx.drawImage(img, sx, sy, sw, sh, 0, 0, canvasW, canvasH);
+        return this.wrapper;
+    };
+    darken(ratio = 0.5) {
+        const {canvas: {width, height}, ctx} = this;
+        ctx.globalCompositeOperation = "source-atop";
+        ctx.fillStyle = `rgba(0, 0, 0, ${ratio})`;
+        ctx.fillRect(0, 0, width, height);
+        ctx.globalCompositeOperation = "source-over";
+        return this.wrapper;
+    };
+}
+
+// if mipLevel == 0  gen all mip level
+function textureMipmapByTile(img, mipLevel = 1, tileCount = [32, 16]) {
+    let canvas = new Canvas2D(),
+        w = img.width, h = img.height, mipmap = [],
+        [wTileCount, hTileCount] = tileCount,
+        singleW = w / wTileCount, singleH = h / hTileCount,
+        hSingleW = singleW / 2, hSingleH = singleH / 2;
+    /**single tile:
+     *                  +----+
+     * +--+             |4343|
+     * |12| =>          |2121|
+     * |34| =>          |4343|
+     * +--+             |2121|
+     *                  +----+
+     */
+    w *= 4; h *= 4;
+    for (let i = 0; w > wTileCount && h > hTileCount && (mipLevel? i < mipLevel: true) ; ++i) {
+        w = (w >>> 1) || w;
+        h = (h >>> 1) || h;
+        canvas.setSize(w, h, true);
+        let sw = w / wTileCount / 2, sh = h / hTileCount / 2,
+            hsw = sw / 2, hsh = sh / 2;
+        for (let x = 0; x < wTileCount; ++x)
+        for (let y = 0; y < hTileCount; ++y) {
+            canvas.drawImage(img, x * singleW + hSingleW, y * singleH + hSingleH, hSingleW, hSingleH,
+                x * 2 * sw,           y * 2 * sh, hsw, hsh);
+            canvas.drawImage(img, x * singleW,            y * singleH + hSingleH,  singleW, hSingleH,
+                x * 2 * sw + hsw,     y * 2 * sh,  sw, hsh);
+            canvas.drawImage(img, x * singleW,            y * singleH + hSingleH, hSingleW, hSingleH,
+                x * 2 * sw + hsw * 3, y * 2 * sh, hsw, hsh);
+
+            canvas.drawImage(img, x * singleW + hSingleW, y * singleH, hSingleW, singleH,
+                x * 2 * sw,           y * 2 * sh + hsh, hsw, sh);
+            canvas.drawImage(img, x * singleW,            y * singleH,  singleW, singleH,
+                x * 2 * sw + hsw,     y * 2 * sw + hsh,  sw, sh);
+            canvas.drawImage(img, x * singleW,            y * singleH, hSingleW, singleH,
+                x * 2 * sw + hsw * 3, y * 2 * sh + hsh, hsw, sh);
+
+            canvas.drawImage(img, x * singleW + hSingleW, y * singleH, hSingleW, hSingleH,
+                x * 2 * sw,           y * 2 * sh + hsh * 3, hsw, hsh);
+            canvas.drawImage(img, x * singleW,            y * singleH,  singleW, hSingleH,
+                x * 2 * sw + hsw,     y * 2 * sh + hsh * 3,  sw, hsh);
+            canvas.drawImage(img, x * singleW,            y * singleH, hSingleW, hSingleH,
+                x * 2 * sw + hsw * 3, y * 2 * sh + hsh * 3, hsw, hsh);
+        }
+        mipmap[i] = canvas.toImage();
+    }
+    return img.mipmap = mipmap;
+}
+
+function prepareTextureAarray(img, tileCount = [32, 16]) {
+    let w = img.width, h = img.height,
+        [wTileCount, hTileCount] = tileCount,
+        singleW = w / wTileCount, singleH = h / hTileCount,
+        canvas = new Canvas2D(singleW, singleH);
+    img.texture4array = [];
+    for (let y = 0; y < hTileCount; ++y)
+    for (let x = 0; x < wTileCount; ++x) {
+        canvas.cropAndZoom(img, x * singleW, y * singleH, singleW, singleH);
+        img.texture4array.push(canvas.toImage());
+    }
+    img.texture4array.tileCount = tileCount;
+    img.texture4array.singleW = singleW;
+    img.texture4array.singleH = singleH;
+    img.texture4array.altesCount = wTileCount * hTileCount;
+    return img.texture4array;
+}
+
+import { asyncLoadResByUrl, setResource } from "./utils/loadResources.js";
+
+const rootStyle = document.createElement("style");
+rootStyle.id = "mc-root-style";
+document.head.prepend(rootStyle);
+
+function setBorderOrBgStyle(name, img, {
+    slice = [],
+    isBorder = slice.length !== 0,
+    url = img.src,
+    width = img.width, height = img.height,
+} = {}) {
+    if (slice.length === 1) slice = [slice[0], slice[0], slice[0], slice[0]];
+    if (slice.length === 2) slice = [slice[0], slice[1], slice[0], slice[1]];
+    if (slice.length === 3) slice = [slice[0], slice[1], slice[2], slice[1]];
+    rootStyle.sheet.insertRule(`:root {
+        --mc-ui-${name}-img: url("${url}");
+        --mc-ui-${name}-img-width: ${width};
+        --mc-ui-${name}-img-height: ${height};`
+    + (!isBorder? "": `
+        --mc-ui-${name}-img-border-top: ${slice[0]};
+        --mc-ui-${name}-img-border-right: ${slice[1]};
+        --mc-ui-${name}-img-border-bottom: ${slice[2]};
+        --mc-ui-${name}-img-border-left: ${slice[3]};`)
+    + `
+    }`, 0);
+    setResource(`mc-ui-${name}-img`, img);
+    return img;
+}
+
+const genDrawAndSet = (img, canvas, coordZoomFactor) => (sx, sy, sw, sh, name, styleOption, drawOption) =>
+    setBorderOrBgStyle(name, canvas.cropAndZoom(img, sx, sy, sw, sh, coordZoomFactor, drawOption).toImage(), styleOption);
+
+asyncLoadResByUrl("texture/gui.png")
+.then(img => {
+    const coordZoomFactor = img.width / 256;
+    const canvas = new Canvas2D();
+    const drawAndSet = genDrawAndSet(img, canvas, coordZoomFactor);
+
+    drawAndSet(0, 66, 200, 20, "button", {slice: [3]});
+    drawAndSet(0, 86, 200, 20, "button-hover", {slice: [3]});
+    drawAndSet(0, 46, 200, 20, "button-active", {slice: [3]});
+    setBorderOrBgStyle("button-disabled", canvas.toImage(), {slice: [3]});
+
+    drawAndSet(0, 0, 182, 22, "hotbar-background");
+    rootStyle.sheet.insertRule(":root { --mc-ui-hotbar-item-cell-width: 20; --mc-ui-hotbar-item-cell-height: 20; }", 0);
+    drawAndSet(0, 22, 24, 24, "hotbar-selector-background");
+
+    drawAndSet(200, 46, 16, 16, "inventory-item-background");
+    drawAndSet(228, 248, 28, 8, "hotbar-inventory-btn-foreground");
+
+    // move buttons
+    for (let [name, coord] of Object.entries({
+        up: [0, 107, 26, 26],
+        left: [26, 107, 26, 26],
+        down: [52, 107, 26, 26],
+        right: [78, 107, 26, 26],
+        jump: [104, 107, 26, 26],
+        upleft: [0, 133, 26, 26],
+        upright: [26, 133, 26, 26],
+        flyup: [52, 133, 26, 26],
+        flydown: [78, 133, 26, 26],
+        fly: [104, 133, 26, 26],
+        sneak: [218, 82, 18, 18],
+    })) {
+        drawAndSet(...coord, "move-btn-" + name);
+        setBorderOrBgStyle(`move-btn-${name}-active`, canvas.darken().toImage());
+    }
+});
+asyncLoadResByUrl("texture/spritesheet.png")
+.then(img => {
+    const coordZoomFactor = img.width / 256;
+    const canvas = new Canvas2D();
+    const drawAndSet = genDrawAndSet(img, canvas, coordZoomFactor);
+    drawAndSet(60, 0, 18, 18, "close-btn");
+    drawAndSet(78, 0, 18, 18, "close-btn-active");
+    drawAndSet(34, 43, 14, 14, "inventory-items", {slice: [3]});
+    drawAndSet(49, 43, 13, 14, "inventory-tab-background-left", {slice: [3]});
+    drawAndSet(65, 55, 14, 14, "inventory-tab-background-right", {slice: [3]});
+    drawAndSet(8, 32, 8, 8, "hotbar-inventory-btn-bg", {slice: [1]});
+    drawAndSet(0, 32, 8, 8, "hotbar-inventory-btn-bg-active", {slice: [1]});
+});
+asyncLoadResByUrl("texture/background.png")
+.then(img => {
+    const coordZoomFactor = img.width / 16;
+    const canvas = new Canvas2D();
+    canvas.cropAndZoom(img, 0, 0, 16, 16, coordZoomFactor, { canvasW: 128, canvasH: 128, });
+    setBorderOrBgStyle("background", canvas.toImage());
+    setBorderOrBgStyle("background-darken", canvas.darken().toImage());
+});
+asyncLoadResByUrl("texture/panorama.png")
+.then(img => {
+    const {width, height} = img;
+    const canvas = new Canvas2D(height, height);
+    let ans = [];
+    for (let i = 0; i < 6; ++i) {
+        let face = "pz,px,nz,nx,py,ny".split(",")[i];
+        canvas.cropAndZoom(img, i * height, 0, height, height);
+        ans[face] = canvas.toImage();
+        setResource("welcomePage/texture_" + face, ans[face]);
+    }
+    for (let face of "px,nx,py,ny,pz,nz".split(","))
+        ans.push(ans[face]);
+    setResource("welcomePage/textures", ans);
+});
+asyncLoadResByUrl("texture/title.png");
+
+import { Render } from "./Renderer/Render.js";
+import { Camera } from "./Renderer/Camera.js";
+import * as glsl from "./Renderer/glsl.js";
+import { mat4, vec3 } from "./utils/gmath.js";
+import { Block } from "./World/Block.js";
+class BlockInventoryTexRender extends Render {
+    constructor() {
+        let canvas = document.createElement("canvas");
+        super(canvas);
+        this.canvas = canvas;
+        this.ctx2d = new Canvas2D();
+        this.setSize(512, 512);
+        const {ctx} = this;
+        ctx.enable(ctx.DEPTH_TEST);
+        const {SQRT2} = Math;
+        const wsize = 0.425 + SQRT2/4;
+        let mainCamera = new Camera(this.aspectRatio, {
+            projectionType: Camera.projectionType.ortho,
+            viewType: Camera.viewType.lookAt,
+            left: -wsize, right: wsize, bottom: -wsize, top: wsize, near: -1, far: 5,
+            position: [1, 12 / 16, 1],
+        });
+        this.mainCamera = mainCamera;
+        this.addCamera(mainCamera);
+        this.prg = this.isWebGL2
+            ? this.createProgram("blockInventoryTexure", glsl.blockInventoryTexure_webgl2.vert, glsl.blockInventoryTexure_webgl2.frag)
+            : this.createProgram("blockInventoryTexure", glsl.blockInventoryTexure.vert, glsl.blockInventoryTexure.frag);
+        this.bo = {
+            ver: this.createVbo([], ctx.DYNAMIC_DRAW),
+            nor: this.createVbo([], ctx.DYNAMIC_DRAW),
+            col: this.createVbo([], ctx.DYNAMIC_DRAW),
+            tex: this.createVbo([], ctx.DYNAMIC_DRAW),
+            ele: this.createIbo([], ctx.DYNAMIC_DRAW),
+        };
+        let mM = mat4.identity();
+        mat4.translate(mM, [-0.5, -0.5, -0.5], mM);
+        this.mM = mM;
+        this.mvpM = mat4.multiply(mainCamera.projview, mM);
+        let imM = mat4.inverse(mM);
+        this.itmM = mat4.transpose(imM, imM);
+    };
+    toImage() {
+        let img = new Image(this.canvas.width, this.canvas.height);
+        img.src = this.canvas.toDataURL();
+        return img;
+    };
+    gen(block) {
+        if (block.name === "air") {
+            this.ctx2d.clear();
+            return this.ctx2d.toImage();
+        }
+        if (block.renderType === Block.renderType.NORMAL || block.renderType === Block.renderType.CACTUS) {
+            let normal = [], color = [], ver = [], tex = [], ele = [], totalVer = 0;
+            for (let face in block.vertexs) {
+                let vs = block.vertexs[face],
+                    pa = vec3.create(vs[0], vs[1], vs[2]),
+                    pb = vec3.create(vs[3], vs[4], vs[5]),
+                    pc = vec3.create(vs[6], vs[7], vs[8]),
+                    ab = vec3.subtract(pb, pa),
+                    ac = vec3.subtract(pc, pa),
+                    n = vec3.cross(ab, ac),
+                    verNum = vs.length / 3;
+                for (let i = 0; i < verNum; ++i) {
+                    normal.push(...n);
+                    color.push(1.0, 1.0, 1.0, 1.0);
+                }
+                ver.push(...vs);
+                tex.push(...block.texture.uv[face]);
+                ele.push(...block.elements[face].map(v => v + totalVer));
+                totalVer += verNum;
+            }
+            const blockTex = this.getOrCreateTexture(block.texture.img);
+            const {ctx, prg} = this;
+            this.bindBoData(this.bo.ver, ver, {drawType: ctx.DYNAMIC_DRAW});
+            this.bindBoData(this.bo.nor, normal, {drawType: ctx.DYNAMIC_DRAW});
+            this.bindBoData(this.bo.col, color, {drawType: ctx.DYNAMIC_DRAW});
+            this.bindBoData(this.bo.tex, tex, {drawType: ctx.DYNAMIC_DRAW});
+            this.bindBoData(this.bo.ele, ele, {drawType: ctx.DYNAMIC_DRAW});
+            prg.use().bindTex("blockTex", blockTex)
+            .setUni("diffuseLightColor", block.luminance? [0, 0, 0]: [1.0, 1.0, 1.0])
+            .setUni("ambientLightColor", block.luminance? [1, 1, 1]: [0.2, 0.2, 0.2])
+            .setUni("diffuseLightDirection", vec3.normalize([0.5, 3.0, 4.0]))
+            .setUni("diffuseLightDirection", vec3.normalize([0.4, 1, 0.7]))
+            .setUni("mvpMatrix", this.mvpM)
+            .setUni("normalMatrix", this.itmM)
+            .setAtt("position", this.bo.ver)
+            .setAtt("normal", this.bo.nor, 3)
+            .setAtt("color", this.bo.col)
+            .setAtt("textureCoord", this.bo.tex);
+            ctx.clear(ctx.COLOR_BUFFER_BIT | ctx.DEPTH_BUFFER_BIT);
+            ctx.bindBuffer(this.bo.ele.type, this.bo.ele);
+            ctx.drawElements(ctx.TRIANGLES, this.bo.ele.length, ctx.UNSIGNED_SHORT, 0);
+            ctx.flush();
+            return this.toImage();
+        }
+        const {ctx2d} = this;
+        ctx2d.clear();
+        const img = block.texture.img.mipmap? block.texture.img.mipmap[0]: block.texture.img;
+        const w = img.width, h = img.height, uv = Object.values(block.texture.uv)[0];
+        if (img.texture4array) {
+            const t = img.texture4array, {singleW, singleH} = t;
+            if (Array.isArray(t))
+                ctx2d.drawImage(t[uv[2]], 0, 0, singleW, singleH, 0, 0, this.canvas.width, this.canvas.height);
+            else
+                ctx2d.drawImage(t, 0, singleH * uv[2], singleW, singleH, 0, 0, this.canvas.width, this.canvas.height);
+        }
+        else
+            ctx2d.drawImage(img, w * uv[0], h * uv[1], w * (uv[6] - uv[0]), h * (uv[4] - uv[1]), 0, 0, this.canvas.width, this.canvas.height);
+        return ctx2d.toImage();
+    };
+    setSize(w, h, dpr = 1) {
+        super.setSize(w, h, dpr);
+        this.ctx2d.setSize(w, h);
+    };
+}
+const blockInventoryTexRender = new BlockInventoryTexRender();
+function blockInventoryTexture(block) {
+    return blockInventoryTexRender.gen(block);
+}
+
+
+export {
+    textureMipmapByTile,
+    prepareTextureAarray,
+    blockInventoryTexture,
+};

+ 79 - 0
legacy/WebMC/src/utils/EventDispatcher.js

@@ -0,0 +1,79 @@
+
+class EventDispatcher {
+    constructor() {
+        this._listeners = {};
+        this._IDcount = 0n;
+        this._IDmap = new Map();
+    };
+    hasEventListener(type, listener) {
+        return !!(this._listeners[type] && this._listeners[type].find(o => o.listener === listener));
+    };
+    addEventListener(type, listener, {
+        once = false,
+    } = {}) {
+        if (typeof listener !== "function") return console.warn("Cannot bind a non-function as an event listener!");
+        const listeners = this._listeners[type] || [];
+        this._listeners[type] = listeners;
+        let i = listeners.findIndex(o => o.listener === listener);
+        if (i !== -1) return this._IDmap[listeners[i].id];
+        let t = { listener, once, type, id: this._IDcount++ };
+        this._IDmap.set(t.id, t);
+        listeners.push(t);
+        return t.id;
+    };
+    removeEventListenerByID(id) {
+        if (typeof id !== "bigint" || !this._IDmap.has(id)) return false;
+        let t = this._IDmap.get(id);
+        this._IDmap.delete(id);
+        let arr = this._listeners[t.type];
+        arr.splice(arr.indexOf(t), 1);
+        return true;
+    };
+    removeEventListener(typeOrID, listener) {
+        if (this.removeEventListenerByID(typeOrID)) return true;
+        const type = typeOrID;
+        if (!(type in this._listeners)) return false;
+        let arr = this._listeners[type],
+            i = arr.findIndex(o => o.listener === listener);
+        if (i !== -1) {
+            this._IDmap.delete(arr[i].id);
+            arr.splice(i, 1);
+        }
+        return true;
+    };
+    dispatchEvent(type, ...datas) {
+        if (!(type in this._listeners)) return;
+        return this._listeners[type].slice(0).map(o => {
+            o.listener(...datas);
+            if (o.once) this.removeEventListenerByID(o.id);
+        });
+    };
+};
+
+class EventDispatcherManager {
+    constructor() {
+        this._eventDispatchers = {};
+    };
+    hasEventDispatcher(name) {
+        return !!this._eventDispatchers[name];
+    };
+    addEventDispatcher(name, eventDispatcher = new EventDispatcher()) {
+        return this._eventDispatchers[name] = eventDispatcher;
+    };
+    getOrNewEventDispatcher(name) {
+        if (!(name in this._eventDispatchers))
+            this.addEventDispatcher(name);
+        return this._eventDispatchers[name];
+    };
+    removeEventDispatcher(name) {
+        delete this._eventDispatchers[name];
+    };
+};
+
+const eventDispatcherManager = new EventDispatcherManager();
+
+export {
+    EventDispatcher,
+    eventDispatcherManager,
+    eventDispatcherManager as edm,
+};

+ 42 - 0
legacy/WebMC/src/utils/FiniteStateMachine.js

@@ -0,0 +1,42 @@
+
+import { EventDispatcher } from "./EventDispatcher.js";
+
+class FiniteStateMachine extends EventDispatcher {
+    constructor({
+        id = "",
+        initial = "",
+        transitions = [],
+    } = {}) {
+        super();
+        this.id = id;
+        this.initial = initial;
+        this.currentState = initial;
+        this.graph = {};
+        this.addTransitions(transitions);
+    };
+    addTransitions(transitions) { transitions.forEach(transition => this.addTransition(transition)); };
+    addTransition({
+        from, to,
+        eventName = from + "=>" + to,
+    } = {}) {
+        this.graph[from] = this.graph[from] || {};
+        this.graph[to] = this.graph[to] || {};
+        this.graph[from][to] = eventName;
+    };
+    transition(to, ...data) {
+        const from = this.currentState;
+        // console.log(from, "=>", to, this.graph[from][to], data);
+        if (!(from in this.graph)) return console.error(`FSM: cannot find state "${from}"`);
+        if (!(to in this.graph)) return console.error(`FSM: cannot find state "${to}"`);
+        if (!(to in this.graph[from])) return console.error(`FSM: cannot find transition "${from}" => "${to}"`);
+        const eventName = this.graph[from][to];
+        this.currentState = to;
+        this.dispatchEvent(eventName, ...data);
+        this.dispatchEvent("transitioned", from, to, eventName, data);
+    };
+};
+
+export {
+    FiniteStateMachine,
+    FiniteStateMachine as FSM,
+};

+ 501 - 0
legacy/WebMC/src/utils/gmath.js

@@ -0,0 +1,501 @@
+
+let Mat4Type = Float32Array,
+    Vec3Type = Float32Array;
+
+const EPSILON = 0.000001;
+
+const mat4 = {
+    setArrType(arrType) { return Mat4Type = arrType; },
+    identity(src = new Mat4Type(16)) {
+        src.set([
+            1, 0, 0, 0,
+            0, 1, 0, 0,
+            0, 0, 1, 0,
+            0, 0, 0, 1
+        ]);
+        return src;
+    },
+    //相乘 参3=参1x参2
+    multiply(mat1, mat2, dest = new Mat4Type(16)){
+        let [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p] = mat1,
+            [A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P] = mat2;
+        dest.set([
+            A * a + B * e + C * i + D * m,
+            A * b + B * f + C * j + D * n,
+            A * c + B * g + C * k + D * o,
+            A * d + B * h + C * l + D * p,
+            E * a + F * e + G * i + H * m,
+            E * b + F * f + G * j + H * n,
+            E * c + F * g + G * k + H * o,
+            E * d + F * h + G * l + H * p,
+            I * a + J * e + K * i + L * m,
+            I * b + J * f + K * j + L * n,
+            I * c + J * g + K * k + L * o,
+            I * d + J * h + K * l + L * p,
+            M * a + N * e + O * i + P * m,
+            M * b + N * f + O * j + P * n,
+            M * c + N * g + O * k + P * o,
+            M * d + N * h + O * l + P * p
+        ]);
+        return dest;
+    },
+    //原始矩阵 缩放向量 保存结果的矩阵
+    scale(mat, vec, dest = new Mat4Type(16)) {
+        dest.set([
+            mat[0]  * vec[0],
+            mat[1]  * vec[0],
+            mat[2]  * vec[0],
+            mat[3]  * vec[0],
+            mat[4]  * vec[1],
+            mat[5]  * vec[1],
+            mat[6]  * vec[1],
+            mat[7]  * vec[1],
+            mat[8]  * vec[2],
+            mat[9]  * vec[2],
+            mat[10] * vec[2],
+            mat[11] * vec[2],
+            mat[12], mat[13], mat[14], mat[15]
+        ]);
+        return dest;
+    },
+    //原始矩阵 从原点开始移动一定距离的向量 结果矩阵
+    translate(mat, vec, dest = new Mat4Type(16)) {
+        let [x, y, z] = vec,
+            [a, b, c, d, e, f, g, h, i, j, k, l] = mat;
+        dest.set([
+            a, b, c, d,
+            e, f, g, h,
+            i, j, k, l,
+            mat[12] + a * x + e * y + i * z,
+            mat[13] + b * x + f * y + j * z,
+            mat[14] + c * x + g * y + k * z,
+            mat[15] + d * x + h * y + l * z
+        ]);
+        return dest;
+    },
+    //原始矩阵 旋转角度 旋转轴向量 结果矩阵
+    rotate(mat, angle, axis, dest = new Mat4Type(16)) {
+        let sq = Math.sqrt(axis[0] ** 2 + axis[1] ** 2 + axis[2] ** 2);
+        if (!sq) return null;
+        let [a, b, c] = axis;
+        if (sq != 1) {sq = 1 / sq; a *= sq; b *= sq; c *= sq;}
+        let d = Math.sin(angle), e = Math.cos(angle), f = 1 - e,
+            [g, h, i, j, k, l, m, n, o, p, q, r] = mat,
+            s = a * a * f + e,
+            t = b * a * f + c * d,
+            u = c * a * f - b * d,
+            v = a * b * f - c * d,
+            w = b * b * f + e,
+            x = c * b * f + a * d,
+            y = a * c * f + b * d,
+            z = b * c * f - a * d,
+            A = c * c * f + e;
+        dest.set([
+            g * s + k * t + o * u,
+            h * s + l * t + p * u,
+            i * s + m * t + q * u,
+            j * s + n * t + r * u,
+            g * v + k * w + o * x,
+            h * v + l * w + p * x,
+            i * v + m * w + q * x,
+            j * v + n * w + r * x,
+            g * y + k * z + o * A,
+            h * y + l * z + p * A,
+            i * y + m * z + q * A,
+            j * y + n * z + r * A,
+            mat[12], mat[13], mat[14], mat[15]
+        ]);
+        return dest;
+    },
+    //视图变换矩阵
+    //镜头位置向量 镜头参考点向量 镜头方向向量 结果矩阵
+    //将镜头理解为人头 镜头方向就是头顶的朝向
+    // https://www.3dgep.com/understanding-the-view-matrix/
+    lookAt(eye, target, up, dest = new Mat4Type(16)) {
+        let [eyeX, eyeY, eyeZ] = eye,
+            [upX, upY, upZ] = up,
+            [targetX, targetY, targetZ] = target;
+        if (eyeX == targetX && eyeY == targetY && eyeZ == targetZ) return mat4.identity(dest);
+        // z = normal(eye - target)
+        // x = normal(cross(up, z))
+        // y = cross(z, x)
+        let x0, x1, x2, y0, y1, y2,
+            z0 = eyeX - targetX,
+            z1 = eyeY - targetY,
+            z2 = eyeZ - targetZ,
+            l = 1 / Math.sqrt(z0 ** 2 + z1 ** 2 + z2 ** 2);
+        z0 *= l; z1 *= l; z2 *= l;
+        x0 = upY * z2 - upZ * z1;
+        x1 = upZ * z0 - upX * z2;
+        x2 = upX * z1 - upY * z0;
+        l = Math.sqrt(x0 ** 2 + x1 ** 2 + x2 ** 2);
+        if (l) {
+            l = 1 / l;
+            x0 *= l; x1 *= l; x2 *= l;
+        }
+        else x0 = x1 = x2 = 0;
+        y0 = z1 * x2 - z2 * x1; y1 = z2 * x0 - z0 * x2; y2 = z0 * x1 - z1 * x0;
+        l = Math.sqrt(y0 ** 2 + y1 ** 2 + y2 ** 2);
+        if (l) {
+            l = 1 / l;
+            y0 *= l; y1 *= l; y2 *= l;
+        }
+        else y0 = y1 = y2 = 0;
+        // x0, y0, z0, 0,
+        // x1, y1, z1, 0,
+        // x2, y2, z2, 0,
+        // -dot(x, eye), -dot(y, eye), -dot(z, eye), 1
+        dest.set([
+            x0, y0, z0, 0,
+            x1, y1, z1, 0,
+            x2, y2, z2, 0,
+            -(x0 * eyeX + x1 * eyeY + x2 * eyeZ),
+            -(y0 * eyeX + y1 * eyeY + y2 * eyeZ),
+            -(z0 * eyeX + z1 * eyeY + z2 * eyeZ),
+            1
+        ]);
+        return dest;
+    },
+    fpsView(eye, pitch, yaw, rollZ = 0, dest = new Mat4Type(16)) {
+        let [eyeX, eyeY, eyeZ] = eye,
+            cosPitch = Math.cos(pitch),
+            sinPitch = Math.sin(pitch),
+            cosYaw = Math.cos(yaw),
+            sinYaw = Math.sin(yaw),
+            cosZ = Math.cos(-rollZ),
+            sinZ = Math.sin(-rollZ),
+            x0 = cosYaw * cosZ, x1 = cosYaw * sinZ, x2 = -sinYaw,
+            y0 = sinPitch * sinYaw * cosZ - cosPitch * sinZ,
+            y1 = sinPitch * sinYaw * sinZ + cosPitch * cosZ,
+            y2 = sinPitch * cosYaw,
+            z0 = cosPitch * sinYaw * cosZ + sinPitch * sinZ,
+            z1 = cosPitch * sinYaw * sinZ - sinPitch * cosZ,
+            z2 = cosPitch * cosYaw;
+        dest.set([
+            x0, y0, z0, 0,
+            x1, y1, z1, 0,
+            x2, y2, z2, 0,
+            -(x0 * eyeX + x1 * eyeY + x2 * eyeZ),
+            -(y0 * eyeX + y1 * eyeY + y2 * eyeZ),
+            -(z0 * eyeX + z1 * eyeY + z2 * eyeZ),
+            1
+        ]);
+        return dest;
+    },
+    //投影变换矩阵
+    //视角(degrees) 屏幕高宽比 近截面位置>0 远截面位置 结果矩阵
+    // 当想要转换左右手坐标系时,用
+    // 将视图矩阵最后一行的前三个取反 乘 将下面的投影矩阵2fn*d和1取反
+    // 当转换左右手坐标系时 三角形的旋转方向就反过来了 需要注意
+    perspective(fovy, aspect, near, far, dest = new Mat4Type(16)) {
+        let t = 1 / Math.tan(fovy * Math.PI / 360),
+            d = 1 / (far - near);
+        dest.set([
+            t / aspect, 0, 0, 0,
+            0, t, 0, 0,
+            0, 0, -(far + near) * d, -1,
+            0, 0, -2 * far * near * d, 0
+        ]);
+        return dest;
+    },
+    ortho(left, right, bottom, top, near, far, dest = new Mat4Type(16)) {
+        var lr = 1 / (left - right),
+            bt = 1 / (bottom - top),
+            nf = 1 / (near - far);
+        dest.set([
+            -2 * lr,       0,      0, 0,
+                  0, -2 * bt,      0, 0,
+                  0,       0, 2 * nf, 0,
+            (left + right) * lr,
+            (top + bottom) * bt,
+            (far + near) * nf,
+            1
+        ]);
+        return dest;
+    },
+    //转置
+    transpose(mat, dest = new Mat4Type(16)) {
+        dest.set([
+            mat[0], mat[4], mat[8], mat[12],
+            mat[1], mat[5], mat[9], mat[13],
+            mat[2], mat[6], mat[10], mat[14],
+            mat[3], mat[7], mat[11], mat[15]
+        ]);
+        return dest;
+    },
+    //逆矩阵
+    inverse(mat, dest = new Mat4Type(16)) {
+        let [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p] = mat,
+            q = a * f - b * e, r = a * g - c * e,
+            s = a * h - d * e, t = b * g - c * f,
+            u = b * h - d * f, v = c * h - d * g,
+            w = i * n - j * m, x = i * o - k * m,
+            y = i * p - l * m, z = j * o - k * n,
+            A = j * p - l * n, B = k * p - l * o,
+            det = q * B - r * A + s * z + t * y - u * x + v * w;
+        if (det === 0) return mat4.identity(dest);
+        let ivd = 1.0 / det;
+        dest.set([
+            ( f * B - g * A + h * z) * ivd,
+            (-b * B + c * A - d * z) * ivd,
+            ( n * v - o * u + p * t) * ivd,
+            (-j * v + k * u - l * t) * ivd,
+            (-e * B + g * y - h * x) * ivd,
+            ( a * B - c * y + d * x) * ivd,
+            (-m * v + o * s - p * r) * ivd,
+            ( i * v - k * s + l * r) * ivd,
+            ( e * A - f * y + h * w) * ivd,
+            (-a * A + b * y - d * w) * ivd,
+            ( m * u - n * s + p * q) * ivd,
+            (-i * u + j * s - l * q) * ivd,
+            (-e * z + f * x - g * w) * ivd,
+            ( a * z - b * x + c * w) * ivd,
+            (-m * t + n * r - o * q) * ivd,
+            ( i * t - j * r + k * q) * ivd
+        ]);
+        return dest;
+    }
+};
+
+const vec3 = {
+    setArrType(arrType) { return Vec3Type = arrType; },
+    create(x = 0, y = 0, z = 0, dest = new Vec3Type(3)) {
+        dest[0] = x;
+        dest[1] = y;
+        dest[2] = z;
+        return dest;
+    },
+    length(src) {
+        return Math.sqrt(src[0] ** 2 + src[1] ** 2 + src[2] ** 2);
+    },
+    get len() { return vec3.length; },
+    add(v1, v2, dest = new Vec3Type(3)) {
+        dest.set([v1[0] + v2[0], v1[1] + v2[1], v1[2] + v2[2]]);
+        return dest;
+    },
+    subtract(v1, v2, dest = new Vec3Type(3)) {
+        dest.set([v1[0] - v2[0], v1[1] - v2[1], v1[2] - v2[2]]);
+        return dest;
+    },
+    get sub() { return vec3.subtract; },
+    multiply(v1, v2, dest = new Vec3Type(3)) {
+        dest.set([v1[0] * v2[0], v1[1] * v2[1], v1[2] * v2[2]]);
+        return dest;
+    },
+    get mul() { return vec3.multiply; },
+    cross(v1, v2, dest = new Vec3Type(3)) {
+        dest.set([
+            v1[1] * v2[2] - v1[2] * v2[1],
+            v1[2] * v2[0] - v1[0] * v2[2],
+            v1[0] * v2[1] - v1[1] * v2[0]
+        ]);
+        return dest;
+    },
+    dot(v1, v2) {
+        return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2];
+    },
+    scale(v, s, dest = new Vec3Type(3)) {
+        dest.set([v[0] * s, v[1] * s, v[2] * s]);
+        return dest;
+    },
+    scaleAndAdd(src, v, s, dest = new Vec3Type(3)) {
+        dest.set([src[0] + v[0] * s, src[1] + v[1] * s, src[2] + v[2] * s]);
+        return dest;
+    },
+    divide(v1, v2, dest = new Vec3Type(3)) {
+        dest.set([v1[0] / v2[0], v1[1] / v2[1], v1[2] / v2[2]]);
+        return dest;
+    },
+    get div() { return vec3.divide; },
+    normalize(v, dest = new Vec3Type(3)) {
+        // return vec3.scale(v, 1 / vec3.length(v), dest);
+        let len = Math.sqrt(v[0] ** 2 + v[1] ** 2 + v[2] ** 2);
+        dest.set([v[0] / len, v[1] / len, v[2] / len]);
+        return dest;
+    },
+    negate(v, dest = new Vec3Type(3)) {
+        dest.set([-v[0], -v[1], -v[2]]);
+        return dest;
+    },
+    rotateX(v, a, dest = new Vec3Type(3)) {
+        let s = Math.sin(a), c = Math.cos(a);
+        dest.set([
+            v[0],
+            v[1] * c - v[2] * s,
+            v[1] * s + v[2] * c
+        ]);
+        return dest;
+    },
+    rotateY(v, a, dest = new Vec3Type(3)) {
+        let s = Math.sin(a), c = Math.cos(a);
+        dest.set([
+            v[2] * s + v[0] * c,
+            v[1],
+            v[2] * c - v[0] * s
+        ]);
+        return dest;
+    },
+    rotateZ(v, a, dest = new Vec3Type(3)) {
+        let s = Math.sin(a), c = Math.cos(a);
+        dest.set([
+            v[0] * c - v[1] * s,
+            v[0] * s + v[1] * c,
+            v[2]
+        ]);
+        return dest;
+    },
+    inverse(v, dest = new Vec3Type(3)) {
+        dest.set([1 / v[0], 1 / v[1], 1 / v[2]]);
+        return dest;
+    },
+    exactEquals(v1, v2) {
+        return v1[0] === v2[0] && v1[1] === v2[1] && v1[2] === v2[2];
+    },
+    equals(v1, v2) {
+        let [x, y, z] = v1, [u, v, w] = v2,
+            {abs, max} = Math;
+        return (abs(x - u) <= EPSILON * max(1.0, abs(x), abs(u)) &&
+                abs(y - v) <= EPSILON * max(1.0, abs(y), abs(v)) &&
+                abs(z - w) <= EPSILON * max(1.0, abs(z), abs(w)));
+    },
+    transformMat4(v, m, dest = new Vec3Type(3)) {
+        let [x, y, z] = v,
+            w = (m[3] * x + m[7] * y + m[11] * z + m[15]) || 1.0;
+        dest.set([
+            (m[0] * x + m[4] * y + m[8] * z + m[12]) / w,
+            (m[1] * x + m[5] * y + m[9] * z + m[13]) / w,
+            (m[2] * x + m[6] * y + m[10]* z + m[14]) / w
+        ]);
+        return dest;
+    },
+    move_toward(v, to, delta, dest = new Vec3Type(3)) {
+        let vd = vec3.subtract(to, v), len = vec3.length(vd);
+        if (len <= delta || len <= EPSILON)
+            dest.set(to);
+        else
+            // dest = v + (vd / len * delta);
+            vec3.scaleAndAdd(v, vd, delta / len, dest);
+        return dest;
+    },
+};
+
+let Vec2Type = Float32Array;
+const vec2 = {
+    setArrType(arrType) { return Vec2Type = arrType; },
+    create(x = 0, y = 0, dest = new Vec2Type(2)) {
+        dest[0] = x;
+        dest[1] = y;
+        return dest;
+    },
+    length(src) {
+        return Math.sqrt(src[0] ** 2 + src[1] ** 2);
+    },
+    get len() { return vec2.length; },
+    add(v1, v2, dest = new Vec2Type(2)) {
+        dest.set([v1[0] + v2[0], v1[1] + v2[1]]);
+        return dest;
+    },
+    subtract(v1, v2, dest = new Vec2Type(2)) {
+        dest.set([v1[0] - v2[0], v1[1] - v2[1]]);
+        return dest;
+    },
+    get sub() { return vec2.subtract; },
+    multiply(v1, v2, dest = new Vec2Type(2)) {
+        dest.set([v1[0] * v2[0], v1[1] * v2[1]]);
+        return dest;
+    },
+    get mul() { return vec2.multiply; },
+    cross(v1, v2) {
+        return v1[0] * v2[1] - v1[1] * v2[0];
+    },
+    dot(v1, v2) {
+        return v1[0] * v2[0] + v1[1] * v2[1];
+    },
+    scale(v, s, dest = new Vec2Type(2)) {
+        dest.set([v[0] * s, v[1] * s]);
+        return dest;
+    },
+    scaleAndAdd(src, v, s, dest = new Vec2Type(2)) {
+        dest.set([src[0] + v[0] * s, src[1] + v[1] * s]);
+        return dest;
+    },
+    divide(v1, v2, dest = new Vec2Type(2)) {
+        dest.set([v1[0] / v2[0], v1[1] / v2[1]]);
+        return dest;
+    },
+    get div() { return vec2.divide; },
+    normalize(v, dest = new Vec2Type(2)) {
+        // return vec2.scale(v, 1 / vec2.length(v), dest);
+        let len = Math.sqrt(v[0] ** 2 + v[1] ** 2);
+        dest.set([v[0] / len, v[1] / len]);
+        return dest;
+    },
+    negate(v, dest = new Vec2Type(2)) {
+        dest.set([-v[0], -v[1]]);
+        return dest;
+    },
+    rotateOrigin(v, a, dest = new Vec2Type(2)) {
+        let s = Math.sin(a), c = Math.cos(a);
+        dest.set([
+            v[0] * c - v[1] * s,
+            v[0] * s + v[1] * c
+        ]);
+        return dest;
+    },
+    roatePoint(v1, v2, a, dest = new Vec2Type(2)) {
+        let s = Math.sin(a), c = Math.cos(a),
+            p0 = v1[0] - v2[0], p1 = v1[1] - v2[1];
+        dest.set([
+            p0 * c - p1 * s + v2[0],
+            p0 * s + p1 * c + v2[1]
+        ]);
+        return dest;
+    },
+    // angle of a relative to b
+    angle(a, b) {
+        let [x1, y1] = a, [x2, y2] = b, { atan2 } = Math;
+        return atan2(y2, x2) - atan2(y1, x1);
+    },
+    inverse(v, dest = new Vec2Type(2)) {
+        dest.set([1 / v[0], 1 / v[1]]);
+        return dest;
+    },
+    exactEquals(v1, v2) {
+        return v1[0] === v2[0] && v1[1] === v2[1];
+    },
+    equals(v1, v2) {
+        let [x, y] = v1, [u, v] = v2,
+            {abs, max} = Math;
+        return (abs(x - u) <= EPSILON * max(1.0, abs(x), abs(u)) &&
+                abs(y - v) <= EPSILON * max(1.0, abs(y), abs(v)));
+    },
+    move_toward(v, to, delta, dest = new Vec2Type(2)) {
+        let vd = vec2.subtract(to, v), len = vec2.length(vd);
+        if (len <= delta || len <= EPSILON)
+            dest.set(to);
+        else
+            // dest = v + (vd / len * delta);
+            vec2.scaleAndAdd(v, vd, delta / len, dest);
+        return dest;
+    },
+};
+
+const PI = Math.PI;
+const d2r = deg => deg * PI / 180;
+const r2d = rad => rad * 180 / PI;
+const squaredEuclideanDis = (point1, point2) => point1.reduce((sum, x, i) => sum + (x - point2[i]) ** 2, 0);
+const euclideanDis = (point1, point2) => Math.sqrt(squaredEuclideanDis(point1, point2));
+const manhattanDis = (point1, point2) => point1.reduce((sum, x, i) => sum + Math.abs(x - point2[i]), 0);
+const chebyshevDis = (point1, point2) => point1.reduce((max, x, i) => Math.max(max, Math.abs(x - point2[i])), 0);
+const minkowskiDis = (point1, point2, p) => (point1.reduce((sum, x, i) => sum + Math.abs(x - point2[i]) ** p, 0)) ** (1 / p);
+
+export {
+    mat4, vec3, vec2, EPSILON,
+    d2r as degree2radian,
+    r2d as radian2degree,
+    squaredEuclideanDis,
+    euclideanDis,
+    manhattanDis,
+    chebyshevDis,
+    minkowskiDis,
+};

+ 34 - 0
legacy/WebMC/src/utils/isWebGL2Context.js

@@ -0,0 +1,34 @@
+
+// source: https://get.webgl.org/webgl2/
+function isWebGL2Context(ctx = null) {
+    // check if it really supports WebGL2. Issues, Some browers claim to support WebGL2
+    // but in reality pass less than 20% of the conformance tests. Add a few simple
+    // tests to fail so as not to mislead users.
+    if (ctx) for (let param of [
+        { pname: "MAX_TEXTURE_SIZE", min: 0, },
+        { pname: "MAX_3D_TEXTURE_SIZE", min: 256, },
+        // Since the texture atlas is 32*16, there are 512 textures.
+        // TODO: Use multiple texture arrays instead of a single texture array
+        // { pname: "MAX_ARRAY_TEXTURE_LAYERS", min: 256, },
+        { pname: "MAX_ARRAY_TEXTURE_LAYERS", min: 512, },
+        { pname: "MAX_DRAW_BUFFERS", min: 4, },
+        { pname: "MAX_COLOR_ATTACHMENTS", min: 4, },
+        { pname: "MAX_VERTEX_UNIFORM_BLOCKS", min: 12, },
+        { pname: "MAX_VERTEX_TEXTURE_IMAGE_UNITS", min: 16, },
+        { pname: "MAX_FRAGMENT_INPUT_COMPONENTS", min: 60, },
+        { pname: "MAX_UNIFORM_BUFFER_BINDINGS", min: 24, },
+        { pname: "MAX_COMBINED_UNIFORM_BLOCKS", min: 24, },
+    ]) {
+        let value = ctx.getParameter(ctx[param.pname]);
+        if (typeof value !== "number" || Number.isNaN(value) || value < param.min) {
+            ctx = null;
+            break;
+        }
+    }
+    return !!ctx;
+};
+
+export {
+    isWebGL2Context,
+    isWebGL2Context as default,
+};

+ 84 - 0
legacy/WebMC/src/utils/loadResources.js

@@ -0,0 +1,84 @@
+
+const ajaxByUrlAndType = (url, type) => new Promise((s, f) => {
+    const ajax = new XMLHttpRequest();
+    ajax.onreadystatechange = function() {
+        if (this.readyState !== 4) return;
+        if (this.status === 200) s(this.response);
+        else f(new Error(this.statusText));
+    };
+    ajax.responseType = type;
+    ajax.open("GET", url);
+    ajax.send();
+});
+const getJSON = url => ajaxByUrlAndType(url, "json");
+const getText = url => ajaxByUrlAndType(url, "text");
+const getImg = url => new Promise((s, f) => {
+    const img = new Image();
+    img.onload = function() { this.uri = url; s(this); };
+    img.onerror = f;
+    img.src = url;
+});
+const getFilename = url => {
+    let [filename, filenameNoExt, fileExtension] = url.match(/([^\\\/<>|:"*?]+)\.(\w+$)/);
+    return [filename || "", filenameNoExt || "", fileExtension || ""];
+};
+
+const RESOURCES = {};
+
+import { edm } from "./EventDispatcher.js";
+
+const RESLoadEventDispatcher = edm.getOrNewEventDispatcher("mc.load");
+
+const awaitResCallbacks = {};
+const newAwaitRes = url => {
+    let promise = new Promise((resolve, reject) => {
+        if (url in RESOURCES) resolve(RESOURCES[url]);
+        else if (url in awaitResCallbacks)
+            awaitResCallbacks[url].push({resolve, reject});
+        else awaitResCallbacks[url] = [{resolve, reject}];
+    });
+    RESLoadEventDispatcher.dispatchEvent("newAwaitRes", url, promise);
+    return promise;
+};
+const notifyAllAwaitRes = (url, val, {
+    status = "resolve",
+    type = ""
+} = {}) => {
+    if (status === "resolve") RESOURCES[url] = val;
+    if (url in awaitResCallbacks) {
+        awaitResCallbacks[url].forEach(o => o[status](val, status, url, type));
+        if (status === "resolve") delete awaitResCallbacks[url];
+    }
+};
+const setResource = (key, val) => notifyAllAwaitRes(key, val);
+const waitResource = key => newAwaitRes(key);
+
+const asyncLoadResByUrl = (url, {
+    type = {
+        png: "img",
+        json: "json",
+    }[getFilename(url)[2].toLowerCase()] || "text",
+} = {}) => {
+    // console.log(url);
+    if (url in RESOURCES) return Promise.resolve(RESOURCES[url]);
+    let handles = [
+        res => notifyAllAwaitRes(url, res, { type }),
+        err => notifyAllAwaitRes(url, err, { type, status: "reject" })
+    ];
+    if (url in awaitResCallbacks)
+        return newAwaitRes(url);
+    else if (type === "img" || type === "png")
+        getImg(url).then(...handles);
+    else if (type === "json")
+        getJSON(url).then(...handles);
+    else if (type === "text")
+        getText(url).then(...handles);
+    return newAwaitRes(url);
+};
+
+export {
+    asyncLoadResByUrl as default,
+    asyncLoadResByUrl,
+    RESOURCES,
+    setResource, waitResource,
+};

二进制
legacy/WebMC/texture/background.png


二进制
legacy/WebMC/texture/gui.png


二进制
legacy/WebMC/texture/icons.png


二进制
legacy/WebMC/texture/jumpingBlock.gif


二进制
legacy/WebMC/texture/mc-font.ttf


二进制
legacy/WebMC/texture/panorama.png


二进制
legacy/WebMC/texture/spritesheet.png


二进制
legacy/WebMC/texture/terrain-atlas.png


二进制
legacy/WebMC/texture/title.png


+ 5 - 0
main.go

@@ -106,6 +106,11 @@ func main() {
 	*/
 	RunStartup()
 
+	/*
+		Development build test execution
+	*/
+	Run_Test()
+
 	//Initiate all the static files transfer
 	fs := http.FileServer(http.Dir("./web"))
 	if *enable_gzip {

+ 66 - 0
mod/agi/agi.image.go

@@ -18,6 +18,7 @@ import (
 	"github.com/oliamb/cutter"
 	"github.com/robertkrimen/otto"
 
+	"imuslab.com/arozos/mod/neuralnet"
 	user "imuslab.com/arozos/mod/user"
 )
 
@@ -256,6 +257,70 @@ func (g *Gateway) injectImageLibFunctions(vm *otto.Otto, u *user.User) {
 		}
 	})
 
+	vm.Set("_imagelib_classify", func(call otto.FunctionCall) otto.Value {
+		vsrc, err := call.Argument(0).ToString()
+		if err != nil {
+			g.raiseError(err)
+			return otto.FalseValue()
+		}
+
+		classifier, err := call.Argument(1).ToString()
+		if err != nil {
+			classifier = "default"
+		}
+
+		if classifier == "" || classifier == "undefined" {
+			classifier = "default"
+		}
+
+		//Convert the vsrc to real path
+		rsrc, err := virtualPathToRealPath(vsrc, u)
+		if err != nil {
+			g.raiseError(err)
+			return otto.FalseValue()
+		}
+
+		if classifier == "default" || classifier == "darknet19" {
+			//Use darknet19 for classification
+			r, err := neuralnet.AnalysisPhotoDarknet19(rsrc)
+			if err != nil {
+				g.raiseError(err)
+				return otto.FalseValue()
+			}
+
+			result, err := vm.ToValue(r)
+			if err != nil {
+				g.raiseError(err)
+				return otto.FalseValue()
+			}
+
+			return result
+
+		} else if classifier == "yolo3" {
+			//Use yolo3 for classification, return positions of object as well
+			r, err := neuralnet.AnalysisPhotoYOLO3(rsrc)
+			if err != nil {
+				g.raiseError(err)
+				return otto.FalseValue()
+			}
+
+			result, err := vm.ToValue(r)
+			if err != nil {
+				g.raiseError(err)
+				return otto.FalseValue()
+			}
+
+			return result
+
+		} else {
+			//Unsupported classifier
+			log.Println("[AGI] Unsupported image classifier name: " + classifier)
+			g.raiseError(err)
+			return otto.FalseValue()
+		}
+
+	})
+
 	//Wrap all the native code function into an imagelib class
 	vm.Run(`
 		var imagelib = {};
@@ -263,5 +328,6 @@ func (g *Gateway) injectImageLibFunctions(vm *otto.Otto, u *user.User) {
 		imagelib.resizeImage = _imagelib_resizeImage;
 		imagelib.cropImage = _imagelib_cropImage;
 		imagelib.loadThumbString = _imagelib_loadThumbString;
+		imagelib.classify = _imagelib_classify;
 	`)
 }

+ 146 - 0
mod/neuralnet/neuralnet.go

@@ -0,0 +1,146 @@
+package neuralnet
+
+import (
+	"errors"
+	"os/exec"
+	"path/filepath"
+	"runtime"
+	"strconv"
+	"strings"
+
+	"imuslab.com/arozos/mod/filesystem"
+)
+
+/*
+	Neural Net Package
+
+	Require darknet binary in system folder to work
+*/
+
+type ImageClass struct {
+	Name       string
+	Percentage float64
+	Positions  []int
+}
+
+func getDarknetBinary() (string, error) {
+	darknetRoot := "./system/neuralnet/"
+	binaryName := "darknet_" + runtime.GOOS + "_" + runtime.GOARCH
+	if runtime.GOOS == "windows" {
+		binaryName += ".exe"
+	}
+	expectedDarknetBinary := filepath.Join(darknetRoot, binaryName)
+
+	absPath, _ := filepath.Abs(expectedDarknetBinary)
+	if !filesystem.FileExists(absPath) {
+		return "", errors.New("Darknet executable not found on " + absPath)
+	}
+
+	return absPath, nil
+}
+
+//Analysis and get what is inside the image using Darknet19, fast but only support 1 main object
+func AnalysisPhotoDarknet19(filename string) ([]*ImageClass, error) {
+	results := []*ImageClass{}
+
+	//Check darknet installed
+	darknetBinary, err := getDarknetBinary()
+	if err != nil {
+		return results, err
+	}
+
+	//Check source image exists
+	imageSourceAbs, err := filepath.Abs(filename)
+	if !filesystem.FileExists(imageSourceAbs) || err != nil {
+		return results, errors.New("Source file not found")
+	}
+
+	//Analysis the image
+	cmd := exec.Command(darknetBinary, "classifier", "predict", "cfg/imagenet1k.data", "cfg/darknet19.cfg", "darknet19.weights", imageSourceAbs)
+	cmd.Dir = filepath.Dir(darknetBinary)
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		return results, err
+	}
+
+	//Process the output text
+	lines := strings.Split(string(out), "\n")
+	for _, line := range lines {
+		if strings.Contains(line, "%:") {
+			//This is a resulting line. Split and push it into results
+			info := strings.Split(strings.TrimSpace(line), "%: ") //[0] => Percentage in string, [1] => tag
+			if s, err := strconv.ParseFloat(info[0], 32); err == nil {
+				thisClassification := ImageClass{
+					Name:       info[1],
+					Percentage: s,
+					Positions:  []int{},
+				}
+
+				results = append(results, &thisClassification)
+			}
+		}
+	}
+
+	return results, nil
+}
+
+//Analysis what is in the image using YOLO3, very slow but support multiple objects
+func AnalysisPhotoYOLO3(filename string) ([]*ImageClass, error) {
+	results := []*ImageClass{}
+
+	//Check darknet installed
+	darknetBinary, err := getDarknetBinary()
+	if err != nil {
+		return results, err
+	}
+
+	//Check source image exists
+	imageSourceAbs, err := filepath.Abs(filename)
+	if !filesystem.FileExists(imageSourceAbs) || err != nil {
+		return results, errors.New("Source file not found")
+	}
+
+	//Analysis the image
+	cmd := exec.Command(darknetBinary, "detect", "cfg/yolov3.cfg", "yolov3.weights", imageSourceAbs, "-out")
+	cmd.Dir = filepath.Dir(darknetBinary)
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		return results, err
+	}
+
+	lines := strings.Split(string(out), "\n")
+	var previousClassificationObject *ImageClass = nil
+	for _, line := range lines {
+		line = strings.TrimSpace(line)
+		if len(line) > 0 && line[len(line)-1:] == "%" && strings.Contains(line, ":") {
+			//This is a output value
+			//Trim out the %
+			line = line[:len(line)-1]
+			info := strings.Split(line, ":") //[0] => class name, [1] => percentage
+			if s, err := strconv.ParseFloat(strings.TrimSpace(info[1]), 32); err == nil {
+				thisClassification := ImageClass{
+					Name:       info[0],
+					Percentage: s,
+				}
+				previousClassificationObject = &thisClassification
+				results = append(results, &thisClassification)
+			}
+		} else if len(line) > 0 && line[:4] == "pos=" && strings.Contains(line, ",") && previousClassificationObject != nil {
+			//This is position makeup data, append to previous classification
+			positionsString := strings.Split(line[4:], ",")
+			positionsInt := []int{}
+			for _, pos := range positionsString {
+				posInt, err := strconv.Atoi(pos)
+				if err != nil {
+					positionsInt = append(positionsInt, -1)
+				} else {
+					positionsInt = append(positionsInt, posInt)
+				}
+			}
+
+			previousClassificationObject.Positions = positionsInt
+		}
+	}
+
+	return results, nil
+}

+ 114 - 0
out.txt

@@ -0,0 +1,114 @@
+layer     filters    size              input                output
+    0 conv     32  3 x 3 / 1   608 x 608 x   3   ->   608 x 608 x  32  0.639 BFLOPs
+    1 conv     64  3 x 3 / 2   608 x 608 x  32   ->   304 x 304 x  64  3.407 BFLOPs
+    2 conv     32  1 x 1 / 1   304 x 304 x  64   ->   304 x 304 x  32  0.379 BFLOPs
+    3 conv     64  3 x 3 / 1   304 x 304 x  32   ->   304 x 304 x  64  3.407 BFLOPs
+    4 res    1                 304 x 304 x  64   ->   304 x 304 x  64
+    5 conv    128  3 x 3 / 2   304 x 304 x  64   ->   152 x 152 x 128  3.407 BFLOPs
+    6 conv     64  1 x 1 / 1   152 x 152 x 128   ->   152 x 152 x  64  0.379 BFLOPs
+    7 conv    128  3 x 3 / 1   152 x 152 x  64   ->   152 x 152 x 128  3.407 BFLOPs
+    8 res    5                 152 x 152 x 128   ->   152 x 152 x 128
+    9 conv     64  1 x 1 / 1   152 x 152 x 128   ->   152 x 152 x  64  0.379 BFLOPs
+   10 conv    128  3 x 3 / 1   152 x 152 x  64   ->   152 x 152 x 128  3.407 BFLOPs
+   11 res    8                 152 x 152 x 128   ->   152 x 152 x 128
+   12 conv    256  3 x 3 / 2   152 x 152 x 128   ->    76 x  76 x 256  3.407 BFLOPs
+   13 conv    128  1 x 1 / 1    76 x  76 x 256   ->    76 x  76 x 128  0.379 BFLOPs
+   14 conv    256  3 x 3 / 1    76 x  76 x 128   ->    76 x  76 x 256  3.407 BFLOPs
+   15 res   12                  76 x  76 x 256   ->    76 x  76 x 256
+   16 conv    128  1 x 1 / 1    76 x  76 x 256   ->    76 x  76 x 128  0.379 BFLOPs
+   17 conv    256  3 x 3 / 1    76 x  76 x 128   ->    76 x  76 x 256  3.407 BFLOPs
+   18 res   15                  76 x  76 x 256   ->    76 x  76 x 256
+   19 conv    128  1 x 1 / 1    76 x  76 x 256   ->    76 x  76 x 128  0.379 BFLOPs
+   20 conv    256  3 x 3 / 1    76 x  76 x 128   ->    76 x  76 x 256  3.407 BFLOPs
+   21 res   18                  76 x  76 x 256   ->    76 x  76 x 256
+   22 conv    128  1 x 1 / 1    76 x  76 x 256   ->    76 x  76 x 128  0.379 BFLOPs
+   23 conv    256  3 x 3 / 1    76 x  76 x 128   ->    76 x  76 x 256  3.407 BFLOPs
+   24 res   21                  76 x  76 x 256   ->    76 x  76 x 256
+   25 conv    128  1 x 1 / 1    76 x  76 x 256   ->    76 x  76 x 128  0.379 BFLOPs
+   26 conv    256  3 x 3 / 1    76 x  76 x 128   ->    76 x  76 x 256  3.407 BFLOPs
+   27 res   24                  76 x  76 x 256   ->    76 x  76 x 256
+   28 conv    128  1 x 1 / 1    76 x  76 x 256   ->    76 x  76 x 128  0.379 BFLOPs
+   29 conv    256  3 x 3 / 1    76 x  76 x 128   ->    76 x  76 x 256  3.407 BFLOPs
+   30 res   27                  76 x  76 x 256   ->    76 x  76 x 256
+   31 conv    128  1 x 1 / 1    76 x  76 x 256   ->    76 x  76 x 128  0.379 BFLOPs
+   32 conv    256  3 x 3 / 1    76 x  76 x 128   ->    76 x  76 x 256  3.407 BFLOPs
+   33 res   30                  76 x  76 x 256   ->    76 x  76 x 256
+   34 conv    128  1 x 1 / 1    76 x  76 x 256   ->    76 x  76 x 128  0.379 BFLOPs
+   35 conv    256  3 x 3 / 1    76 x  76 x 128   ->    76 x  76 x 256  3.407 BFLOPs
+   36 res   33                  76 x  76 x 256   ->    76 x  76 x 256
+   37 conv    512  3 x 3 / 2    76 x  76 x 256   ->    38 x  38 x 512  3.407 BFLOPs
+   38 conv    256  1 x 1 / 1    38 x  38 x 512   ->    38 x  38 x 256  0.379 BFLOPs
+   39 conv    512  3 x 3 / 1    38 x  38 x 256   ->    38 x  38 x 512  3.407 BFLOPs
+   40 res   37                  38 x  38 x 512   ->    38 x  38 x 512
+   41 conv    256  1 x 1 / 1    38 x  38 x 512   ->    38 x  38 x 256  0.379 BFLOPs
+   42 conv    512  3 x 3 / 1    38 x  38 x 256   ->    38 x  38 x 512  3.407 BFLOPs
+   43 res   40                  38 x  38 x 512   ->    38 x  38 x 512
+   44 conv    256  1 x 1 / 1    38 x  38 x 512   ->    38 x  38 x 256  0.379 BFLOPs
+   45 conv    512  3 x 3 / 1    38 x  38 x 256   ->    38 x  38 x 512  3.407 BFLOPs
+   46 res   43                  38 x  38 x 512   ->    38 x  38 x 512
+   47 conv    256  1 x 1 / 1    38 x  38 x 512   ->    38 x  38 x 256  0.379 BFLOPs
+   48 conv    512  3 x 3 / 1    38 x  38 x 256   ->    38 x  38 x 512  3.407 BFLOPs
+   49 res   46                  38 x  38 x 512   ->    38 x  38 x 512
+   50 conv    256  1 x 1 / 1    38 x  38 x 512   ->    38 x  38 x 256  0.379 BFLOPs
+   51 conv    512  3 x 3 / 1    38 x  38 x 256   ->    38 x  38 x 512  3.407 BFLOPs
+   52 res   49                  38 x  38 x 512   ->    38 x  38 x 512
+   53 conv    256  1 x 1 / 1    38 x  38 x 512   ->    38 x  38 x 256  0.379 BFLOPs
+   54 conv    512  3 x 3 / 1    38 x  38 x 256   ->    38 x  38 x 512  3.407 BFLOPs
+   55 res   52                  38 x  38 x 512   ->    38 x  38 x 512
+   56 conv    256  1 x 1 / 1    38 x  38 x 512   ->    38 x  38 x 256  0.379 BFLOPs
+   57 conv    512  3 x 3 / 1    38 x  38 x 256   ->    38 x  38 x 512  3.407 BFLOPs
+   58 res   55                  38 x  38 x 512   ->    38 x  38 x 512
+   59 conv    256  1 x 1 / 1    38 x  38 x 512   ->    38 x  38 x 256  0.379 BFLOPs
+   60 conv    512  3 x 3 / 1    38 x  38 x 256   ->    38 x  38 x 512  3.407 BFLOPs
+   61 res   58                  38 x  38 x 512   ->    38 x  38 x 512
+   62 conv   1024  3 x 3 / 2    38 x  38 x 512   ->    19 x  19 x1024  3.407 BFLOPs
+   63 conv    512  1 x 1 / 1    19 x  19 x1024   ->    19 x  19 x 512  0.379 BFLOPs
+   64 conv   1024  3 x 3 / 1    19 x  19 x 512   ->    19 x  19 x1024  3.407 BFLOPs
+   65 res   62                  19 x  19 x1024   ->    19 x  19 x1024
+   66 conv    512  1 x 1 / 1    19 x  19 x1024   ->    19 x  19 x 512  0.379 BFLOPs
+   67 conv   1024  3 x 3 / 1    19 x  19 x 512   ->    19 x  19 x1024  3.407 BFLOPs
+   68 res   65                  19 x  19 x1024   ->    19 x  19 x1024
+   69 conv    512  1 x 1 / 1    19 x  19 x1024   ->    19 x  19 x 512  0.379 BFLOPs
+   70 conv   1024  3 x 3 / 1    19 x  19 x 512   ->    19 x  19 x1024  3.407 BFLOPs
+   71 res   68                  19 x  19 x1024   ->    19 x  19 x1024
+   72 conv    512  1 x 1 / 1    19 x  19 x1024   ->    19 x  19 x 512  0.379 BFLOPs
+   73 conv   1024  3 x 3 / 1    19 x  19 x 512   ->    19 x  19 x1024  3.407 BFLOPs
+   74 res   71                  19 x  19 x1024   ->    19 x  19 x1024
+   75 conv    512  1 x 1 / 1    19 x  19 x1024   ->    19 x  19 x 512  0.379 BFLOPs
+   76 conv   1024  3 x 3 / 1    19 x  19 x 512   ->    19 x  19 x1024  3.407 BFLOPs
+   77 conv    512  1 x 1 / 1    19 x  19 x1024   ->    19 x  19 x 512  0.379 BFLOPs
+   78 conv   1024  3 x 3 / 1    19 x  19 x 512   ->    19 x  19 x1024  3.407 BFLOPs
+   79 conv    512  1 x 1 / 1    19 x  19 x1024   ->    19 x  19 x 512  0.379 BFLOPs
+   80 conv   1024  3 x 3 / 1    19 x  19 x 512   ->    19 x  19 x1024  3.407 BFLOPs
+   81 conv    255  1 x 1 / 1    19 x  19 x1024   ->    19 x  19 x 255  0.189 BFLOPs
+   82 yolo
+   83 route  79
+   84 conv    256  1 x 1 / 1    19 x  19 x 512   ->    19 x  19 x 256  0.095 BFLOPs
+   85 upsample            2x    19 x  19 x 256   ->    38 x  38 x 256
+   86 route  85 61
+   87 conv    256  1 x 1 / 1    38 x  38 x 768   ->    38 x  38 x 256  0.568 BFLOPs
+   88 conv    512  3 x 3 / 1    38 x  38 x 256   ->    38 x  38 x 512  3.407 BFLOPs
+   89 conv    256  1 x 1 / 1    38 x  38 x 512   ->    38 x  38 x 256  0.379 BFLOPs
+   90 conv    512  3 x 3 / 1    38 x  38 x 256   ->    38 x  38 x 512  3.407 BFLOPs
+   91 conv    256  1 x 1 / 1    38 x  38 x 512   ->    38 x  38 x 256  0.379 BFLOPs
+   92 conv    512  3 x 3 / 1    38 x  38 x 256   ->    38 x  38 x 512  3.407 BFLOPs
+   93 conv    255  1 x 1 / 1    38 x  38 x 512   ->    38 x  38 x 255  0.377 BFLOPs
+   94 yolo
+   95 route  91
+   96 conv    128  1 x 1 / 1    38 x  38 x 256   ->    38 x  38 x 128  0.095 BFLOPs
+   97 upsample            2x    38 x  38 x 128   ->    76 x  76 x 128
+   98 route  97 36
+   99 conv    128  1 x 1 / 1    76 x  76 x 384   ->    76 x  76 x 128  0.568 BFLOPs
+  100 conv    256  3 x 3 / 1    76 x  76 x 128   ->    76 x  76 x 256  3.407 BFLOPs
+  101 conv    128  1 x 1 / 1    76 x  76 x 256   ->    76 x  76 x 128  0.379 BFLOPs
+  102 conv    256  3 x 3 / 1    76 x  76 x 128   ->    76 x  76 x 256  3.407 BFLOPs
+  103 conv    128  1 x 1 / 1    76 x  76 x 256   ->    76 x  76 x 128  0.379 BFLOPs
+  104 conv    256  3 x 3 / 1    76 x  76 x 128   ->    76 x  76 x 256  3.407 BFLOPs
+  105 conv    255  1 x 1 / 1    76 x  76 x 256   ->    76 x  76 x 2C:\Users\Toby\Desktop\golang\arozos\a.jpg: Predicted in 11.034518 seconds.
+keyboard: 94%
+chair: 100%
+mouse: 55%
+tvmonitor: 94%
+55  0.754 BFLOPs
+  106 yolo
+Loading weights from yolov3.weights...Done!

+ 8 - 0
system/neuralnet/cfg/coco.data

@@ -0,0 +1,8 @@
+classes= 80
+train  = /home/pjreddie/data/coco/trainvalno5k.txt
+valid  = coco_testdev
+#valid = data/coco_val_5k.list
+names = data/coco.names
+backup = /home/pjreddie/backup/
+eval=coco
+

+ 789 - 0
system/neuralnet/cfg/yolov3.cfg

@@ -0,0 +1,789 @@
+[net]
+# Testing
+# batch=1
+# subdivisions=1
+# Training
+batch=64
+subdivisions=16
+width=608
+height=608
+channels=3
+momentum=0.9
+decay=0.0005
+angle=0
+saturation = 1.5
+exposure = 1.5
+hue=.1
+
+learning_rate=0.001
+burn_in=1000
+max_batches = 500200
+policy=steps
+steps=400000,450000
+scales=.1,.1
+
+[convolutional]
+batch_normalize=1
+filters=32
+size=3
+stride=1
+pad=1
+activation=leaky
+
+# Downsample
+
+[convolutional]
+batch_normalize=1
+filters=64
+size=3
+stride=2
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=32
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=64
+size=3
+stride=1
+pad=1
+activation=leaky
+
+[shortcut]
+from=-3
+activation=linear
+
+# Downsample
+
+[convolutional]
+batch_normalize=1
+filters=128
+size=3
+stride=2
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=64
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=128
+size=3
+stride=1
+pad=1
+activation=leaky
+
+[shortcut]
+from=-3
+activation=linear
+
+[convolutional]
+batch_normalize=1
+filters=64
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=128
+size=3
+stride=1
+pad=1
+activation=leaky
+
+[shortcut]
+from=-3
+activation=linear
+
+# Downsample
+
+[convolutional]
+batch_normalize=1
+filters=256
+size=3
+stride=2
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=128
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=256
+size=3
+stride=1
+pad=1
+activation=leaky
+
+[shortcut]
+from=-3
+activation=linear
+
+[convolutional]
+batch_normalize=1
+filters=128
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=256
+size=3
+stride=1
+pad=1
+activation=leaky
+
+[shortcut]
+from=-3
+activation=linear
+
+[convolutional]
+batch_normalize=1
+filters=128
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=256
+size=3
+stride=1
+pad=1
+activation=leaky
+
+[shortcut]
+from=-3
+activation=linear
+
+[convolutional]
+batch_normalize=1
+filters=128
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=256
+size=3
+stride=1
+pad=1
+activation=leaky
+
+[shortcut]
+from=-3
+activation=linear
+
+
+[convolutional]
+batch_normalize=1
+filters=128
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=256
+size=3
+stride=1
+pad=1
+activation=leaky
+
+[shortcut]
+from=-3
+activation=linear
+
+[convolutional]
+batch_normalize=1
+filters=128
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=256
+size=3
+stride=1
+pad=1
+activation=leaky
+
+[shortcut]
+from=-3
+activation=linear
+
+[convolutional]
+batch_normalize=1
+filters=128
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=256
+size=3
+stride=1
+pad=1
+activation=leaky
+
+[shortcut]
+from=-3
+activation=linear
+
+[convolutional]
+batch_normalize=1
+filters=128
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=256
+size=3
+stride=1
+pad=1
+activation=leaky
+
+[shortcut]
+from=-3
+activation=linear
+
+# Downsample
+
+[convolutional]
+batch_normalize=1
+filters=512
+size=3
+stride=2
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=256
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=512
+size=3
+stride=1
+pad=1
+activation=leaky
+
+[shortcut]
+from=-3
+activation=linear
+
+
+[convolutional]
+batch_normalize=1
+filters=256
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=512
+size=3
+stride=1
+pad=1
+activation=leaky
+
+[shortcut]
+from=-3
+activation=linear
+
+
+[convolutional]
+batch_normalize=1
+filters=256
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=512
+size=3
+stride=1
+pad=1
+activation=leaky
+
+[shortcut]
+from=-3
+activation=linear
+
+
+[convolutional]
+batch_normalize=1
+filters=256
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=512
+size=3
+stride=1
+pad=1
+activation=leaky
+
+[shortcut]
+from=-3
+activation=linear
+
+[convolutional]
+batch_normalize=1
+filters=256
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=512
+size=3
+stride=1
+pad=1
+activation=leaky
+
+[shortcut]
+from=-3
+activation=linear
+
+
+[convolutional]
+batch_normalize=1
+filters=256
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=512
+size=3
+stride=1
+pad=1
+activation=leaky
+
+[shortcut]
+from=-3
+activation=linear
+
+
+[convolutional]
+batch_normalize=1
+filters=256
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=512
+size=3
+stride=1
+pad=1
+activation=leaky
+
+[shortcut]
+from=-3
+activation=linear
+
+[convolutional]
+batch_normalize=1
+filters=256
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=512
+size=3
+stride=1
+pad=1
+activation=leaky
+
+[shortcut]
+from=-3
+activation=linear
+
+# Downsample
+
+[convolutional]
+batch_normalize=1
+filters=1024
+size=3
+stride=2
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=512
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=1024
+size=3
+stride=1
+pad=1
+activation=leaky
+
+[shortcut]
+from=-3
+activation=linear
+
+[convolutional]
+batch_normalize=1
+filters=512
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=1024
+size=3
+stride=1
+pad=1
+activation=leaky
+
+[shortcut]
+from=-3
+activation=linear
+
+[convolutional]
+batch_normalize=1
+filters=512
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=1024
+size=3
+stride=1
+pad=1
+activation=leaky
+
+[shortcut]
+from=-3
+activation=linear
+
+[convolutional]
+batch_normalize=1
+filters=512
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=1024
+size=3
+stride=1
+pad=1
+activation=leaky
+
+[shortcut]
+from=-3
+activation=linear
+
+######################
+
+[convolutional]
+batch_normalize=1
+filters=512
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+size=3
+stride=1
+pad=1
+filters=1024
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=512
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+size=3
+stride=1
+pad=1
+filters=1024
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=512
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+size=3
+stride=1
+pad=1
+filters=1024
+activation=leaky
+
+[convolutional]
+size=1
+stride=1
+pad=1
+filters=255
+activation=linear
+
+
+[yolo]
+mask = 6,7,8
+anchors = 10,13,  16,30,  33,23,  30,61,  62,45,  59,119,  116,90,  156,198,  373,326
+classes=80
+num=9
+jitter=.3
+ignore_thresh = .7
+truth_thresh = 1
+random=1
+
+
+[route]
+layers = -4
+
+[convolutional]
+batch_normalize=1
+filters=256
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[upsample]
+stride=2
+
+[route]
+layers = -1, 61
+
+
+
+[convolutional]
+batch_normalize=1
+filters=256
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+size=3
+stride=1
+pad=1
+filters=512
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=256
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+size=3
+stride=1
+pad=1
+filters=512
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=256
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+size=3
+stride=1
+pad=1
+filters=512
+activation=leaky
+
+[convolutional]
+size=1
+stride=1
+pad=1
+filters=255
+activation=linear
+
+
+[yolo]
+mask = 3,4,5
+anchors = 10,13,  16,30,  33,23,  30,61,  62,45,  59,119,  116,90,  156,198,  373,326
+classes=80
+num=9
+jitter=.3
+ignore_thresh = .7
+truth_thresh = 1
+random=1
+
+
+
+[route]
+layers = -4
+
+[convolutional]
+batch_normalize=1
+filters=128
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[upsample]
+stride=2
+
+[route]
+layers = -1, 36
+
+
+
+[convolutional]
+batch_normalize=1
+filters=128
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+size=3
+stride=1
+pad=1
+filters=256
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=128
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+size=3
+stride=1
+pad=1
+filters=256
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+filters=128
+size=1
+stride=1
+pad=1
+activation=leaky
+
+[convolutional]
+batch_normalize=1
+size=3
+stride=1
+pad=1
+filters=256
+activation=leaky
+
+[convolutional]
+size=1
+stride=1
+pad=1
+filters=255
+activation=linear
+
+
+[yolo]
+mask = 0,1,2
+anchors = 10,13,  16,30,  33,23,  30,61,  62,45,  59,119,  116,90,  156,198,  373,326
+classes=80
+num=9
+jitter=.3
+ignore_thresh = .7
+truth_thresh = 1
+random=1
+

二进制
system/neuralnet/darknet_darwin_amd64


二进制
system/neuralnet/darknet_linux_amd64


二进制
system/neuralnet/darknet.exe → system/neuralnet/darknet_windows_amd64.exe


+ 80 - 0
system/neuralnet/data/coco.names

@@ -0,0 +1,80 @@
+person
+bicycle
+car
+motorbike
+aeroplane
+bus
+train
+truck
+boat
+traffic light
+fire hydrant
+stop sign
+parking meter
+bench
+bird
+cat
+dog
+horse
+sheep
+cow
+elephant
+bear
+zebra
+giraffe
+backpack
+umbrella
+handbag
+tie
+suitcase
+frisbee
+skis
+snowboard
+sports ball
+kite
+baseball bat
+baseball glove
+skateboard
+surfboard
+tennis racket
+bottle
+wine glass
+cup
+fork
+knife
+spoon
+bowl
+banana
+apple
+sandwich
+orange
+broccoli
+carrot
+hot dog
+pizza
+donut
+cake
+chair
+sofa
+pottedplant
+bed
+diningtable
+toilet
+tvmonitor
+laptop
+mouse
+remote
+keyboard
+cell phone
+microwave
+oven
+toaster
+sink
+refrigerator
+book
+clock
+vase
+scissors
+teddy bear
+hair drier
+toothbrush

二进制
system/neuralnet/data/labels/100_0.png


二进制
system/neuralnet/data/labels/100_1.png


二进制
system/neuralnet/data/labels/100_2.png


二进制
system/neuralnet/data/labels/100_3.png


二进制
system/neuralnet/data/labels/100_4.png


二进制
system/neuralnet/data/labels/100_5.png


二进制
system/neuralnet/data/labels/100_6.png


二进制
system/neuralnet/data/labels/100_7.png


二进制
system/neuralnet/data/labels/101_0.png


二进制
system/neuralnet/data/labels/101_1.png


二进制
system/neuralnet/data/labels/101_2.png


二进制
system/neuralnet/data/labels/101_3.png


二进制
system/neuralnet/data/labels/101_4.png


二进制
system/neuralnet/data/labels/101_5.png


二进制
system/neuralnet/data/labels/101_6.png


二进制
system/neuralnet/data/labels/101_7.png


二进制
system/neuralnet/data/labels/102_0.png


二进制
system/neuralnet/data/labels/102_1.png


部分文件因为文件数量过多而无法显示