├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── py ├── baked.png ├── biome.json ├── biomes.cpp ├── biomes_gen.py ├── cache_gen.py ├── colormap.png ├── foliage.png ├── grass.png ├── heightmap.png ├── index.json ├── pickup.py ├── specific.zip ├── tool_merge.py └── weightmap.png ├── src ├── application.rs ├── color │ ├── biome.rs │ ├── blockstate.rs │ ├── calculate.rs │ ├── de.rs │ └── mod.rs ├── main.rs ├── render │ ├── control.rs │ ├── data.rs │ ├── key.rs │ ├── mod.rs │ └── tile.rs ├── service.rs └── tilegen │ ├── mod.rs │ ├── pathgen.rs │ ├── qtree.rs │ └── tile.rs └── web ├── index.html └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | # VSCode 13 | /.vscode/ 14 | 15 | # Specific Log 16 | /py/*.log -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "voxelmap_cache_renderer" 3 | description = "offline voxelmap-cache renderer with some tools" 4 | version = "0.1.9" 5 | authors = ["RDCarrot "] 6 | edition = "2018" 7 | 8 | 9 | [features] 10 | 11 | default = [] 12 | 13 | service = ["bytes", "actix-multipart", "actix-rt", "actix-files", "actix-web", "rustls", "futures", "num_cpus"] 14 | 15 | 16 | [dependencies] 17 | zip = { version = "^0.5", features = ["deflate-miniz"] } 18 | serde = { version = "^1.0", features = ["derive"] } 19 | serde_json = "^1.0" 20 | image = "^0.23" 21 | log = "^0.4" 22 | env_logger = "^0.7" 23 | clap = "^2.33" 24 | bytes = { optional = true, version = "^0.5" } 25 | actix-multipart = { optional = true, version = "^0.2" } 26 | actix-rt = { optional = true, version = "^1.1" } 27 | actix-files = { optional = true, version = "^0.2" } 28 | actix-web = { optional = true, version = "^2.0", features = ["rustls"] } 29 | rustls = { optional = true, version = "^0.16" } 30 | futures = { optional = true, version = "^0.3" } 31 | num_cpus = { optional = true, version = "^1.13" } 32 | 33 | 34 | [profile.release] 35 | opt-level = 3 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # voxelmap-cache-render 2 | Offline renderer for Minecraft voxelmap cache, with python (generate colormap) and Rust (fast render) 3 | 4 | [中文使用方法简介](https://bbs.craft.moe/d/1921-voxelmapmod) 5 | 6 | ---- 7 | 8 | ## Archived 9 | 10 | Since Minecraft 1.18, the changes of world height makes voxelmap-cache format and voxelmap itself unstable. 11 | 12 | Decide not to maintain. 13 | 14 | ---- 15 | 16 | ## rust renderer 17 | ### file struct 18 | ```bash 19 | ./ 20 | voxelmapcache.exe 21 | resource/ 22 | biome.json 23 | foliage.png 24 | grass.png 25 | index.json 26 | colormap.png 27 | weightmap.png 28 | ``` 29 | these files can be found in `py/` 30 | ### usage 31 | 1. generate picture from `.minecraft[/versions/]/.mods/mamiyaotaru/voxelmap/cache///overworld/` 32 | 33 | ```bash 34 | USAGE: 35 | voxelmapcache.exe render --input_dir --output_dir [OPTIONS] 36 | 37 | -i, --input_dir input folder 38 | -o, --output_dir output folder 39 | OPTIONS: 40 | --env_lit environment light, from 0 to 15, default is 15 41 | --gamma gamma for gamma correction, default is 1.0 42 | -t, --thread use multi-thread and set thread number, default is 1 43 | ``` 44 | 45 | 2. generate map tiles with pictures from `step 1` 46 | ```bash 47 | USAGE 48 | voxelmapcache.exe tile --input_dir --output_dir --path_mode [OPTIONS] 49 | 50 | -i, --input_dir input folder 51 | -o, --output_dir output folder 52 | --path_mode generated path mode, can be "layer+", "layer+:", "layer+:,", "layer-", "layer-:", "layer-:," 53 | example: layer mode, the original scale is marked as 5 and the max-level scale is marked as 0 54 | => "layer-:5,0" or "layer-:5" 55 | example: layer mode, the original scale is marked as 0, automatically scan all files 56 | => "layer+:0" or "layer+" 57 | OPTIONS: 58 | --filter filter used in scale, can be "nearest", "triangle", "gaussian", "catmullrom", "lanczos3"; default is "nearest" 59 | --use_multi_thread whether to use multi-thread; if set, use fixed 4 threads 60 | --check_exist check if the same picture exist and then skip rewrite it 61 | ``` 62 | 63 | ## python colormap generator 64 | 65 | 1. biomes_gen 66 | 67 | ensure `biomes.cpp` in the same folder and run `biomes_gen`. 68 | output `biomes.json` 69 | 70 | 2. cache_gen 71 | 72 | ``` 73 | USAGE: cache_gen.py [-l LOG] [-w LINEWIDTH] assets [assets ...] 74 | -l LOG log file name; default is 'STDOUT' 75 | -w LINEWIDTH number of baked-model each row; default is 32 76 | assets resource packs, can be multiple. 77 | notice that: 1. .jar is one kind of resource pack; 2. specific.zip are required as the last resource packs to fix some model. 78 | 79 | EXAMPLE: python3 cache_gen.py -l run.log Minecraft\.minecraft\versions\1.15.2\1.15.2.jar specific.zip 80 | ``` 81 | ​ output `index.json`,`baked.png` ,`heightmap.png`,`colormap.png`, `weightmap.png`, `grass.png`,`foliage.png` 82 | 83 | 3. pick up `index.json`,`colormap.png`, `weightmap.png`, `grass.png`,`foliage.png`, `biome.json` into `resource` folder. 84 | -------------------------------------------------------------------------------- /py/baked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWCarrot/voxelmap-cache-render/ff4361fba50f2c25ebd36b8cc28561f9439212b3/py/baked.png -------------------------------------------------------------------------------- /py/biome.json: -------------------------------------------------------------------------------- 1 | [{"id": 0, "name": "Ocean", "temperature": 0.5, "rainfall": 0.5, "watercolor": 4159204}, {"id": 1, "name": "Plains", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 2, "name": "Desert", "temperature": 2.0, "rainfall": 0.0, "watercolor": 4159204}, {"id": 3, "name": "Mountains", "temperature": 0.2, "rainfall": 0.3, "watercolor": 4159204}, {"id": 4, "name": "Forest", "temperature": 0.7, "rainfall": 0.8, "watercolor": 4159204}, {"id": 5, "name": "Taiga", "temperature": 0.25, "rainfall": 0.8, "watercolor": 4159204}, {"id": 6, "name": "Swamp", "temperature": 0.8, "rainfall": 0.9, "watercolor": 6388580, "ops_grass": {"Fixed": 5011004}, "ops_foliage": {"Fixed": 5011004}}, {"id": 7, "name": "River", "temperature": 0.5, "rainfall": 0.5, "watercolor": 4159204}, {"id": 8, "name": "Nether", "temperature": 2.0, "rainfall": 0.0, "watercolor": 4159204}, {"id": 9, "name": "The End", "temperature": 0.5, "rainfall": 0.5, "watercolor": 4159204}, {"id": 10, "name": "Frozen Ocean", "temperature": 0.0, "rainfall": 0.5, "watercolor": 3750089}, {"id": 11, "name": "Frozen River", "temperature": 0.0, "rainfall": 0.5, "watercolor": 3750089}, {"id": 12, "name": "Snowy Tundra", "temperature": 0.0, "rainfall": 0.5, "watercolor": 4159204}, {"id": 13, "name": "Snowy Mountains", "temperature": 0.0, "rainfall": 0.5, "watercolor": 4159204}, {"id": 14, "name": "Mushroom Fields", "temperature": 0.9, "rainfall": 1.0, "watercolor": 4159204}, {"id": 15, "name": "Mushroom Field Shore", "temperature": 0.9, "rainfall": 1.0, "watercolor": 4159204}, {"id": 16, "name": "Beach", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 17, "name": "Desert Hills", "temperature": 2.0, "rainfall": 0.0, "watercolor": 4159204}, {"id": 18, "name": "Wooded Hills", "temperature": 0.7, "rainfall": 0.8, "watercolor": 4159204}, {"id": 19, "name": "Taiga Hills", "temperature": 0.25, "rainfall": 0.8, "watercolor": 4159204}, {"id": 20, "name": "Mountain Edge", "temperature": 0.2, "rainfall": 0.3, "watercolor": 4159204}, {"id": 21, "name": "Jungle", "temperature": 0.95, "rainfall": 0.9, "watercolor": 4159204}, {"id": 22, "name": "Jungle Hills", "temperature": 0.95, "rainfall": 0.9, "watercolor": 4159204}, {"id": 23, "name": "Jungle Edge", "temperature": 0.95, "rainfall": 0.8, "watercolor": 4159204}, {"id": 24, "name": "Deep Ocean", "temperature": 0.5, "rainfall": 0.5, "watercolor": 4159204}, {"id": 25, "name": "Stone Shore", "temperature": 0.2, "rainfall": 0.3, "watercolor": 4159204}, {"id": 26, "name": "Snowy Beach", "temperature": 0.05, "rainfall": 0.3, "watercolor": 4159204}, {"id": 27, "name": "Birch Forest", "temperature": 0.6, "rainfall": 0.6, "watercolor": 4159204}, {"id": 28, "name": "Birch Forest Hills", "temperature": 0.6, "rainfall": 0.6, "watercolor": 4159204}, {"id": 29, "name": "Dark Forest", "temperature": 0.7, "rainfall": 0.8, "watercolor": 4159204, "ops_grass": {"Average": 2634762}, "ops_foliage": {"Average": 2634762}}, {"id": 30, "name": "Snowy Taiga", "temperature": -0.5, "rainfall": 0.4, "watercolor": 4159204}, {"id": 31, "name": "Snowy Taiga Hills", "temperature": -0.5, "rainfall": 0.4, "watercolor": 4159204}, {"id": 32, "name": "Giant Tree Taiga", "temperature": 0.3, "rainfall": 0.8, "watercolor": 4159204}, {"id": 33, "name": "Giant Tree Taiga Hills", "temperature": 0.3, "rainfall": 0.8, "watercolor": 4159204}, {"id": 34, "name": "Wooded Mountains", "temperature": 0.2, "rainfall": 0.3, "watercolor": 4159204}, {"id": 35, "name": "Savanna", "temperature": 1.2, "rainfall": 0.0, "watercolor": 4159204}, {"id": 36, "name": "Savanna Plateau", "temperature": 1.0, "rainfall": 0.0, "watercolor": 4159204}, {"id": 37, "name": "Badlands", "temperature": 2.0, "rainfall": 0.0, "watercolor": 4159204, "ops_grass": {"Fixed": 9470285}, "ops_foliage": {"Fixed": 10387789}}, {"id": 38, "name": "Wooded Badlands Plateau", "temperature": 2.0, "rainfall": 0.0, "watercolor": 4159204, "ops_grass": {"Fixed": 9470285}, "ops_foliage": {"Fixed": 10387789}}, {"id": 39, "name": "Badlands Plateau", "temperature": 2.0, "rainfall": 0.0, "watercolor": 4159204, "ops_grass": {"Fixed": 9470285}, "ops_foliage": {"Fixed": 10387789}}, {"id": 40, "name": "Small End Islands", "temperature": 0.5, "rainfall": 0.5, "watercolor": 4159204}, {"id": 41, "name": "End Midlands", "temperature": 0.5, "rainfall": 0.5, "watercolor": 4159204}, {"id": 42, "name": "End Highlands", "temperature": 0.5, "rainfall": 0.5, "watercolor": 4159204}, {"id": 43, "name": "End Barrens", "temperature": 0.5, "rainfall": 0.5, "watercolor": 4159204}, {"id": 44, "name": "Warm Ocean", "temperature": 0.8, "rainfall": 0.5, "watercolor": 4445678}, {"id": 45, "name": "Lukewarm Ocean", "temperature": 0.8, "rainfall": 0.5, "watercolor": 4566514}, {"id": 46, "name": "Cold Ocean", "temperature": 0.8, "rainfall": 0.5, "watercolor": 4020182}, {"id": 47, "name": "Deep Warm Ocean", "temperature": 0.8, "rainfall": 0.5, "watercolor": 4159204}, {"id": 48, "name": "Deep Lukewarm Ocean", "temperature": 0.8, "rainfall": 0.5, "watercolor": 4159204}, {"id": 49, "name": "Deep Cold Ocean", "temperature": 0.8, "rainfall": 0.5, "watercolor": 4159204}, {"id": 50, "name": "Deep Frozen Ocean", "temperature": 0.8, "rainfall": 0.5, "watercolor": 4159204}, {"id": 51, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 52, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 53, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 54, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 55, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 56, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 57, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 58, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 59, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 60, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 61, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 62, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 63, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 64, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 65, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 66, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 67, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 68, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 69, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 70, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 71, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 72, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 73, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 74, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 75, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 76, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 77, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 78, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 79, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 80, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 81, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 82, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 83, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 84, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 85, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 86, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 87, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 88, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 89, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 90, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 91, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 92, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 93, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 94, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 95, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 96, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 97, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 98, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 99, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 100, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 101, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 102, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 103, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 104, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 105, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 106, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 107, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 108, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 109, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 110, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 111, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 112, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 113, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 114, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 115, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 116, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 117, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 118, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 119, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 120, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 121, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 122, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 123, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 124, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 125, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 126, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 127, "name": "The Void", "temperature": 0.5, "rainfall": 0.5, "watercolor": 4159204}, {"id": 128, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 129, "name": "Sunflower Plains", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 130, "name": "Desert Lakes", "temperature": 2.0, "rainfall": 0.0, "watercolor": 4159204}, {"id": 131, "name": "Gravelly Mountains", "temperature": 0.2, "rainfall": 0.3, "watercolor": 4159204}, {"id": 132, "name": "Flower Forest", "temperature": 0.7, "rainfall": 0.8, "watercolor": 4159204}, {"id": 133, "name": "Taiga Mountains", "temperature": 0.25, "rainfall": 0.8, "watercolor": 4159204}, {"id": 134, "name": "Swamp Hills", "temperature": 0.8, "rainfall": 0.9, "watercolor": 4159204}, {"id": 135, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 136, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 137, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 138, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 139, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 140, "name": "Ice Spikes", "temperature": 0.0, "rainfall": 0.5, "watercolor": 4159204}, {"id": 141, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 142, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 143, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 144, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 145, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 146, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 147, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 148, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 149, "name": "Modified Jungle", "temperature": 0.95, "rainfall": 0.9, "watercolor": 4159204}, {"id": 150, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 151, "name": "Modified Jungle Edge", "temperature": 0.95, "rainfall": 0.8, "watercolor": 4159204}, {"id": 152, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 153, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 154, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 155, "name": "Tall Birch Forest", "temperature": 0.6, "rainfall": 0.6, "watercolor": 4159204}, {"id": 156, "name": "Tall Birch Hills", "temperature": 0.6, "rainfall": 0.6, "watercolor": 4159204}, {"id": 157, "name": "Dark Forest Hills", "temperature": 0.7, "rainfall": 0.8, "watercolor": 4159204}, {"id": 158, "name": "Snowy Taiga Mountains", "temperature": -0.5, "rainfall": 0.4, "watercolor": 4159204}, {"id": 159, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 160, "name": "Giant Spruce Taiga", "temperature": 0.25, "rainfall": 0.8, "watercolor": 4159204}, {"id": 161, "name": "Giant Spruce Taiga Hills", "temperature": 0.25, "rainfall": 0.8, "watercolor": 4159204}, {"id": 162, "name": "Gravelly Mountains+", "temperature": 0.2, "rainfall": 0.3, "watercolor": 4159204}, {"id": 163, "name": "Shattered Savanna", "temperature": 1.1, "rainfall": 0.0, "watercolor": 4159204}, {"id": 164, "name": "Shattered Savanna Plateau", "temperature": 1.0, "rainfall": 0.0, "watercolor": 4159204}, {"id": 165, "name": "Eroded Badlands", "temperature": 2.0, "rainfall": 0.0, "watercolor": 4159204, "ops_grass": {"Fixed": 9470285}, "ops_foliage": {"Fixed": 10387789}}, {"id": 166, "name": "Modified Wooded Badlands Plateau", "temperature": 2.0, "rainfall": 0.0, "watercolor": 4159204, "ops_grass": {"Fixed": 9470285}, "ops_foliage": {"Fixed": 10387789}}, {"id": 167, "name": "Modified Badlands Plateau", "temperature": 2.0, "rainfall": 0.0, "watercolor": 4159204, "ops_grass": {"Fixed": 9470285}, "ops_foliage": {"Fixed": 10387789}}, {"id": 168, "name": "Bamboo Jungle", "temperature": 0.95, "rainfall": 0.9, "watercolor": 4159204}, {"id": 169, "name": "Bamboo Jungle Hills", "temperature": 0.95, "rainfall": 0.9, "watercolor": 4159204}, {"id": 170, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 171, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 172, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 173, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 174, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 175, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 176, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 177, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 178, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 179, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 180, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 181, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 182, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 183, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 184, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 185, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 186, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 187, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 188, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 189, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 190, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 191, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 192, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 193, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 194, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 195, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 196, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 197, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 198, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 199, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 200, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 201, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 202, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 203, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 204, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 205, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 206, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 207, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 208, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 209, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 210, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 211, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 212, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 213, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 214, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 215, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 216, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 217, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 218, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 219, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 220, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 221, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 222, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 223, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 224, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 225, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 226, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 227, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 228, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 229, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 230, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 231, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 232, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 233, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 234, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 235, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 236, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 237, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 238, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 239, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 240, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 241, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 242, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 243, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 244, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 245, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 246, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 247, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 248, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 249, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 250, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 251, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 252, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 253, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 254, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}, {"id": 255, "name": "Unknown Biome", "temperature": 0.8, "rainfall": 0.4, "watercolor": 4159204}] -------------------------------------------------------------------------------- /py/biomes.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2014, Eric Haines 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 18 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 19 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 20 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 21 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 22 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 23 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 24 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 25 | THE POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | 28 | #include "stdafx.h" 29 | 30 | #include "biomes.h" 31 | 32 | Biome gBiomes[256]={ // IMPORTANT: do not change 256 size here. 33 | // ID Name Temperature, rainfall, grass, foliage colors 34 | // - note: the colors here are just placeholders, they are computed in the program 35 | { /* 0 */ "Ocean", 0.5f, 0.5f, 0x92BD59, 0x77AB2F }, // default values of temp and rain 36 | { /* 1 */ "Plains", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 37 | { /* 2 */ "Desert", 2.0f, 0.0f, 0x92BD59, 0x77AB2F }, 38 | { /* 3 */ "Mountains", 0.2f, 0.3f, 0x92BD59, 0x77AB2F }, 39 | { /* 4 */ "Forest", 0.7f, 0.8f, 0x92BD59, 0x77AB2F }, 40 | { /* 5 */ "Taiga", 0.25f, 0.8f, 0x92BD59, 0x77AB2F }, 41 | { /* 6 */ "Swamp", 0.8f, 0.9f, 0x92BD59, 0x77AB2F }, 42 | { /* 7 */ "River", 0.5f, 0.5f, 0x92BD59, 0x77AB2F }, // default values of temp and rain 43 | { /* 8 */ "Nether", 2.0f, 0.0f, 0x92BD59, 0x77AB2F }, 44 | { /* 9 */ "The End", 0.5f, 0.5f, 0x92BD59, 0x77AB2F }, // default values of temp and rain 45 | { /* 10 */ "Frozen Ocean", 0.0f, 0.5f, 0x92BD59, 0x77AB2F }, 46 | { /* 11 */ "Frozen River", 0.0f, 0.5f, 0x92BD59, 0x77AB2F }, 47 | { /* 12 */ "Snowy Tundra", 0.0f, 0.5f, 0x92BD59, 0x77AB2F }, 48 | { /* 13 */ "Snowy Mountains", 0.0f, 0.5f, 0x92BD59, 0x77AB2F }, 49 | { /* 14 */ "Mushroom Fields", 0.9f, 1.0f, 0x92BD59, 0x77AB2F }, 50 | { /* 15 */ "Mushroom Field Shore", 0.9f, 1.0f, 0x92BD59, 0x77AB2F }, 51 | { /* 16 */ "Beach", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 52 | { /* 17 */ "Desert Hills", 2.0f, 0.0f, 0x92BD59, 0x77AB2F }, 53 | { /* 18 */ "Wooded Hills", 0.7f, 0.8f, 0x92BD59, 0x77AB2F }, 54 | { /* 19 */ "Taiga Hills", 0.25f, 0.8f, 0x92BD59, 0x77AB2F }, 55 | { /* 20 */ "Mountain Edge", 0.2f, 0.3f, 0x92BD59, 0x77AB2F }, 56 | { /* 21 */ "Jungle", 0.95f, 0.9f, 0x92BD59, 0x77AB2F }, 57 | { /* 22 */ "Jungle Hills", 0.95f, 0.9f, 0x92BD59, 0x77AB2F }, 58 | { /* 23 */ "Jungle Edge", 0.95f, 0.8f, 0x92BD59, 0x77AB2F }, 59 | { /* 24 */ "Deep Ocean", 0.5f, 0.5f, 0x92BD59, 0x77AB2F }, 60 | { /* 25 */ "Stone Shore", 0.2f, 0.3f, 0x92BD59, 0x77AB2F }, 61 | { /* 26 */ "Snowy Beach", 0.05f, 0.3f, 0x92BD59, 0x77AB2F }, 62 | { /* 27 */ "Birch Forest", 0.6f, 0.6f, 0x92BD59, 0x77AB2F }, 63 | { /* 28 */ "Birch Forest Hills", 0.6f, 0.6f, 0x92BD59, 0x77AB2F }, 64 | { /* 29 */ "Dark Forest", 0.7f, 0.8f, 0x92BD59, 0x77AB2F }, 65 | { /* 30 */ "Snowy Taiga", -0.5f, 0.4f, 0x92BD59, 0x77AB2F }, 66 | { /* 31 */ "Snowy Taiga Hills", -0.5f, 0.4f, 0x92BD59, 0x77AB2F }, 67 | { /* 32 */ "Giant Tree Taiga", 0.3f, 0.8f, 0x92BD59, 0x77AB2F }, 68 | { /* 33 */ "Giant Tree Taiga Hills", 0.3f, 0.8f, 0x92BD59, 0x77AB2F }, 69 | { /* 34 */ "Wooded Mountains", 0.2f, 0.3f, 0x92BD59, 0x77AB2F }, 70 | { /* 35 */ "Savanna", 1.2f, 0.0f, 0x92BD59, 0x77AB2F }, 71 | { /* 36 */ "Savanna Plateau", 1.0f, 0.0f, 0x92BD59, 0x77AB2F }, 72 | { /* 37 */ "Badlands", 2.0f, 0.0f, 0x92BD59, 0x77AB2F }, 73 | { /* 38 */ "Wooded Badlands Plateau", 2.0f, 0.0f, 0x92BD59, 0x77AB2F }, 74 | { /* 39 */ "Badlands Plateau", 2.0f, 0.0f, 0x92BD59, 0x77AB2F }, 75 | { /* 40 */ "Small End Islands", 0.5f, 0.5f, 0x92BD59, 0x77AB2F }, 76 | { /* 41 */ "End Midlands", 0.5f, 0.5f, 0x92BD59, 0x77AB2F }, 77 | { /* 42 */ "End Highlands", 0.5f, 0.5f, 0x92BD59, 0x77AB2F }, 78 | { /* 43 */ "End Barrens", 0.5f, 0.5f, 0x92BD59, 0x77AB2F }, 79 | { /* 44 */ "Warm Ocean", 0.8f, 0.5f, 0x92BD59, 0x77AB2F }, 80 | { /* 45 */ "Lukewarm Ocean", 0.8f, 0.5f, 0x92BD59, 0x77AB2F }, 81 | { /* 46 */ "Cold Ocean", 0.8f, 0.5f, 0x92BD59, 0x77AB2F }, 82 | { /* 47 */ "Deep Warm Ocean", 0.8f, 0.5f, 0x92BD59, 0x77AB2F }, 83 | { /* 48 */ "Deep Lukewarm Ocean", 0.8f, 0.5f, 0x92BD59, 0x77AB2F }, 84 | { /* 49 */ "Deep Cold Ocean", 0.8f, 0.5f, 0x92BD59, 0x77AB2F }, 85 | { /* 50 */ "Deep Frozen Ocean", 0.8f, 0.5f, 0x92BD59, 0x77AB2F }, 86 | { /* 51 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 87 | { /* 52 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 88 | { /* 53 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 89 | { /* 54 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 90 | { /* 55 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 91 | { /* 56 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 92 | { /* 57 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 93 | { /* 58 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 94 | { /* 59 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 95 | { /* 60 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 96 | { /* 61 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 97 | { /* 62 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 98 | { /* 63 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 99 | { /* 64 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 100 | { /* 65 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 101 | { /* 66 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 102 | { /* 67 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 103 | { /* 68 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 104 | { /* 69 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 105 | { /* 70 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 106 | { /* 71 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 107 | { /* 72 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 108 | { /* 73 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 109 | { /* 74 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 110 | { /* 75 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 111 | { /* 76 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 112 | { /* 77 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 113 | { /* 78 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 114 | { /* 79 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 115 | { /* 80 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 116 | { /* 81 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 117 | { /* 82 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 118 | { /* 83 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 119 | { /* 84 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 120 | { /* 85 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 121 | { /* 86 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 122 | { /* 87 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 123 | { /* 88 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 124 | { /* 89 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 125 | { /* 90 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 126 | { /* 91 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 127 | { /* 92 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 128 | { /* 93 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 129 | { /* 94 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 130 | { /* 95 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 131 | { /* 96 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 132 | { /* 97 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 133 | { /* 98 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 134 | { /* 99 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 135 | { /* 100 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 136 | { /* 101 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 137 | { /* 102 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 138 | { /* 103 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 139 | { /* 104 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 140 | { /* 105 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 141 | { /* 106 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 142 | { /* 107 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 143 | { /* 108 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 144 | { /* 109 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 145 | { /* 110 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 146 | { /* 111 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 147 | { /* 112 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 148 | { /* 113 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 149 | { /* 114 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 150 | { /* 115 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 151 | { /* 116 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 152 | { /* 117 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 153 | { /* 118 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 154 | { /* 119 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 155 | { /* 120 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 156 | { /* 121 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 157 | { /* 122 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 158 | { /* 123 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 159 | { /* 124 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 160 | { /* 125 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 161 | { /* 126 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 162 | { /* 127 */ "The Void", 0.5f, 0.5f, 0x92BD59, 0x77AB2F }, // default values of temp and rain; also, no height differences 163 | { /* 128 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 164 | { /* 129 */ "Sunflower Plains", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 165 | { /* 130 */ "Desert Lakes", 2.0f, 0.0f, 0x92BD59, 0x77AB2F }, 166 | { /* 131 */ "Gravelly Mountains", 0.2f, 0.3f, 0x92BD59, 0x77AB2F }, 167 | { /* 132 */ "Flower Forest", 0.7f, 0.8f, 0x92BD59, 0x77AB2F }, 168 | { /* 133 */ "Taiga Mountains", 0.25f, 0.8f, 0x92BD59, 0x77AB2F }, 169 | { /* 134 */ "Swamp Hills", 0.8f, 0.9f, 0x92BD59, 0x77AB2F }, 170 | { /* 135 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 171 | { /* 136 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 172 | { /* 137 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 173 | { /* 138 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 174 | { /* 139 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 175 | { /* 140 */ "Ice Spikes", 0.0f, 0.5f, 0x92BD59, 0x77AB2F }, 176 | { /* 141 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 177 | { /* 142 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 178 | { /* 143 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 179 | { /* 144 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 180 | { /* 145 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 181 | { /* 146 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 182 | { /* 147 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 183 | { /* 148 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 184 | { /* 149 */ "Modified Jungle", 0.95f, 0.9f, 0x92BD59, 0x77AB2F }, 185 | { /* 150 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 186 | { /* 151 */ "Modified Jungle Edge", 0.95f, 0.8f, 0x92BD59, 0x77AB2F }, 187 | { /* 152 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 188 | { /* 153 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 189 | { /* 154 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 190 | { /* 155 */ "Tall Birch Forest", 0.6f, 0.6f, 0x92BD59, 0x77AB2F }, 191 | { /* 156 */ "Tall Birch Hills", 0.6f, 0.6f, 0x92BD59, 0x77AB2F }, 192 | { /* 157 */ "Dark Forest Hills", 0.7f, 0.8f, 0x92BD59, 0x77AB2F }, 193 | { /* 158 */ "Snowy Taiga Mountains", -0.5f, 0.4f, 0x92BD59, 0x77AB2F }, 194 | { /* 159 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 195 | { /* 160 */ "Giant Spruce Taiga", 0.25f, 0.8f, 0x92BD59, 0x77AB2F }, 196 | { /* 161 */ "Giant Spruce Taiga Hills", 0.25f, 0.8f, 0x92BD59, 0x77AB2F }, 197 | { /* 162 */ "Gravelly Mountains+", 0.2f, 0.3f, 0x92BD59, 0x77AB2F }, 198 | { /* 163 */ "Shattered Savanna", 1.1f, 0.0f, 0x92BD59, 0x77AB2F }, 199 | { /* 164 */ "Shattered Savanna Plateau", 1.0f, 0.0f, 0x92BD59, 0x77AB2F }, 200 | { /* 165 */ "Eroded Badlands", 2.0f, 0.0f, 0x92BD59, 0x77AB2F }, 201 | { /* 166 */ "Modified Wooded Badlands Plateau", 2.0f, 0.0f, 0x92BD59, 0x77AB2F }, 202 | { /* 167 */ "Modified Badlands Plateau", 2.0f, 0.0f, 0x92BD59, 0x77AB2F }, 203 | { /* 168 */ "Bamboo Jungle", 0.95f, 0.9f, 0x92BD59, 0x77AB2F }, 204 | { /* 169 */ "Bamboo Jungle Hills", 0.95f, 0.9f, 0x92BD59, 0x77AB2F }, 205 | { /* 170 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 206 | { /* 171 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 207 | { /* 172 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 208 | { /* 173 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 209 | { /* 174 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 210 | { /* 175 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 211 | { /* 176 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 212 | { /* 177 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 213 | { /* 178 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 214 | { /* 179 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 215 | { /* 180 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 216 | { /* 181 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 217 | { /* 182 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 218 | { /* 183 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 219 | { /* 184 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 220 | { /* 185 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 221 | { /* 186 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 222 | { /* 187 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 223 | { /* 188 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 224 | { /* 189 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 225 | { /* 190 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 226 | { /* 191 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 227 | { /* 192 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 228 | { /* 193 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 229 | { /* 194 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 230 | { /* 195 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 231 | { /* 196 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 232 | { /* 197 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 233 | { /* 198 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 234 | { /* 199 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 235 | { /* 200 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 236 | { /* 201 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 237 | { /* 202 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 238 | { /* 203 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 239 | { /* 204 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 240 | { /* 205 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 241 | { /* 206 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 242 | { /* 207 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 243 | { /* 208 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 244 | { /* 209 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 245 | { /* 210 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 246 | { /* 211 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 247 | { /* 212 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 248 | { /* 213 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 249 | { /* 214 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 250 | { /* 215 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 251 | { /* 216 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 252 | { /* 217 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 253 | { /* 218 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 254 | { /* 219 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 255 | { /* 220 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 256 | { /* 221 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 257 | { /* 222 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 258 | { /* 223 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 259 | { /* 224 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 260 | { /* 225 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 261 | { /* 226 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 262 | { /* 227 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 263 | { /* 228 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 264 | { /* 229 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 265 | { /* 230 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 266 | { /* 231 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 267 | { /* 232 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 268 | { /* 233 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 269 | { /* 234 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 270 | { /* 235 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 271 | { /* 236 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 272 | { /* 237 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 273 | { /* 238 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 274 | { /* 239 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 275 | { /* 240 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 276 | { /* 241 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 277 | { /* 242 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 278 | { /* 243 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 279 | { /* 244 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 280 | { /* 245 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 281 | { /* 246 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 282 | { /* 247 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 283 | { /* 248 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 284 | { /* 249 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 285 | { /* 250 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 286 | { /* 251 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 287 | { /* 252 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 288 | { /* 253 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 289 | { /* 254 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 290 | { /* 255 */ "Unknown Biome", 0.8f, 0.4f, 0x92BD59, 0x77AB2F }, 291 | }; 292 | 293 | typedef struct BiomeCorner 294 | { 295 | int red; 296 | int green; 297 | int blue; 298 | } BiomeCorner; 299 | 300 | static BiomeCorner grassCorners[3] = 301 | { 302 | { 191, 183, 85 }, // lower left, temperature starts at 1.0 on left 303 | { 128, 180, 151 }, // lower right 304 | { 71, 205, 51 } // upper left 305 | }; 306 | 307 | static BiomeCorner foliageCorners[3] = 308 | { 309 | { 174, 164, 42 }, // lower left, temperature starts at 1.0 on left 310 | { 96, 161, 123 }, // lower right 311 | { 26, 191, 0 } // upper left 312 | }; 313 | 314 | // NOTE: elevation is number of meters above a height of 64. If elevation is < 64, pass in 0. 315 | int BiomeColor( float temperature, float rainfall, int elevation, BiomeCorner corners[3] ) 316 | { 317 | // get UVs 318 | temperature = clamp(temperature - (float)elevation*0.00166667f,0.0f,1.0f); 319 | // crank it up: temperature = clamp(temperature - (float)elevation*0.166667f,0.0f,1.0f); 320 | rainfall = clamp(rainfall,0.0f,1.0f); 321 | rainfall *= temperature; 322 | 323 | // UV is essentially temperature, rainfall 324 | 325 | // lambda values for barycentric coordinates 326 | float lambda[3]; 327 | lambda[0] = temperature - rainfall; 328 | lambda[1] = 1.0f - temperature; 329 | lambda[2] = rainfall; 330 | 331 | float red = 0.0f, green = 0.0f, blue = 0.0f; 332 | for ( int i = 0; i < 3; i++ ) 333 | { 334 | red += lambda[i] * corners[i].red; 335 | green += lambda[i] * corners[i].green; 336 | blue += lambda[i] * corners[i].blue; 337 | } 338 | 339 | int r = (int)clamp(red,0.0f,255.0f); 340 | int g = (int)clamp(green,0.0f,255.0f); 341 | int b = (int)clamp(blue,0.0f,255.0f); 342 | 343 | return (r<<16)|(g<<8)|b; 344 | } 345 | 346 | int BiomeGrassColor( float temperature, float rainfall, int elevation ) 347 | { 348 | return BiomeColor( temperature, rainfall, elevation, grassCorners ); 349 | } 350 | 351 | int BiomeFoliageColor( float temperature, float rainfall, int elevation ) 352 | { 353 | return BiomeColor( temperature, rainfall, elevation, foliageCorners ); 354 | } 355 | 356 | void PrecomputeBiomeColors() 357 | { 358 | for ( int biome = 0; biome < 256; biome++ ) 359 | { 360 | gBiomes[biome].grass = ComputeBiomeColor( biome, 0, 1 ); 361 | gBiomes[biome].foliage = ComputeBiomeColor( biome, 0, 0 ); 362 | } 363 | } 364 | 365 | // elevation == 0 means for precomputed colors and for elevation off 366 | // or 64 high or below. 367 | int ComputeBiomeColor( int biome, int elevation, int isGrass ) 368 | { 369 | switch ( biome & 0x7 ) 370 | { 371 | case SWAMPLAND_BIOME: 372 | // the fefefe makes it so that carries are copied to the low bit, 373 | // then their magic "go to green" color offset is added in, then 374 | // divide by two gives a carry that will nicely go away. 375 | // old method: 376 | //color = BiomeGrassColor( gBiomes[biome].temperature, gBiomes[biome].rainfall ); 377 | //gBiomes[biome].grass = ((color & 0xfefefe) + 0x4e0e4e) / 2; 378 | //color = BiomeFoliageColor( gBiomes[biome].temperature, gBiomes[biome].rainfall ); 379 | //gBiomes[biome].foliage = ((color & 0xfefefe) + 0x4e0e4e) / 2; 380 | 381 | // new method: 382 | // yes, it's hard-wired in. It actually varies with temperature: 383 | // return temperature < -0.1D ? 0x4c763c : 0x6a7039; 384 | // where temperature is varied by PerlinNoise, but I haven't recreated the 385 | // PerlinNoise function yet. Rich green vs. sickly swamp brown. I'm going with brown. 386 | return 0x6a7039; 387 | 388 | // These are actually perfectly normal. Only sub-type 3, roofed forest, is different. 389 | //case FOREST_BIOME: // forestType 0 390 | //case FOREST_HILLS_BIOME: // forestType 0 391 | //case BIRCH_FOREST_BIOME: // forestType 2 392 | //case BIRCH_FOREST_HILLS_BIOME: // forestType 2 393 | // break; 394 | 395 | case ROOFED_FOREST_BIOME: // forestType 3 396 | if ( isGrass ) 397 | { 398 | int color = BiomeGrassColor( gBiomes[biome].temperature, gBiomes[biome].rainfall, elevation ); 399 | // the fefefe makes it so that carries are copied to the low bit, 400 | // then their magic "go to green" color offset is added in, then 401 | // divide by two gives a carry that will nicely go away. 402 | return ((color & 0xfefefe) + 0x28340a) / 2; 403 | } 404 | else 405 | { 406 | return BiomeFoliageColor( gBiomes[biome].temperature, gBiomes[biome].rainfall, elevation ); 407 | } 408 | 409 | case MESA_BIOME: 410 | case MESA_PLATEAU_F_BIOME: 411 | case MESA_PLATEAU_BIOME: 412 | // yes, it's hard-wired 413 | return isGrass ? 0x90814d : 0x9e814d; 414 | 415 | default: 416 | return isGrass ? BiomeGrassColor( gBiomes[biome].temperature, gBiomes[biome].rainfall, elevation ) : 417 | BiomeFoliageColor( gBiomes[biome].temperature, gBiomes[biome].rainfall, elevation ); 418 | } 419 | } 420 | 421 | int BiomeSwampRiverColor( int color ) 422 | { 423 | int r=(int)((color>>16)&0xff); 424 | int g=(int)((color>>8)&0xff); 425 | int b=(int)color&0xff; 426 | 427 | // swamp color modifier is 0xE0FFAE 428 | r=(r*0xE0)/255; 429 | // does nothing: g=(g*0xFF)/255; 430 | b=(b*0xAE)/255; 431 | color=(r<<16)|(g<<8)|b; 432 | 433 | return color; 434 | } 435 | -------------------------------------------------------------------------------- /py/biomes_gen.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | 4 | default = 0x3F76E4 5 | table = { 6 | 'Swamp': 0x617B64, 7 | 'River': 0x3F76E4, 8 | 'Ocean': 0x3F76E4, 9 | 'Lukewarm Ocean': 0x45ADF2, 10 | 'Warm Ocean': 0x43D5EE, 11 | 'Cold Ocean': 0x3D57D6, 12 | 'Frozen River': 0x3938C9, 13 | 'Frozen Ocean': 0x3938C9, 14 | } 15 | spec = { 16 | 'Swamp': ( 17 | {'Fixed': 0x4C763C}, {'Fixed': 0x4C763C}, 18 | ), 19 | 'Dark Forest': ( 20 | {'Average': 0x28340A}, {'Average': 0x28340A} 21 | ), 22 | 'Badlands': ( 23 | {'Fixed': 0x90814D}, {'Fixed': 0x9E814D}, 24 | ) 25 | } 26 | 27 | def parse_cpp(path): 28 | pattern = re.compile(r'\s*\{\s*/\*\s*(\d{1,3})\s*\*/\s*\"(.+)\",\s*([-\d\.]+)f,\s*([-\d\.]+)f,\s*(0x[\dABCDEF]{6}),\s*(0x[\dABCDEF]{6})\s*\},\s*') 29 | data = list() 30 | with open(path) as ifile: 31 | mark = 0 32 | line = ifile.readline() 33 | while line is not None: 34 | if mark == 0 and line.startswith('Biome gBiomes[256]={'): 35 | mark = 1 36 | if mark > 0 and line.startswith('};'): 37 | mark = -1 38 | break 39 | if mark > 0: 40 | m = pattern.match(line) 41 | if m is not None: 42 | s = ( 43 | int(m.group(1)), 44 | m.group(2), 45 | float(m.group(3)), 46 | float(m.group(4)), 47 | int(m.group(5), 16), 48 | int(m.group(6), 16), 49 | table.get(m.group(2), default) 50 | ) 51 | print(s) 52 | data.append(s) 53 | line = ifile.readline() 54 | return data 55 | 56 | 57 | def gen_source(path, data): 58 | with open(path, 'w') as ofile: 59 | ofile.write('// generate automatically\n') 60 | ofile.write('pub const BIOME_DATA: [(&\'static str, f32, f32, u32); 256] = [\n') 61 | ofile.write(' // (name, temperature, rainfall, water_color) \n') 62 | for s in data: 63 | ofile.write(' (/* %3d */ %-36s, %.2f, %.2f, 0x%X),\n' % (s[0], '"' + s[1] + '"', s[2], s[3], s[6])) 64 | ofile.write('];\n') 65 | ofile.write('\n') 66 | ofile.write('pub const COLORMAP_GRASS: &\'static [u8] = include_bytes!("grass.png");\n') 67 | ofile.write('\n') 68 | ofile.write('pub const COLORMAP_FOLIAGE: &\'static [u8] = include_bytes!("foliage.png");\n') 69 | ofile.write('\n') 70 | 71 | def gen_json(path, data): 72 | json_data = list() 73 | for s in data: 74 | obj = { 75 | 'id': s[0], 76 | 'name': s[1], 77 | 'temperature': s[2], 78 | 'rainfall': s[3], 79 | 'watercolor': s[6], 80 | } 81 | if s[0] in (6,): 82 | ops = spec['Swamp'] 83 | obj['ops_grass'] = ops[0] 84 | obj['ops_foliage'] = ops[1] 85 | elif s[0] in (29,): 86 | ops = spec['Dark Forest'] 87 | obj['ops_grass'] = ops[0] 88 | obj['ops_foliage'] = ops[1] 89 | elif s[0] in (37, 38, 39, 165, 166, 167): 90 | ops = spec['Badlands'] 91 | obj['ops_grass'] = ops[0] 92 | obj['ops_foliage'] = ops[1] 93 | json_data.append(obj) 94 | with open(path, 'w') as ofile: 95 | json.dump(json_data, ofile) 96 | 97 | if __name__ == "__main__": 98 | data = parse_cpp('biomes.cpp') 99 | gen_json('biome.json', data) 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /py/cache_gen.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import copy 3 | import re 4 | import json 5 | import zipfile 6 | 7 | import numpy as np 8 | import cv2 9 | 10 | 11 | 12 | def empty_2d_array(x, y): 13 | return [ [None for j in range(y)] for i in range(x) ] 14 | 15 | def _to_tuple(data): 16 | for k in data.keys(): 17 | outer = data[k] 18 | for i in range(len(outer)): 19 | inner = outer[i] 20 | outer[i] = tuple(inner) 21 | data[k] = tuple(outer) 22 | return data 23 | 24 | def _get_inv(table): 25 | inv = { 26 | 'west':empty_2d_array(4, 4), 27 | 'down':empty_2d_array(4, 4), 28 | 'north':empty_2d_array(4, 4), 29 | 'south':empty_2d_array(4, 4), 30 | 'up':empty_2d_array(4, 4), 31 | 'east':empty_2d_array(4, 4) 32 | } 33 | for x in range(4): 34 | for y in range(4): 35 | for (k, l2d) in table.items(): 36 | t = l2d[x][y] 37 | inv[t][x][y] = k 38 | return inv 39 | 40 | ROTATE = _to_tuple({ 41 | 'west': [ 42 | ['west', 'north', 'east', 'south'], # west 43 | ['west', 'north', 'east', 'south'], # west 44 | ['west', 'north', 'east', 'south'], # west 45 | ['west', 'north', 'east', 'south'], # west 46 | ], 47 | 'down': [ 48 | ['down', 'down', 'down', 'down'], # down 49 | ['north', 'east', 'south', 'west'], # north 50 | ['up', 'up', 'up', 'up'], # up 51 | ['south', 'west', 'north', 'east'], # south 52 | ], 53 | 'north': [ 54 | ['north', 'east', 'south', 'west'], # north 55 | ['up', 'up', 'up', 'up'], # up 56 | ['south', 'west', 'north', 'east'], # south 57 | ['down', 'down', 'down', 'down'], # down 58 | ], 59 | 'south': [ 60 | ['south', 'west', 'north', 'east'], # south 61 | ['down', 'down', 'down', 'down'], # down 62 | ['north', 'east', 'south', 'west'], # north 63 | ['up', 'up', 'up', 'up'], # up 64 | ], 65 | 'up': [ 66 | ['up', 'up', 'up', 'up'], # up 67 | ['south', 'west', 'north', 'east'], # south 68 | ['down', 'down', 'down', 'down'], # down 69 | ['north', 'east', 'south', 'west'], # north 70 | ], 71 | 'east': [ 72 | ['east', 'south', 'west', 'north' ], # east 73 | ['east', 'south', 'west', 'north' ], # east 74 | ['east', 'south', 'west', 'north' ], # east 75 | ['east', 'south', 'west', 'north' ], # east 76 | ] 77 | }) 78 | INV_ROTATE = _get_inv(ROTATE) 79 | FACE_INDEX = { 80 | 'west':(0,4,2,7), 81 | 'down':(0,1,4,5), 82 | 'north':(1,0,3,2), 83 | 'south':(4,5,6,7), 84 | 'up':(6,7,2,3), 85 | 'east':(5,1,7,3) 86 | } 87 | FACE_AXIS = { 88 | 'west': np.array([-1, 0, 0], np.float), 89 | 'down': np.array([0, -1, 0], np.float), 90 | 'north': np.array([0, 0, -1], np.float), 91 | 'south': np.array([0, 0, 1], np.float), 92 | 'up': np.array([0, 1, 0], np.float), 93 | 'east': np.array([1, 0, 0], np.float), 94 | } 95 | FACE_ROTATE_FACTOR_X = { 96 | 'west': 90, 97 | 'down': 0, 98 | 'north': 0, 99 | 'south': 0, 100 | 'up': 0, 101 | 'east': -90, 102 | } 103 | FACE_ROTATE_FACTOR_Y = { 104 | 'west': 0, 105 | 'down': -90, 106 | 'north': 0, 107 | 'south': 0, 108 | 'up': 90, 109 | 'east': 0, 110 | } 111 | 112 | class AssetsLoader: 113 | 114 | def __init__(self, resources): 115 | if isinstance(resources, list): 116 | self.zips = [zipfile.ZipFile(resource, 'r') for resource in resources] 117 | else: 118 | self.zips = [zipfile.ZipFile(resources, 'r')] 119 | self.names = dict() 120 | for (i, rsc) in enumerate(self.zips): 121 | for name in rsc.namelist(): 122 | self.names[name] = i 123 | 124 | def get_blocks(self): 125 | pattern = re.compile(r'assets/(\S+)/blockstates/(\S+).json') 126 | st = list() 127 | for name in self.names.keys(): 128 | m = pattern.match(name) 129 | if m is not None: 130 | st.append((m.group(1), m.group(2))) 131 | st.sort() 132 | return st 133 | 134 | def get_blockstate(self, namespace, name): 135 | full = 'assets/%s/blockstates/%s.json' % (namespace, name) 136 | i = self.names.get(full) 137 | if i is not None: 138 | with self.zips[i].open(full) as ifile: 139 | return json.load(ifile) 140 | return None 141 | 142 | def get_model(self, namespace, name): 143 | full = 'assets/%s/models/%s.json' % (namespace, name) 144 | i = self.names.get(full) 145 | if i is not None: 146 | with self.zips[i].open(full) as ifile: 147 | return json.load(ifile) 148 | return None 149 | 150 | def get_texture(self, namespace, name): 151 | info = 'assets/%s/textures/%s.png.mcmeta' % (namespace, name) 152 | full = 'assets/%s/textures/%s.png' % (namespace, name) 153 | i = self.names.get(full) 154 | if i is not None: 155 | with self.zips[i].open(full) as ifile: 156 | buf = ifile.read() 157 | buf = np.frombuffer(buf, np.uint8) 158 | img = cv2.imdecode(buf, cv2.IMREAD_UNCHANGED) 159 | if info in self.names: 160 | w = img.shape[1] 161 | img = img[0:w,0:w,:] 162 | return img 163 | return None 164 | 165 | 166 | class AppliedModel: 167 | 168 | def __init__(self, json_data): 169 | if isinstance(json_data, list): 170 | maxw = 0 171 | for one in json_data: 172 | w = one.get('weight', 1) 173 | if maxw < w: 174 | maxw = w 175 | self._parse_model(one) 176 | else: 177 | self._parse_model(json_data) 178 | 179 | def _parse_model(self, one): 180 | self.model = one.get('model') 181 | self.x = int(one.get('x', 0)) 182 | self.y = int(one.get('y', 0)) 183 | self.uvlock = bool(one.get('uvlock', False)) 184 | 185 | def get_faces(self, face): 186 | if isinstance(self.model, Model): 187 | x = self.x // 90 188 | y = self.y // 90 189 | original_face = INV_ROTATE[face][x][y] 190 | rotate = np.array([ 191 | FACE_AXIS[ROTATE['east'][x][y]], 192 | FACE_AXIS[ROTATE['up'][x][y]], 193 | FACE_AXIS[ROTATE['south'][x][y]] 194 | ], np.float).T 195 | center = np.array([8, 8, 8], np.float) 196 | faces_tuple = self.model.get_faces(original_face) 197 | for (vs, fs) in faces_tuple: 198 | for v in vs: 199 | v[:] = np.dot(rotate, v - center) + center 200 | cullface = fs.cullface 201 | if cullface is not None: 202 | fs.cullface = ROTATE[cullface][x][y] 203 | if self.uvlock: 204 | fs.rotation = (fs.rotation + 720 - x * FACE_ROTATE_FACTOR_X[face] - y * FACE_ROTATE_FACTOR_Y[face]) % 360 205 | return faces_tuple 206 | return None 207 | 208 | 209 | class ModelProvider: 210 | 211 | def __init__(self, loader: AssetsLoader, namespace): 212 | self.namespace = namespace 213 | self.loader = loader 214 | self.cache = dict() 215 | 216 | # def set_namespace(self, namespace): 217 | # self.namespace = namespace 218 | 219 | def get(self, name, nocache=False): 220 | namespace = self.namespace 221 | i = name.find(':') 222 | if i >= 0: 223 | namespace = name[:i] 224 | name = name[i+1:] 225 | if nocache: 226 | mdl = self.loader.get_model(namespace, name) 227 | if mdl is not None: 228 | mdl = Model(mdl) 229 | else: 230 | mdl = self.cache.get(namespace + ':' + name) 231 | if mdl is None: 232 | mdl = self.loader.get_model(namespace, name) 233 | if mdl is not None: 234 | mdl = Model(mdl) 235 | self.cache[namespace + ':' + name] = mdl 236 | return mdl 237 | 238 | 239 | class TextureProvider: 240 | 241 | def __init__(self, loader: AssetsLoader, namespace): 242 | self.namespace = namespace 243 | self.loader = loader 244 | self.cache = dict() 245 | 246 | # def set_namespace(self, namespace): 247 | # self.namespace = namespace 248 | 249 | def get(self, name, nocache=False): 250 | namespace = self.namespace 251 | i = name.find(':') 252 | if i >= 0: 253 | namespace = name[:i] 254 | name = name[i+1:] 255 | if nocache: 256 | tex = self.loader.get_texture(namespace, name) 257 | if tex is not None: 258 | tex = Model(tex) 259 | else: 260 | tex = self.cache.get(namespace + ':' + name) 261 | if tex is None: 262 | tex = self.loader.get_texture(namespace, name) 263 | if tex is not None: 264 | self.cache[namespace + ':' + name] = tex 265 | return tex 266 | 267 | 268 | class IdGen: 269 | 270 | def __init__(self): 271 | self.cache = [None] 272 | 273 | def generate(self, applied_model: AppliedModel): 274 | self.cache.append(applied_model) 275 | return len(self.cache) - 1 276 | 277 | def total(self): 278 | return len(self.cache) 279 | 280 | def link(self, mdlpvd: ModelProvider): 281 | targets = dict() 282 | for am in self.cache[1:]: 283 | name = am.model 284 | mdl = targets.get(name) 285 | if mdl is None: 286 | mdl = mdlpvd.get(name, True) 287 | mdl.link(mdlpvd) 288 | print('load model:', name) 289 | targets[name] = mdl 290 | am.model = mdl 291 | return self.cache 292 | 293 | 294 | class Blockstate: 295 | 296 | def __init__(self, json_data): 297 | self.props = dict() 298 | self.pairs = list() 299 | self.ids = list() 300 | if 'variants' in json_data: 301 | self._parse_variants(json_data['variants']) 302 | return 303 | if 'multipart' in json_data: 304 | self._parse_multipart(json_data['multipart']) 305 | return 306 | 307 | def serialize(self, id_gen: IdGen): 308 | self.ids = [0] * len(self.pairs) 309 | json_data = dict() 310 | if len(self.props) == 0: 311 | tp = 'single' 312 | elif isinstance(self.pairs[0][0], list): 313 | tp = 'multipart' 314 | else: 315 | tp = 'variants' 316 | if tp == 'single': 317 | value = id_gen.generate(self.pairs[0][1]) 318 | self.ids[0] = value 319 | json_data[tp] = value 320 | else: 321 | keys = dict() 322 | for (k1, v1) in self.props.items(): 323 | for (k2, v) in v1.items(): 324 | k = '%s=%s' % (k1, k2) 325 | keys[k] = v 326 | if tp == 'variants': 327 | values = dict() 328 | for (i, pair) in enumerate(self.pairs): 329 | name = str(pair[0]) 330 | value = id_gen.generate(pair[1]) 331 | self.ids[i] = value 332 | values[name] = value 333 | else: 334 | values = list() 335 | for (i, pair) in enumerate(self.pairs): 336 | value = id_gen.generate(pair[1]) 337 | self.ids[i] = value 338 | value = {'when': pair[0], 'apply': value} 339 | values.append(value) 340 | json_data[tp] = {'keys': keys, 'values': values} 341 | return json_data 342 | 343 | 344 | def _parse_variants(self, json_data): 345 | for k in json_data.keys(): 346 | if k == '': 347 | v = json_data[''] 348 | self.pairs.append((0x00, AppliedModel(v))) 349 | return 350 | for p in k.split(','): 351 | tmp = p.split('=') 352 | pname = tmp[0] 353 | pvalue = tmp[1] 354 | key = self.props.get(pname) 355 | if key is None: 356 | key = dict() 357 | self.props[pname] = key 358 | key[pvalue] = 0x00 359 | self._gen_key() 360 | for (k, v) in json_data.items(): 361 | msk = 0x00 362 | for p in k.split(','): 363 | tmp = p.split('=') 364 | pname = tmp[0] 365 | pvalue = tmp[1] 366 | m = self.props[pname][pvalue] 367 | msk |= m 368 | self.pairs.append((msk, AppliedModel(v))) 369 | 370 | def _parse_multipart(self, json_data): 371 | for item in json_data: 372 | cond = item.get('when') 373 | if cond is not None: 374 | or_cond = cond.get('OR') 375 | if or_cond is not None: 376 | for c in or_cond: 377 | self._add_key_one(c) 378 | else: 379 | self._add_key_one(cond) 380 | self._gen_key() 381 | for item in json_data: 382 | cond = item.get('when') 383 | keys = list() 384 | if cond is not None: 385 | or_cond = cond.get('OR') 386 | if or_cond is not None: 387 | for c in or_cond: 388 | keys.extend(self._cal_key(c)) 389 | else: 390 | keys.extend(self._cal_key(cond)) 391 | if len(keys) == 0: 392 | keys.append(0) 393 | self.pairs.append((keys, AppliedModel(item['apply']))) 394 | 395 | def _add_key_one(self, cond): 396 | for (k, v) in cond.items(): 397 | v = str(v) 398 | key = self.props.get(k) 399 | if key is None: 400 | key = dict() 401 | self.props[k] = key 402 | for value in v.split('|'): 403 | key[value] = 0x00 404 | 405 | def _gen_key(self): 406 | msk = 0x01 407 | for (k, v) in self.props.items(): 408 | for p in v.keys(): 409 | v[p] = msk 410 | msk <<= 1 411 | 412 | def _cal_key(self, cond): 413 | msks = [0x00] 414 | for (k, v) in cond.items(): 415 | v = str(v) 416 | propname = self.props[k] 417 | if '|' in v: 418 | new_msks = [] 419 | for value in v.split('|'): 420 | key = propname[value] 421 | tmp = [i | key for i in msks] 422 | new_msks.extend(tmp) 423 | msks = new_msks 424 | else: 425 | count = len(msks) 426 | key = propname[v] 427 | for i in range(count): 428 | msks[i] |= key 429 | return msks 430 | 431 | 432 | 433 | 434 | class Face: 435 | 436 | def __init__(self, json_data): 437 | self.uv = np.array(json_data.get('uv', [0, 0, 16, 16]), np.float) 438 | self.texture = json_data.get('texture') 439 | self.cullface = json_data.get('cullface', None) 440 | self.rotation = json_data.get('rotation', 0) 441 | self.tintindex = json_data.get('tintindex', None) 442 | 443 | def link(self, textures): 444 | while self.texture.startswith('#'): 445 | t = self.texture[1:] 446 | self.texture = textures.get(t, self.texture) 447 | 448 | 449 | class Element: 450 | 451 | def __init__(self, json_data): 452 | self.v_from = np.array(json_data.get('from'), np.float) 453 | self.v_to = np.array(json_data.get('to'), np.float) 454 | self.rotation = None ###ignore 455 | self.shade = json_data.get('shade', False) 456 | self.faces = dict() 457 | faces = json_data.get('faces') 458 | if faces is not None: 459 | for (k, v) in faces.items(): 460 | self.faces[k] = Face(v) 461 | 462 | def link(self, textures): 463 | for (k, v) in self.faces.items(): 464 | v.link(textures) 465 | 466 | def get_vertex(self, index: int): 467 | v = np.copy(self.v_from) 468 | if index & 0x1 > 0: 469 | v[0] = self.v_to[0] 470 | if index & 0x2 > 0: 471 | v[1] = self.v_to[1] 472 | if index & 0x4 > 0: 473 | v[2] = self.v_to[2] 474 | return v 475 | 476 | def get_face(self, face): 477 | return self.faces.get(face) 478 | 479 | def get_face_vertex(self, face): 480 | return tuple([self.get_vertex(i) for i in FACE_INDEX[face]]) 481 | 482 | def get_face_area(self, vertexs): 483 | diff = vertexs[2] - vertexs[0] 484 | return np.prod(diff[diff > 0]) 485 | 486 | class Model: 487 | 488 | def __init__(self, json_data): 489 | self.parent = json_data.get('parent', None) 490 | self.ambientocclusion = json_data.get('ambientocclusion', None) 491 | self.display = None ###ignore 492 | textures = json_data.get('textures') 493 | if textures is not None: 494 | self.textures = textures 495 | else: 496 | self.textures = dict() 497 | elements = json_data.get('elements') 498 | if elements is not None: 499 | self.elements = [Element(e) for e in elements] 500 | else: 501 | self.elements = None 502 | 503 | def link(self, model_cache): 504 | if self.parent is not None: 505 | stk = [self] 506 | parent = self.parent 507 | while parent is not None: 508 | mdl = model_cache.get(parent) 509 | stk.append(copy.deepcopy(mdl)) 510 | parent = mdl.parent 511 | parent = stk.pop() 512 | while len(stk) > 0: 513 | this = stk.pop() 514 | if this.ambientocclusion is None: 515 | this.ambientocclusion = parent.ambientocclusion 516 | for (k, v) in this.textures.items(): 517 | parent.textures[k] = v 518 | this.textures = parent.textures 519 | if this.elements is None: 520 | this.elements = parent.elements 521 | parent = this 522 | del self.parent 523 | if self.ambientocclusion is None: 524 | self.ambientocclusion = True 525 | if self.elements is None: 526 | self.elements = list() 527 | for e in self.elements: 528 | e.link(self.textures) 529 | 530 | def get_faces(self, face): 531 | res = list() 532 | for e in self.elements: 533 | facetex = e.get_face(face) 534 | if facetex is not None: 535 | vertex = e.get_face_vertex(face) 536 | res.append((vertex, copy.copy(facetex))) 537 | return res 538 | 539 | 540 | ### 541 | ### 542 | ### 543 | 544 | 545 | EQUIV_ROTATE1 = [0, 270, 90, 180] 546 | EQUIV_ROTATE2 = [90, 180, 0, 270] 547 | 548 | 549 | class FullRenderer: 550 | 551 | def __init__(self, count, linewidth, tex_pvd): 552 | self.iw = linewidth 553 | self.ih = (count + linewidth - 1) // linewidth 554 | self.tex_pvd = tex_pvd 555 | self.img = np.zeros((self.ih * 16, self.iw * 16, 4), np.uint8) 556 | self.heightmap = np.zeros((self.ih, self.iw), np.uint8) # height x 8 557 | 558 | def draw(self, fs_tuples, index): 559 | if index >= self.ih * self.iw: 560 | return False 561 | ix = (index % self.iw) 562 | iy = (index // self.iw) 563 | offset = np.array((ix, iy, ix, iy), np.int32) * 16 564 | elements = [FullRenderer.rectify(vertexs, face) for (vertexs, face) in fs_tuples] 565 | sorted(elements, key=lambda x: x[4]) 566 | height = 0 567 | if len(elements) == 0: 568 | return False 569 | for (src_uv, tex_uv, tex, r, h) in elements: 570 | tex = self.tex_pvd.get(tex) 571 | if tex is None: 572 | continue 573 | tex_uv = tex_uv / 16.0 * np.array((tex.shape[1], tex.shape[0], tex.shape[1], tex.shape[0]), np.float) 574 | tex_uv[0:2] = np.floor(tex_uv[0:2]) 575 | tex_uv[2:4] = np.ceil(tex_uv[2:4]) 576 | tex_uv = np.array(tex_uv, np.int32) 577 | tex = tex[tex_uv[1]:tex_uv[3],tex_uv[0]:tex_uv[2],:] 578 | r = -(r // 90) 579 | tex = np.rot90(tex, r) 580 | src_uv[0:2] = np.floor(src_uv[0:2]) 581 | src_uv[2:4] = np.ceil(src_uv[2:4]) 582 | src_uv = np.array(src_uv, np.int32) 583 | tex = cv2.resize(tex, (src_uv[2] - src_uv[0], src_uv[3] - src_uv[1]), interpolation=cv2.INTER_NEAREST) 584 | src_uv = src_uv + offset 585 | if tex.shape[2] < self.img.shape[2]: 586 | self.img[src_uv[1]:src_uv[3],src_uv[0]:src_uv[2],0:3] = tex 587 | self.img[src_uv[1]:src_uv[3],src_uv[0]:src_uv[2],3] = 255 588 | else: 589 | bg = np.array(self.img[src_uv[1]:src_uv[3],src_uv[0]:src_uv[2], :], np.float) / 255 590 | fg = np.array(tex, np.float) / 255 591 | FullRenderer.blend(bg, fg) 592 | self.img[src_uv[1]:src_uv[3],src_uv[0]:src_uv[2],:] = np.array(bg * 255, np.uint8) 593 | height = h 594 | self.heightmap[iy, ix] = int(height * 8) 595 | return True 596 | 597 | @staticmethod 598 | def blend(bg: np.ndarray, fg: np.ndarray): 599 | fg_a = fg[:,:,3] 600 | factor = fg_a 601 | fg[:,:,0] *= factor 602 | fg[:,:,1] *= factor 603 | fg[:,:,2] *= factor 604 | bg_a = bg[:,:,3] 605 | factor = bg_a * (1.0 - fg_a) 606 | bg[:,:,0] *= factor 607 | bg[:,:,1] *= factor 608 | bg[:,:,2] *= factor 609 | factor = fg_a + bg_a * (1.0 - fg_a) 610 | mask = factor > 0 611 | t = fg[:,:,0] + bg[:,:,0] 612 | t[mask] /= factor[mask] 613 | t[~mask] = 0.0 614 | bg[:,:,0] = t 615 | t = fg[:,:,1] + bg[:,:,1] 616 | t[mask] /= factor[mask] 617 | t[~mask] = 0.0 618 | bg[:,:,1] = t 619 | t = fg[:,:,2] + bg[:,:,2] 620 | t[mask] /= factor[mask] 621 | t[~mask] = 0.0 622 | bg[:,:,2] = t 623 | bg[:,:,3] = factor 624 | 625 | 626 | @staticmethod 627 | def rectify(vertexs, face): 628 | h = vertexs[0][1] 629 | (src_uv ,src_r) = FullRenderer.rectify_verts(vertexs) 630 | (tex_uv, tex_r) = FullRenderer.rectify_uv(face.uv) 631 | r = (src_r + tex_r + face.rotation) % 360 632 | tex = face.texture 633 | return (src_uv, tex_uv, tex, r, h) 634 | 635 | @staticmethod 636 | def rectify_uv(uv): 637 | k = 0x0 638 | uv = np.copy(uv) 639 | if uv[0] > uv[2]: 640 | k |= 0x1 641 | t = uv[0] 642 | uv[0] = uv[2] 643 | uv[2] = t 644 | if uv[1] > uv[3]: 645 | k |= 0x2 646 | t = uv[1] 647 | uv[1] = uv[3] 648 | uv[3] = t 649 | return (uv, EQUIV_ROTATE1[k]) 650 | 651 | @staticmethod 652 | def rectify_verts(vertexs): 653 | vmin = vertexs[0] 654 | imin = 0 655 | vmax = vertexs[0] 656 | imax = 0 657 | c = 1 658 | for v in vertexs[1:]: 659 | if v[0] <= vmin[0] and v[2] <= vmin[2]: 660 | imin = c 661 | vmin = v 662 | if v[0] >= vmax[0] and v[2] >= vmax[2]: 663 | imax = c 664 | vmax = v 665 | c += 1 666 | return (np.array((vmin[0], vmin[2], vmax[0], vmax[2])), EQUIV_ROTATE2[imin]) 667 | 668 | @staticmethod 669 | def color_extraction_weight(): 670 | return np.ones((16,16), np.float) 671 | 672 | def color_extraction(self): 673 | colormap = np.zeros((self.ih, self.iw, 4), np.uint8) 674 | weightmap = np.zeros((self.ih, self.iw), np.uint8) 675 | swi = FullRenderer.color_extraction_weight() 676 | for x in range(self.iw): 677 | ix = x * 16 678 | for y in range(self.ih): 679 | iy = y * 16 680 | color = np.zeros((4,), np.uint8) 681 | selected = self.img[iy:iy+16,ix:ix+16,:] 682 | mask = selected[:,:,3] > 0 683 | if np.any(mask): 684 | for c in range(4): 685 | channel = np.array(selected[:,:,c], np.float) 686 | tmp = channel * swi 687 | color[c] = np.sum(tmp[mask]) / np.sum(swi[mask]) 688 | colormap[y, x, :] = color[:] 689 | weightmap[y, x] = max(np.count_nonzero(mask) - 1, 0) 690 | return (colormap, weightmap) 691 | 692 | 693 | 694 | ### 695 | ### 696 | ### 697 | 698 | import argparse 699 | import logging 700 | from os import path 701 | 702 | 703 | if __name__ == "__main__": 704 | 705 | parser = argparse.ArgumentParser() 706 | parser.add_argument('assets', type=str, help='minecraft resourcepack (or version.jar)', nargs='+') 707 | parser.add_argument('-l', '--log', type=str, help='log output: file or STDOUT', default='STDOUT', required=False) 708 | parser.add_argument('-w', '--linewidth', type=int, help='number of element every line', default=32, required=False) 709 | args = parser.parse_args() 710 | 711 | 712 | if args.log == 'STDOUT': 713 | logging.basicConfig(level=logging.INFO) 714 | else: 715 | logging.basicConfig(filename=args.log, level=logging.INFO) 716 | 717 | 718 | loader = AssetsLoader(args.assets) 719 | cache = dict() 720 | idgen = IdGen() 721 | logging.info('> generate index') 722 | with open('index.json', 'w') as ofile: 723 | whole = dict() 724 | data = dict() 725 | for (namespace, block) in loader.get_blocks(): 726 | name = namespace + ':' + block 727 | bs = loader.get_blockstate(namespace, block) 728 | bs = Blockstate(bs) 729 | cache[name] = bs 730 | data[name] = bs.serialize(idgen) 731 | logging.info('load blockstate {}'.format(name)) 732 | whole['data'] = data 733 | whole['config'] = { 734 | 'linewidth': args.linewidth 735 | } 736 | json.dump(whole, ofile) 737 | logging.info('> write index') 738 | 739 | count = idgen.total() 740 | del idgen 741 | 742 | logging.info('> generate model') 743 | model_cache = dict() 744 | mdlpvd = ModelProvider(loader, 'minecraft') 745 | for (name, bs) in cache.items(): 746 | i = name.find(':') 747 | namespace = name[:i] 748 | name = name[i+1:] 749 | # mdlpvd.set_namespace(namespace) 750 | for (bspair, index) in zip(bs.pairs, bs.ids): 751 | applied_model = bspair[1] 752 | mdlname = namespace + ':' + applied_model.model 753 | mdl = model_cache.get(mdlname) 754 | if mdl is None: 755 | mdl = mdlpvd.get(applied_model.model, True) 756 | if mdl is None: 757 | logging.warning("model is None: {}".format(applied_model.model)) 758 | continue 759 | mdl.link(mdlpvd) 760 | logging.info('load model {}'.format(mdlname)) 761 | model_cache[mdlname] = mdl 762 | applied_model.model = mdl 763 | 764 | del model_cache 765 | del mdlpvd 766 | 767 | logging.info('> render') 768 | tex_pvd = TextureProvider(loader, 'minecraft') 769 | renderer = FullRenderer(count, args.linewidth, tex_pvd) 770 | for (name, bs) in cache.items(): 771 | i = name.find(':') 772 | namespace = name[:i] 773 | name = name[i+1:] 774 | # tex_pvd.set_namespace(namespace) 775 | for (bspair, index) in zip(bs.pairs, bs.ids): 776 | applied_model = bspair[1] 777 | ts = applied_model.get_faces('up') 778 | try: 779 | if renderer.draw(ts, index): 780 | logging.info('draw [{}] {}:{}'.format(index, namespace, name)) 781 | else: 782 | logging.warning('empty [{}] {}:{}'.format(index, namespace, name)) 783 | except Exception as e: 784 | logging.warning('[{}] {}:{} {}'.format(index, namespace, name, e)) 785 | 786 | cv2.imwrite('baked.png', renderer.img) 787 | cv2.imwrite('heightmap.png', renderer.heightmap) 788 | (cmap, wmap) = renderer.color_extraction() 789 | cv2.imwrite('colormap.png', cmap) 790 | cv2.imwrite('weightmap.png', wmap) 791 | c_grass = loader.get_texture('minecraft', 'colormap/grass') 792 | cv2.imwrite('grass.png', c_grass[:,:,0:3]) 793 | c_foliage = loader.get_texture('minecraft', 'colormap/foliage') 794 | cv2.imwrite('foliage.png', c_foliage[:,:,0:3]) 795 | pass 796 | 797 | ''' 798 | if __name__ == "__main__": 799 | ip = input('> jar: ') 800 | loader = AssetsLoader(ip) 801 | idgen = IdGen() 802 | mdlpvd = ModelProvider(loader, 'minecraft') 803 | with open('index.json', 'w') as ofile: 804 | whole = dict() 805 | for (ns, b) in loader.get_blocks(): 806 | #print('load', b) 807 | bs = loader.get_blockstate(ns, b) 808 | #print('parse', b) 809 | bs = Blockstate(bs) 810 | #print('write', b) 811 | data = bs.serialize(idgen) 812 | name = '%s:%s' % (ns, b) 813 | whole[name] = data 814 | json.dump(whole, ofile) 815 | ls = idgen.link(mdlpvd) 816 | 817 | \''' 818 | with open('text.txt', 'w') as ofile: 819 | for s in ls[1792:1796]: 820 | for (vs, fs) in s.get_faces('up'): 821 | ofile.write(str(vs)) 822 | ofile.write('\n') 823 | ofile.write('%s %s %d' % (str(fs.uv), fs.texture, fs.rotation)) 824 | ofile.write('\n') 825 | ofile.write('\n') 826 | ofile.write('\n') 827 | \''' 828 | renderer = FullRenderer(len(ls), 32, TextureProvider(loader, 'minecraft')) 829 | ofile = open('run.log', 'w') 830 | c = 1 - 1 831 | for s in ls[1:]: 832 | c += 1 833 | ts = s.get_faces('up') 834 | try: 835 | renderer.draw(ts, c) 836 | except Exception as e: 837 | ofile.write('[%d] %s\n' % (c, e)) 838 | 839 | ofile.close() 840 | cv2.imwrite('baked.png', renderer.img) 841 | cv2.imwrite('heightmap.png', renderer.heightmap) 842 | (cmap, wmap) = renderer.color_extraction() 843 | cv2.imwrite('colormap.png', cmap) 844 | cv2.imwrite('weightmap.png', wmap) 845 | pass 846 | ''' -------------------------------------------------------------------------------- /py/colormap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWCarrot/voxelmap-cache-render/ff4361fba50f2c25ebd36b8cc28561f9439212b3/py/colormap.png -------------------------------------------------------------------------------- /py/foliage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWCarrot/voxelmap-cache-render/ff4361fba50f2c25ebd36b8cc28561f9439212b3/py/foliage.png -------------------------------------------------------------------------------- /py/grass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWCarrot/voxelmap-cache-render/ff4361fba50f2c25ebd36b8cc28561f9439212b3/py/grass.png -------------------------------------------------------------------------------- /py/heightmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWCarrot/voxelmap-cache-render/ff4361fba50f2c25ebd36b8cc28561f9439212b3/py/heightmap.png -------------------------------------------------------------------------------- /py/pickup.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import shutil 4 | import sys 5 | 6 | CUR = '' 7 | 8 | FILELIST = [ 9 | "index.json", 10 | "colormap.png", 11 | "weightmap.png", 12 | "grass.png", 13 | "foliage.png", 14 | "biome.json" 15 | ] 16 | 17 | 18 | def current_dir(arg0): 19 | sp = os.path.split(os.path.abspath(arg0)) 20 | return sp[0] 21 | 22 | def main(args): 23 | for tgtf_raw in args: 24 | tgtf = os.path.join(tgtf_raw, 'resource') 25 | try: 26 | os.makedirs (tgtf) 27 | except FileExistsError as e: 28 | pass 29 | for name in FILELIST: 30 | src = os.path.abspath(os.path.join(CUR, name)) 31 | shutil.copy(src, tgtf) 32 | print('copyed', src, 'to', tgtf) 33 | return 0 34 | 35 | if __name__ == "__main__": 36 | CUR = current_dir(sys.argv[0]) 37 | args = sys.argv[1:] 38 | if len(args) == 0: 39 | args = [ 40 | os.path.abspath(os.path.join(CUR, '..', 'target', 'debug')), 41 | os.path.abspath(os.path.join(CUR, '..', 'target', 'release')) 42 | ] 43 | main(args) -------------------------------------------------------------------------------- /py/specific.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWCarrot/voxelmap-cache-render/ff4361fba50f2c25ebd36b8cc28561f9439212b3/py/specific.zip -------------------------------------------------------------------------------- /py/tool_merge.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | import os 4 | import re 5 | import argparse 6 | 7 | def list_files(path): 8 | pattern = re.compile(r'(-?\d+),(-?\d+).png') 9 | res = list() 10 | rg = list() #[xmin ymin xmax ymax] 11 | for (dirpath, dirnames, filenames) in os.walk(path): 12 | for filename in filenames: 13 | m = pattern.match(filename) 14 | if m is not None: 15 | x = int(m.group(1)) 16 | y = int(m.group(2)) 17 | p = os.path.join(dirpath, filename) 18 | res.append((x,y,p)) 19 | if len(rg) == 0: 20 | rg.append(x) 21 | rg.append(y) 22 | rg.append(x) 23 | rg.append(y) 24 | else: 25 | if rg[0] > x: 26 | rg[0] = x 27 | if rg[1] > y: 28 | rg[1] = y 29 | if rg[2] < x: 30 | rg[2] = x 31 | if rg[3] < y: 32 | rg[3] = y 33 | rg = (rg[0], rg[1], rg[2] + 1, rg[3] + 1) 34 | return (res, rg) 35 | 36 | 37 | def merge(res, rg): 38 | st = np.array((256, 256), dtype=np.int32) 39 | rg = np.array(rg, dtype=np.int32) 40 | sz = (rg[2:4] - rg[0:2]) * st 41 | img = np.zeros((sz[1], sz[0], 4), dtype=np.uint8) 42 | st = np.array((st[0], st[1], st[0], st[1]), dtype=np.int32) 43 | sz = np.array((rg[0], rg[1], rg[0], rg[1]), dtype=np.int32) 44 | for (x, z, path) in res: 45 | if x < rg[0] or z < rg[1] or x >= rg[2] or z >= rg[3]: 46 | continue 47 | tg = np.array((x, z, x + 1, z + 1), dtype=np.int32) 48 | tg = (tg - sz) * st 49 | part = cv2.imread(path, flags=cv2.IMREAD_UNCHANGED) 50 | if part is None: 51 | continue 52 | img[tg[1]:tg[3],tg[0]:tg[2],:] = part[:,:,:] 53 | return img 54 | 55 | 56 | 57 | if __name__ == "__main__": 58 | 59 | parser = argparse.ArgumentParser() 60 | parser.add_argument('input_dir', type=str) 61 | parser.add_argument('-o', '--output_file', type=str) 62 | parser.add_argument('-r', '--range', type=str) # xmin,ymin;xmax,ymax 63 | 64 | args = parser.parse_args() 65 | 66 | (res, rg) = list_files(args.input_dir) 67 | if not (args.range == 'max'): 68 | sp = args.range.split(' ') 69 | p1 = sp[0:2] 70 | xmin = int(p1[0]) 71 | ymin = int(p1[1]) 72 | p2 = sp[2:4] 73 | xmax = int(p2[0]) + 1 74 | ymax = int(p2[1]) + 1 75 | rg = (xmin, ymin, xmax, ymax) 76 | 77 | h = merge(res, rg) 78 | cv2.imwrite(args.output_file, h) 79 | 80 | pass -------------------------------------------------------------------------------- /py/weightmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWCarrot/voxelmap-cache-render/ff4361fba50f2c25ebd36b8cc28561f9439212b3/py/weightmap.png -------------------------------------------------------------------------------- /src/application.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::path::PathBuf; 3 | use std::io; 4 | use std::fs; 5 | use std::fs::File; 6 | use std::sync::Arc; 7 | use std::thread; 8 | 9 | 10 | use super::color::de; 11 | use super::color::BakedColorManager; 12 | use super::render; 13 | use super::render::RenderOptions; 14 | use super::render::tile::Tile; 15 | 16 | pub struct AppOptions { 17 | render_options: RenderOptions, 18 | input_folder: PathBuf, 19 | output_folder: PathBuf, 20 | thread_num: usize, 21 | } 22 | 23 | impl Default for AppOptions { 24 | fn default() -> Self { 25 | AppOptions { 26 | render_options: Default::default(), 27 | input_folder: Default::default(), 28 | output_folder: Default::default(), 29 | thread_num: 1 30 | } 31 | } 32 | } 33 | 34 | impl AppOptions { 35 | 36 | pub fn render_option_mut(&mut self) -> &mut RenderOptions { 37 | &mut self.render_options 38 | } 39 | 40 | pub fn set_thread_num(&mut self, thread_num: usize) { 41 | self.thread_num = thread_num; 42 | } 43 | 44 | pub fn set_input_folder(&mut self, path: &str) { 45 | self.input_folder = PathBuf::from(path); 46 | } 47 | 48 | pub fn set_output_folder(&mut self, path: &str) { 49 | self.output_folder = PathBuf::from(path); 50 | } 51 | 52 | pub fn ensure_output_folder(&self) -> io::Result<()> { 53 | if !self.output_folder.is_dir() { 54 | std::fs::create_dir_all(self.output_folder.as_path()) 55 | } else { 56 | Ok(()) 57 | } 58 | } 59 | } 60 | 61 | pub struct Application { 62 | options: AppOptions, 63 | color_mgr: BakedColorManager, 64 | } 65 | 66 | #[derive(Debug, Clone)] 67 | pub struct RenderTask { 68 | pub src: PathBuf, 69 | pub tgt: PathBuf, 70 | pub tile_id: (i32, i32) 71 | } 72 | 73 | impl Application { 74 | 75 | pub fn new(options: AppOptions) -> Self { 76 | Application { 77 | color_mgr: build_colormanager(), 78 | options 79 | } 80 | } 81 | 82 | pub fn list_files(&self) -> Vec { 83 | const EXT: &'static str = ".zip"; 84 | if let Ok(read_dir) = self.options.input_folder.read_dir() { 85 | let odir = &self.options.output_folder; 86 | read_dir.filter_map(|entry| { 87 | let src = entry.ok()?.path(); 88 | let filename = src.file_name()?.to_str()?; 89 | if !filename.ends_with(EXT) { 90 | return None; 91 | } 92 | let filename = &filename[0 .. filename.len() - EXT.len()]; 93 | let mut sp = filename.splitn(2, ','); 94 | let x: i32 = sp.next()?.parse().ok()?; 95 | let z: i32 = sp.next()?.parse().ok()?; 96 | let tgt = odir.join(format!("{},{}.png", x, z)); 97 | Some(RenderTask{src, tgt, tile_id: (x, z)}) 98 | }).collect() 99 | } else { 100 | Vec::new() 101 | } 102 | } 103 | 104 | pub fn render_one(&self, src: &Path, tgt: &Path, tile_id: &(i32, i32)) -> Result<(), Box> { 105 | use image::ImageFormat::Png; 106 | 107 | let ifile = File::open(src).map_err(Box::new)?; 108 | let tile = Tile::load(ifile, tile_id.clone(), &self.color_mgr)?; 109 | let pic = render::render(tile, &self.color_mgr, &self.options.render_options); 110 | pic.save_with_format(tgt, Png).map_err(error_trans) 111 | } 112 | 113 | pub fn alloc_tasks(this: Arc ,mut tasks: Vec) { 114 | let thread_num = this.options.thread_num; 115 | let divide = std::cmp::max((tasks.len() + thread_num - 1) / thread_num, 1); 116 | let mut i = 0; 117 | let mut ths = Vec::new(); 118 | let mut c = 0; 119 | while i < tasks.len() { 120 | let j = std::cmp::min(i + divide, tasks.len()); 121 | let slice = Vec::from(&tasks[i..j]); 122 | let that = this.clone(); 123 | let th = thread::Builder::new() 124 | .name(format!("work-{}", c)) 125 | .spawn(move|| { 126 | for task in &slice { 127 | if let Err(e) = that.render_one(task.src.as_path(), task.tgt.as_path(), &task.tile_id) { 128 | log::warn!("[{}] tile{:?} error: {}", thread::current().name().unwrap_or_default(), task.tile_id, e); 129 | } else { 130 | log::info!("[{}] tile{:?} finished", thread::current().name().unwrap_or_default(), task.tile_id); 131 | } 132 | } 133 | }).unwrap(); 134 | ths.push(th); 135 | i = j; 136 | c += 1; 137 | } 138 | tasks.clear(); 139 | for th in ths { 140 | th.join().unwrap(); 141 | } 142 | } 143 | } 144 | 145 | pub fn curdir() -> PathBuf { 146 | use std::env::current_exe; 147 | 148 | if let Ok(dir) = current_exe() { 149 | if let Some(path) = dir.parent() { 150 | PathBuf::from(path) 151 | } else { 152 | PathBuf::default() 153 | } 154 | } else { 155 | PathBuf::default() 156 | } 157 | } 158 | 159 | pub fn build_colormanager() -> BakedColorManager { 160 | 161 | use std::io::BufReader; 162 | 163 | let mut dir = curdir(); 164 | dir.push("resource"); 165 | let biome_color = { 166 | let r4 = File::open(dir.join("biome.json")).unwrap(); 167 | let r5 = File::open(dir.join("grass.png")).unwrap(); 168 | let r6 = File::open(dir.join("foliage.png")).unwrap(); 169 | de::build_biomecolor(r4, BufReader::new(r5), BufReader::new(r6)) 170 | }; 171 | { 172 | let r1 = File::open(dir.join("index.json")).unwrap(); 173 | let r2 = File::open(dir.join("colormap.png")).unwrap(); 174 | let r3 = File::open(dir.join("weightmap.png")).unwrap(); 175 | de::build_backedcolormanager(r1, BufReader::new(r2), BufReader::new(r3), biome_color) 176 | } 177 | } 178 | 179 | fn error_trans(e: image::ImageError) -> Box { 180 | use image::ImageError; 181 | 182 | match e { 183 | ImageError::IoError(err) => Box::new(err), 184 | ImageError::Decoding(err) => Box::new(err), 185 | ImageError::Encoding(err) => Box::new(err), 186 | ImageError::Parameter(err) => Box::new(err), 187 | ImageError::Limits(err) => Box::new(err), 188 | ImageError::Unsupported(err) => Box::new(err), 189 | } 190 | } -------------------------------------------------------------------------------- /src/color/biome.rs: -------------------------------------------------------------------------------- 1 | use image::Rgb; 2 | use image::RgbImage; 3 | use image::DynamicImage; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct Biome(pub usize); 7 | 8 | 9 | const SEA_LEVEL: i32 = 63; 10 | 11 | 12 | pub enum BiomeColorTOps { 13 | None, 14 | Fixed(Rgb), 15 | Average(Rgb) 16 | } 17 | 18 | impl BiomeColorTOps { 19 | 20 | pub fn modify(&self, color: &Rgb) -> Rgb { 21 | match self { 22 | BiomeColorTOps::None => { 23 | color.clone() 24 | }, 25 | BiomeColorTOps::Fixed(fixed) => { 26 | fixed.clone() 27 | }, 28 | BiomeColorTOps::Average(base) => { 29 | Rgb::from([ 30 | ((color[0] as u16 + base[0] as u16) / 2) as u8, 31 | ((color[1] as u16 + base[1] as u16) / 2) as u8, 32 | ((color[2] as u16 + base[2] as u16) / 2) as u8, 33 | ]) 34 | }, 35 | } 36 | } 37 | 38 | } 39 | 40 | 41 | fn clamp(value: f32, min: f32, max: f32) -> f32 { 42 | if value < min { 43 | return min 44 | } 45 | if value > max { 46 | return max 47 | } 48 | value 49 | } 50 | 51 | pub struct BiomeProps { 52 | temperature: f32, 53 | rainfall: f32, 54 | } 55 | 56 | impl BiomeProps { 57 | 58 | pub fn new(temperature:f32, rainfall: f32) -> Self { 59 | BiomeProps { 60 | temperature, 61 | rainfall, 62 | } 63 | } 64 | 65 | pub fn adjust(&self, height: i32) -> Self { 66 | let temperature = self.temperature - (height - SEA_LEVEL) as f32 / 600.0; 67 | let temperature = clamp(temperature, 0.0, 1.0); 68 | let rainfall = clamp(self.rainfall, 0.0, 1.0) * temperature; 69 | BiomeProps { 70 | temperature, 71 | rainfall 72 | } 73 | } 74 | } 75 | 76 | pub struct BiomeColor { 77 | 78 | biomes: Vec<(String, BiomeProps, Rgb, BiomeColorTOps, BiomeColorTOps)>, 79 | 80 | grass: RgbImage, 81 | 82 | foliage: RgbImage, 83 | 84 | } 85 | 86 | impl BiomeColor { 87 | 88 | #[inline] 89 | fn get<'a, T>(vec: &'a Vec, biome: &Biome) -> &'a T { 90 | if biome.0 < vec.len() { 91 | &vec[biome.0] 92 | } else { 93 | &vec[0] 94 | } 95 | } 96 | 97 | pub fn from_raw(biomes: Vec<(String, BiomeProps, Rgb, BiomeColorTOps, BiomeColorTOps)>, grass: RgbImage, foliage: RgbImage) -> Self { 98 | BiomeColor { 99 | biomes, 100 | grass, 101 | foliage, 102 | } 103 | } 104 | 105 | pub fn get_water(&self, biome: &Biome) -> Rgb { 106 | BiomeColor::get(&self.biomes, biome).2.clone() 107 | } 108 | 109 | pub fn get_grass(&self, biome: &Biome, height: i32) -> Rgb { 110 | let t = BiomeColor::get(&self.biomes, biome); 111 | let BiomeProps { temperature, rainfall } = t.1.adjust(height); 112 | let w = (self.grass.width() - 1) as f32; 113 | let h = (self.grass.height() - 1) as f32; 114 | let x = ((1.0 - temperature) * w).round() as u32; 115 | let y = ((1.0 - rainfall) * h).round() as u32; 116 | let c = self.grass.get_pixel(x, y); 117 | t.3.modify(c) 118 | } 119 | 120 | pub fn get_foliage(&self, biome: &Biome, height: i32) -> Rgb { 121 | let t = BiomeColor::get(&self.biomes, biome); 122 | let BiomeProps { temperature, rainfall } = t.1.adjust(height); 123 | let w = (self.foliage.width() - 1) as f32; 124 | let h = (self.foliage.height() - 1) as f32; 125 | let x = ((1.0 - temperature) * w).round() as u32; 126 | let y = ((1.0 - rainfall) * h).round() as u32; 127 | let c = self.foliage.get_pixel(x, y); 128 | t.4.modify(c) 129 | } 130 | } 131 | 132 | 133 | pub enum InnerColor { 134 | None, 135 | Water, 136 | Grass, 137 | Foliage, 138 | } 139 | 140 | impl From<&str> for InnerColor { 141 | 142 | fn from(value: &str) -> Self { 143 | match value { 144 | "minecraft:water" => Self::Water, 145 | "minecraft:grass_block" => Self::Grass, 146 | "minecraft:lily_pad" => Self::Grass, 147 | _ => { 148 | if value.ends_with("leaves") { 149 | return Self::Foliage; 150 | } 151 | Self::None 152 | } 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/color/blockstate.rs: -------------------------------------------------------------------------------- 1 | use std::collections::btree_map::BTreeMap as Map; 2 | use std::collections::btree_map::Values; 3 | use std::collections::btree_map::ValuesMut; 4 | use std::borrow::Borrow; 5 | 6 | 7 | /** 8 | * 9 | */ 10 | #[derive(Clone, Debug)] 11 | pub struct Expression { 12 | 13 | keys: Map, 14 | 15 | values: Map, 16 | 17 | mask: M, 18 | } 19 | 20 | impl Default for Expression { 21 | 22 | fn default() -> Self { 23 | Expression { 24 | keys: Map::default(), 25 | values: Map::default(), 26 | mask: 0, 27 | } 28 | } 29 | } 30 | 31 | impl Expression { 32 | 33 | pub fn insert<'a, I:Iterator>(&'a mut self, key: I, value: V) -> Option { 34 | let mut m = self.get_initial_link(); 35 | for k in key { 36 | self.insert_key(k, &mut m)?; 37 | } 38 | self.values.insert(m, value); 39 | Some(m) 40 | } 41 | 42 | pub fn insert_key(&mut self, k: &K, m: &mut usize) -> Option { 43 | if let Some(n) = self.keys.get(k) { 44 | *m |= *n; 45 | Some(*n) 46 | } else { 47 | if self.mask == std::usize::MAX { 48 | return None; 49 | } 50 | let n = self.mask + 1; 51 | self.mask = n | self.mask; 52 | self.keys.insert(k.clone(), n); 53 | *m |= n; 54 | Some(n) 55 | } 56 | } 57 | 58 | pub fn insert_value_unchecked(&mut self, m: usize, v: V) -> Option { 59 | self.values.insert(m, v) 60 | } 61 | } 62 | 63 | impl Expression { 64 | 65 | pub fn get_initial_link(&self) -> usize { 66 | 0 67 | } 68 | 69 | pub fn size(&self) -> (usize, usize) { 70 | (self.keys.len(), self.values.len()) 71 | } 72 | 73 | pub fn get<'a, Q: 'a, I>(&'a self, key: I, strict: bool) -> Option<&'a V> 74 | where 75 | Q: std::cmp::Ord, 76 | K: Borrow, 77 | I: Iterator, 78 | { 79 | if !strict && self.keys.len() == 0 { 80 | return self.values.get(&0); 81 | } 82 | let mut m = 0; 83 | let keys = &self.keys; 84 | for k in key { 85 | match keys.get(k) { 86 | Some(n) => { 87 | m |= n; 88 | }, 89 | None => { 90 | if strict { return None; } 91 | } 92 | } 93 | } 94 | self.values.get(&m) 95 | } 96 | 97 | pub fn get_mut<'a, Q: 'a, I>(&'a mut self, key: I, strict: bool) -> Option<&'a mut V> 98 | where 99 | Q: std::cmp::Ord, 100 | K: Borrow, 101 | I: Iterator, 102 | { 103 | if !strict && self.keys.len() == 0 { 104 | return self.values.get_mut(&0); 105 | } 106 | let mut m = 0; 107 | let keys = &self.keys; 108 | for k in key { 109 | match keys.get(k) { 110 | Some(n) => { 111 | m |= n; 112 | }, 113 | None => { 114 | if strict { return None; } 115 | } 116 | } 117 | } 118 | self.values.get_mut(&m) 119 | } 120 | 121 | pub fn all<'a>(&'a self) -> Values<'a, usize, V> { 122 | self.values.values() 123 | } 124 | 125 | pub fn all_mut<'a>(&'a mut self) -> ValuesMut<'a, usize, V> { 126 | self.values.values_mut() 127 | } 128 | 129 | pub fn transf_into(self, mut fk: FK, mut fv: FV) -> Result, E> 130 | where 131 | K2: std::cmp::Ord, 132 | FK: FnMut(K)->Result, 133 | FV: FnMut(V)->Result, 134 | { 135 | let mut keys = Map::new(); 136 | for (k, m) in self.keys { 137 | let k2 = fk(k)?; 138 | keys.insert(k2, m); 139 | } 140 | let mut values = Map::new(); 141 | for (m, v) in self.values { 142 | let v2 = fv(v)?; 143 | values.insert(m, v2); 144 | } 145 | Ok(Expression { 146 | keys, 147 | values, 148 | mask: self.mask 149 | }) 150 | } 151 | } 152 | 153 | 154 | /** 155 | * 156 | */ 157 | 158 | #[derive(Clone, Debug)] 159 | pub enum BlockState { 160 | Single(M), 161 | Variants(Expression), 162 | MultiPart(Expression, Vec), 163 | } 164 | 165 | impl BlockState { 166 | 167 | pub fn build_single(m: M) -> Self { 168 | Self::Single(m) 169 | } 170 | 171 | pub fn build_variants(keys: Map, values: Map) -> Self { 172 | let mut mask = 0; 173 | for (k, v) in &keys { 174 | mask |= v; 175 | } 176 | Self::Variants(Expression { keys, values, mask}) 177 | } 178 | 179 | pub fn build_multipart(keys: Map, values: Vec<(Vec, M)>) -> Self { 180 | let mut mask = 0; 181 | for (k, v) in &keys { 182 | mask |= v; 183 | } 184 | let mut masks = Vec::with_capacity(values.len()); 185 | let mut mapvalues = Map::new(); 186 | for (when, val) in values { 187 | let mut groupmask = 0; 188 | for k in when { 189 | mapvalues.insert(k, val.clone()); 190 | groupmask |= k; 191 | } 192 | masks.push(groupmask); 193 | } 194 | Self::MultiPart(Expression {keys, values: mapvalues, mask}, masks) 195 | } 196 | 197 | pub fn get<'a, Q: 'a + ?Sized, I>(&'a self, key: I) -> Vec 198 | where 199 | Q: std::cmp::Ord, 200 | K: Borrow, 201 | I: Iterator, 202 | { 203 | match self { 204 | Self::Single(model) => { 205 | vec![model.clone()] 206 | } 207 | Self::Variants(expr) => { 208 | let mut m = 0; 209 | for k in key { 210 | if let Some(n) = expr.keys.get(k) { 211 | m |= n; 212 | } 213 | } 214 | if let Some(model) = expr.values.get(&m) { 215 | vec![model.clone()] 216 | } else { 217 | Vec::new() 218 | } 219 | } 220 | Self::MultiPart(expr, group) => { 221 | let mut m = 0; 222 | for k in key { 223 | if let Some(n) = expr.keys.get(k) { 224 | m |= n; 225 | } 226 | } 227 | let mut parts = Vec::with_capacity(group.len()); 228 | for mask in group.iter() { 229 | let n = *mask & m; 230 | if let Some(model) = expr.values.get(&n) { 231 | parts.push(model.clone()) 232 | } 233 | } 234 | parts 235 | } 236 | } 237 | 238 | } 239 | } 240 | 241 | impl BlockState { 242 | 243 | pub fn start_group(&mut self) { 244 | match self { 245 | Self::MultiPart(expr, group) => { 246 | group.push(expr.get_initial_link()); 247 | }, 248 | _ => { 249 | panic!("invalid operation") 250 | } 251 | } 252 | } 253 | 254 | pub fn insert_group<'a, I:Iterator>(&'a mut self, key: I, value: M) -> Option { 255 | match self { 256 | Self::MultiPart(expr, group) => { 257 | let mut m = expr.get_initial_link(); 258 | for k in key { 259 | expr.insert_key(k, &mut m)?; 260 | } 261 | let mask = group.last_mut()?; 262 | expr.values.insert(m, value); 263 | *mask |= m; 264 | Some(m) 265 | }, 266 | Self::Variants(expr) => { 267 | let mut m = expr.get_initial_link(); 268 | for k in key { 269 | expr.insert_key(k, &mut m)?; 270 | } 271 | expr.values.insert(m, value); 272 | Some(m) 273 | } 274 | Self::Single(model) => { 275 | panic!("invalid operation") 276 | } 277 | } 278 | } 279 | 280 | pub fn try_simplify_variant(self) -> Self { 281 | match self { 282 | Self::Variants(mut expr) => { 283 | if expr.values.len() == 1 { 284 | if let Some(model) = expr.values.remove(&0) { 285 | return Self::Single(model) 286 | } 287 | } 288 | Self::Variants(expr) 289 | }, 290 | _ => { 291 | panic!("invalid operation") 292 | } 293 | } 294 | } 295 | } 296 | 297 | 298 | 299 | 300 | -------------------------------------------------------------------------------- /src/color/calculate.rs: -------------------------------------------------------------------------------- 1 | use image::Primitive; 2 | use image::Rgba; 3 | 4 | 5 | pub fn blend(bg: &mut Rgba, fg: &Rgba) { 6 | let r0 = bg[0] as u32; 7 | let g0 = bg[1] as u32; 8 | let b0 = bg[2] as u32; 9 | let a0 = bg[3] as u32; 10 | let r1 = fg[0] as u32; 11 | let g1 = fg[1] as u32; 12 | let b1 = fg[2] as u32; 13 | let a1 = fg[3] as u32; 14 | let ia1 = 255 - a1; 15 | let r = (r0 * ia1 + r1 * a1) / 255; 16 | let g = (g0 * ia1 + g1 * a1) / 255; 17 | let b = (b0 * ia1 + b1 * a1) / 255; 18 | let a = std::cmp::max(a0, a1); 19 | bg[0] = r as u8; 20 | bg[1] = g as u8; 21 | bg[2] = b as u8; 22 | bg[3] = a as u8; 23 | } -------------------------------------------------------------------------------- /src/color/de.rs: -------------------------------------------------------------------------------- 1 | use std::collections::btree_map::BTreeMap as Map; 2 | use std::collections::hash_map::HashMap; 3 | use std::io::BufRead; 4 | use std::io::Read; 5 | use std::io::Seek; 6 | use std::str::FromStr; 7 | 8 | use serde::de; 9 | use serde::de::Deserializer; 10 | use serde::de::MapAccess; 11 | use serde::de::Visitor; 12 | use serde::Deserialize; 13 | use serde::Serialize; 14 | use serde_json; 15 | 16 | use image::DynamicImage; 17 | use image::ImageFormat; 18 | /** 19 | * 20 | */ 21 | use image::Rgb; 22 | use image::Rgba; 23 | 24 | use super::biome::BiomeColor; 25 | use super::biome::BiomeColorTOps; 26 | use super::biome::BiomeProps; 27 | use super::BakedColorManager; 28 | 29 | fn u32_to_rgb(c: u32) -> Rgb { 30 | Rgb::from([ 31 | ((c >> 16) & 0xFF) as u8, 32 | ((c >> 8) & 0xFF) as u8, 33 | ((c >> 0) & 0xFF) as u8, 34 | ]) 35 | } 36 | 37 | #[derive(Deserialize)] 38 | enum BiomeColorTOpsRaw { 39 | Fixed(u32), 40 | Average(u32), 41 | } 42 | 43 | fn raw2ops(x: Option) -> BiomeColorTOps { 44 | if let Some(x) = x { 45 | match x { 46 | BiomeColorTOpsRaw::Fixed(c) => BiomeColorTOps::Fixed(u32_to_rgb(c)), 47 | BiomeColorTOpsRaw::Average(c) => BiomeColorTOps::Average(u32_to_rgb(c)), 48 | } 49 | } else { 50 | BiomeColorTOps::None 51 | } 52 | } 53 | 54 | #[derive(Deserialize)] 55 | struct BiomeTupleRaw { 56 | id: usize, 57 | name: String, 58 | temperature: f32, 59 | rainfall: f32, 60 | watercolor: u32, 61 | ops_grass: Option, 62 | ops_foliage: Option, 63 | } 64 | 65 | impl Into<(String, BiomeProps, Rgb, BiomeColorTOps, BiomeColorTOps)> for BiomeTupleRaw { 66 | fn into(self) -> (String, BiomeProps, Rgb, BiomeColorTOps, BiomeColorTOps) { 67 | ( 68 | self.name, 69 | BiomeProps::new(self.temperature, self.rainfall), 70 | u32_to_rgb(self.watercolor), 71 | raw2ops(self.ops_grass), 72 | raw2ops(self.ops_foliage), 73 | ) 74 | } 75 | } 76 | 77 | pub fn build_biomecolor( 78 | biome_data: R, 79 | grass_colormap: RI, 80 | foliage_colormap: RI, 81 | ) -> BiomeColor { 82 | let raws: Vec = serde_json::from_reader(biome_data).unwrap(); 83 | let biomes = raws 84 | .into_iter() 85 | .enumerate() 86 | .map(|(i, e)| { 87 | if i == e.id { 88 | e.into() 89 | } else { 90 | panic!("invalid biome data: {}", e.name) 91 | } 92 | }) 93 | .collect(); 94 | let grass = if let DynamicImage::ImageRgb8(img) = 95 | image::load(grass_colormap, ImageFormat::Png).unwrap() 96 | { 97 | img 98 | } else { 99 | panic!("invalid image format: grass"); 100 | }; 101 | let foliage = if let DynamicImage::ImageRgb8(img) = 102 | image::load(foliage_colormap, ImageFormat::Png).unwrap() 103 | { 104 | img 105 | } else { 106 | panic!("invalid image format: grass"); 107 | }; 108 | BiomeColor::from_raw(biomes, grass, foliage) 109 | } 110 | 111 | /** 112 | * 113 | */ 114 | use super::blockstate::BlockState; 115 | 116 | macro_rules! blockstate_deserialize { 117 | ($K:ty, $M:ty) => { 118 | // start code 119 | 120 | impl<'de> Deserialize<'de> for BlockState<$K, $M> { 121 | fn deserialize(deserializer: D) -> Result 122 | where 123 | D: Deserializer<'de>, 124 | { 125 | use std::fmt; 126 | 127 | const VARIANTS: &'static [&'static str] = &["single", "variants", "multipart"]; 128 | 129 | struct InnerVisitor; 130 | 131 | impl<'de> Visitor<'de> for InnerVisitor { 132 | type Value = BlockState<$K, $M>; 133 | 134 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 135 | formatter.write_str("BlockState enum") 136 | } 137 | 138 | fn visit_map(self, mut map: V) -> Result 139 | where 140 | V: MapAccess<'de>, 141 | { 142 | while let Some(key) = map.next_key::()? { 143 | match key.as_str() { 144 | "single" => { 145 | let v = map.next_value()?; 146 | return Ok(BlockState::build_single(v)); 147 | } 148 | "variants" => { 149 | let VariantRaw { keys, values } = map.next_value()?; 150 | return Ok(BlockState::build_variants(keys.0, values.0)); 151 | } 152 | _ => { 153 | let MultiPartRaw { keys, values } = map.next_value()?; 154 | let keys = keys.0; 155 | let values = 156 | values.into_iter().map(|e| (e.when, e.apply)).collect(); 157 | return Ok(BlockState::build_multipart(keys, values)); 158 | } 159 | } 160 | } 161 | Err(de::Error::unknown_field("", VARIANTS)) 162 | } 163 | } 164 | 165 | deserializer.deserialize_map(InnerVisitor) 166 | } 167 | } 168 | 169 | struct KMap(Map<$K, usize>); 170 | 171 | impl<'de> Deserialize<'de> for KMap { 172 | fn deserialize(deserializer: D) -> Result 173 | where 174 | D: Deserializer<'de>, 175 | { 176 | use std::fmt; 177 | 178 | use serde::de; 179 | 180 | struct InnerVisitor; 181 | 182 | impl<'de> Visitor<'de> for InnerVisitor { 183 | type Value = KMap; 184 | 185 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 186 | formatter.write_str("{\"key\":number}") 187 | } 188 | 189 | fn visit_map(self, mut map: V) -> Result 190 | where 191 | V: MapAccess<'de>, 192 | { 193 | let mut result = Map::new(); 194 | while let Some(sc) = map.next_key::()? { 195 | let s = sc.as_str(); 196 | let key = <$K>::from_str(s).map_err(|e| { 197 | de::Error::custom(format!("unable to parse key: {}", s)) 198 | })?; 199 | let value = map.next_value()?; 200 | result.insert(key, value); 201 | } 202 | Ok(KMap(result)) 203 | } 204 | } 205 | 206 | deserializer.deserialize_map(InnerVisitor) 207 | } 208 | } 209 | 210 | struct NMap(Map); 211 | 212 | impl<'de> Deserialize<'de> for NMap { 213 | fn deserialize(deserializer: D) -> Result 214 | where 215 | D: Deserializer<'de>, 216 | { 217 | use std::fmt; 218 | 219 | use serde::de; 220 | 221 | struct InnerVisitor; 222 | 223 | impl<'de> Visitor<'de> for InnerVisitor { 224 | type Value = NMap; 225 | 226 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 227 | formatter.write_str("{\"number\":value}") 228 | } 229 | 230 | fn visit_map(self, mut map: V) -> Result 231 | where 232 | V: MapAccess<'de>, 233 | { 234 | let mut result = Map::new(); 235 | while let Some(sc) = map.next_key::()? { 236 | let s = sc.as_str(); 237 | let key = usize::from_str(s).map_err(|e| { 238 | de::Error::custom(format!("unable to parse key: {}", s)) 239 | })?; 240 | let value = map.next_value()?; 241 | result.insert(key, value); 242 | } 243 | Ok(NMap(result)) 244 | } 245 | } 246 | 247 | deserializer.deserialize_map(InnerVisitor) 248 | } 249 | } 250 | 251 | #[derive(Deserialize)] 252 | struct VariantRaw { 253 | keys: KMap, 254 | values: NMap, 255 | } 256 | 257 | #[derive(Deserialize)] 258 | struct MultiPartElementRaw { 259 | when: Vec, 260 | apply: $M, 261 | } 262 | 263 | #[derive(Deserialize)] 264 | struct MultiPartRaw { 265 | keys: KMap, 266 | values: Vec, 267 | } 268 | 269 | // end code 270 | }; 271 | } 272 | 273 | /** 274 | * 275 | */ 276 | #[derive(PartialEq, Eq, PartialOrd, Ord)] 277 | pub struct KVPair { 278 | pub key: String, 279 | pub val: String, 280 | } 281 | 282 | impl std::fmt::Debug for KVPair { 283 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 284 | write!(f, "{}={}", self.key, self.val) 285 | } 286 | } 287 | 288 | impl FromStr for KVPair { 289 | type Err = std::io::Error; 290 | 291 | fn from_str(s: &str) -> Result { 292 | let i = s 293 | .find('=') 294 | .ok_or_else(|| std::io::Error::from(std::io::ErrorKind::InvalidData))?; 295 | let key = String::from(&s[..i]); 296 | let val = String::from(&s[i + 1..]); 297 | Ok(KVPair { key, val }) 298 | } 299 | } 300 | 301 | pub type BlockStateC = BlockState; 302 | 303 | blockstate_deserialize!(String, usize); 304 | 305 | #[derive(Deserialize)] 306 | struct IndexRaw { 307 | data: HashMap, 308 | } 309 | 310 | pub fn build_backedcolormanager( 311 | index_file: R, 312 | colormap_file: RI, 313 | weightmap_file: RI, 314 | biome_color: BiomeColor, 315 | ) -> BakedColorManager { 316 | let json: IndexRaw = serde_json::from_reader(index_file).unwrap(); 317 | let colormap = if let DynamicImage::ImageRgba8(img) = 318 | image::load(colormap_file, ImageFormat::Png).unwrap() 319 | { 320 | img 321 | } else { 322 | panic!("invalid image: colormap"); 323 | }; 324 | let weightmap = if let DynamicImage::ImageLuma8(img) = 325 | image::load(weightmap_file, ImageFormat::Png).unwrap() 326 | { 327 | img 328 | } else { 329 | panic!("invalid image: weightmap"); 330 | }; 331 | BakedColorManager::from_raw(json.data, colormap, weightmap, biome_color) 332 | } 333 | -------------------------------------------------------------------------------- /src/color/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod blockstate; 2 | pub mod biome; 3 | pub mod de; 4 | pub mod calculate; 5 | 6 | use std::iter::Empty; 7 | use std::collections::hash_map::HashMap; 8 | 9 | use image::Rgb; 10 | use image::Rgba; 11 | use image::Pixel; 12 | use image::GrayImage; 13 | use image::RgbaImage; 14 | 15 | use biome::Biome; 16 | use biome::BiomeColor; 17 | use biome::InnerColor; 18 | use de::BlockStateC; 19 | 20 | pub trait ColorManager { 21 | 22 | fn get_basic_color<'a, I: Iterator>(&'a self, block: &'a str, key: I, water_logged: bool) -> Rgba; 23 | 24 | fn get_modified_color(&self, basic: Rgba, inner_color: &InnerColor, height: i32, biome: &Biome, water_logged: bool) -> Rgba; 25 | 26 | } 27 | 28 | 29 | pub struct BakedColorManager { 30 | 31 | index: HashMap, 32 | 33 | colormap: Vec>, 34 | 35 | weightmap: Vec, 36 | 37 | biome_color: BiomeColor, 38 | 39 | water_basic: (Rgba, u16), 40 | 41 | } 42 | 43 | impl BakedColorManager { 44 | 45 | pub fn from_raw(index: HashMap, colormap: RgbaImage, weightmap: GrayImage, biome_color: BiomeColor) -> Self { 46 | let mut obj = BakedColorManager { 47 | index, 48 | colormap: colormap.pixels().map(Clone::clone).collect(), 49 | weightmap: weightmap.into_raw(), 50 | biome_color, 51 | water_basic: (Rgba::from([0, 0, 0, 0]), 0), 52 | }; 53 | if let Some(blockstate) = obj.index.get("minecraft:water") { 54 | let it: Empty<&str> = Empty::default(); 55 | if let Some(index) = blockstate.get(it).first() { 56 | let water_index = index.clone(); 57 | obj.water_basic = (obj.colormap[water_index], obj.weightmap[water_index] as u16); 58 | } 59 | } 60 | obj 61 | } 62 | } 63 | 64 | impl ColorManager for BakedColorManager { 65 | 66 | fn get_basic_color<'a, I: Iterator>(&'a self, block: &'a str, key: I, water_logged: bool) -> Rgba { 67 | if let Some(blockstate) = self.index.get(block) { 68 | let colors_index = blockstate.get(key); 69 | if colors_index.len() == 0 && !water_logged { 70 | return Rgba::from([0, 0, 0, 0]); 71 | } 72 | let mut colors_tuple: Vec<_> = colors_index.into_iter().map(|i| (self.colormap[i], self.weightmap[i] as u16)).collect(); 73 | colors_tuple.sort_by_key(|t| t.1); 74 | let max_w = if water_logged { 75 | 255 76 | } else { 77 | colors_tuple.last().unwrap().1 78 | }; 79 | if max_w == 0 { 80 | return Rgba::from([0, 0, 0, 0]); 81 | } 82 | let mut final_color = Rgba::from([0, 0, 0, 0]); 83 | for (mut color, w) in colors_tuple { 84 | if w * 4 < max_w { 85 | continue; 86 | } 87 | color[0] = (color[0] as u16 * w / max_w) as u8; 88 | color[1] = (color[1] as u16 * w / max_w) as u8; 89 | color[2] = (color[2] as u16 * w / max_w) as u8; 90 | color[3] = (color[3] as u16 * w / max_w) as u8; 91 | final_color.blend(&color) 92 | } 93 | final_color 94 | } else { 95 | Rgba::from([0, 0, 0, 0]) 96 | } 97 | } 98 | 99 | fn get_modified_color(&self, mut basic: Rgba, inner_color: &InnerColor, height: i32, biome: &Biome, water_logged: bool) -> Rgba { 100 | match inner_color { 101 | InnerColor::None => { 102 | if water_logged { 103 | let water_color = color_mul(self.water_basic.0, self.biome_color.get_water(biome)); 104 | basic.blend(&water_color) 105 | } 106 | basic 107 | }, 108 | InnerColor::Water => color_mul(basic, self.biome_color.get_water(biome)), 109 | InnerColor::Grass => color_mul(basic, self.biome_color.get_grass(biome, height)), 110 | InnerColor::Foliage => color_mul(basic, self.biome_color.get_foliage(biome, height)) 111 | } 112 | } 113 | 114 | } 115 | 116 | fn color_mul(mut a: Rgba, b: Rgb) -> Rgba { 117 | a[0] = (a[0] as u16 * b[0] as u16 / 255) as u8; 118 | a[1] = (a[1] as u16 * b[1] as u16 / 255) as u8; 119 | a[2] = (a[2] as u16 * b[2] as u16 / 255) as u8; 120 | a 121 | } -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | 2 | mod color; 3 | mod render; 4 | mod application; 5 | mod tilegen; 6 | 7 | #[cfg(feature = "service")] 8 | mod service; 9 | 10 | use std::env; 11 | use std::path::PathBuf; 12 | use std::sync::Arc; 13 | use std::time::Instant; 14 | use std::str::FromStr; 15 | 16 | use clap::App; 17 | use clap::Arg; 18 | use clap::SubCommand; 19 | 20 | const NAME: &'static str = env!("CARGO_PKG_NAME"); 21 | const DESCRIPTION: &'static str = env!("CARGO_PKG_DESCRIPTION"); 22 | const VERSION: &'static str = env!("CARGO_PKG_VERSION"); 23 | const AUTHORS: &'static str = env!("CARGO_PKG_AUTHORS"); 24 | 25 | 26 | const MAX_THREAD: usize = 16; 27 | 28 | fn main() { 29 | 30 | if let Err(_e) = env::var("RUST_LOG") { 31 | env::set_var("RUST_LOG", "info"); 32 | } 33 | env_logger::init(); 34 | 35 | let app = 36 | App::new(NAME) 37 | .about(DESCRIPTION) 38 | .version(VERSION) 39 | .author(AUTHORS); 40 | let app = app.subcommand( 41 | SubCommand::with_name("render") 42 | .arg( 43 | Arg::with_name("input_dir") 44 | .short("i") 45 | .long("input_dir") 46 | .help("input folder") 47 | .takes_value(true) 48 | .required(true) 49 | ) 50 | .arg( 51 | Arg::with_name("output_dir") 52 | .short("o") 53 | .long("output_dir") 54 | .help("output folder") 55 | .takes_value(true) 56 | .required(true) 57 | ) 58 | .arg( 59 | Arg::with_name("env_light") 60 | .long("env_lit") 61 | .help("environment light, from 0 to 15, default is 15") 62 | .takes_value(true) 63 | ) 64 | .arg( 65 | Arg::with_name("gamma") 66 | .long("gamma") 67 | .help("gamma for gamma correction, default is 1.0") 68 | .takes_value(true) 69 | ) 70 | .arg( 71 | Arg::with_name("thread") 72 | .short("t") 73 | .long("thread") 74 | .help("multi-thread: thread number") 75 | .takes_value(true) 76 | ) 77 | ); 78 | let app = app.subcommand( 79 | SubCommand::with_name("tile") 80 | .arg( 81 | Arg::with_name("input_dir") 82 | .short("i") 83 | .long("input_dir") 84 | .help("input folder") 85 | .takes_value(true) 86 | .required(true) 87 | ) 88 | .arg( 89 | Arg::with_name("output_dir") 90 | .short("o") 91 | .long("output_dir") 92 | .help("output folder") 93 | .takes_value(true) 94 | .required(true) 95 | ) 96 | .arg( 97 | Arg::with_name("filter") 98 | .long("filter") 99 | .help("filter used in scale, can be \"nearest\", \"triangle\", \"gaussian\", \"catmullrom\", \"lanczos3\"; default is \"nearest\"") 100 | .takes_value(true) 101 | ) 102 | .arg( 103 | Arg::with_name("path_mode") 104 | .long("path_mode") 105 | .help("generated path mode, can be \"layer+\", \"layer+:\", \"layer+:,\", \"layer-\", \"layer-:\", \"layer-:,\"") 106 | .takes_value(true) 107 | .required(true) 108 | ) 109 | .arg( 110 | Arg::with_name("use_multi_thread") 111 | .long("use_multi_thread") 112 | .help("if use multi-thread; fixed 4 thread") 113 | .takes_value(false) 114 | ) 115 | .arg( 116 | Arg::with_name("check_exist") 117 | .long("check_exist") 118 | .help("check if the same picture exist and then skip rewrite it") 119 | ) 120 | ); 121 | #[cfg(feature = "service")] 122 | let app = app.subcommand( 123 | SubCommand::with_name("renderserver") 124 | .arg( 125 | Arg::with_name("host") 126 | .long("host") 127 | .help("server bind host; default is \"0.0.0.0:8080\"") 128 | .takes_value(true) 129 | .required(false) 130 | ) 131 | .arg( 132 | Arg::with_name("max_tasks") 133 | .long("max_tasks") 134 | .help("max tasks number for server to run at the same time; default is 128") 135 | .takes_value(true) 136 | .required(false) 137 | ) 138 | .arg( 139 | Arg::with_name("workers") 140 | .long("workers") 141 | .help("worker thread num for server") 142 | .takes_value(true) 143 | .required(false) 144 | ) 145 | .arg( 146 | Arg::with_name("compress") 147 | .long("compress") 148 | .help("to enable compress for server") 149 | .required(false) 150 | ) 151 | .arg( 152 | Arg::with_name("tls") 153 | .long("tls") 154 | .help("use tls with specific cert.pem and key.pem; format: --tls path-to-cert.pem,path-to-key.pem") 155 | .takes_value(true) 156 | .required(false) 157 | ) 158 | ); 159 | 160 | let matches = app.get_matches(); 161 | let (name, args) = matches.subcommand(); 162 | let args = if let Some(v) = args { 163 | v 164 | } else { 165 | println!("{}", matches.usage()); 166 | return; 167 | }; 168 | match name { 169 | "render" => { 170 | 171 | let options = { 172 | let mut options = application::AppOptions::default(); 173 | options.set_input_folder(args.value_of("input_dir").unwrap()); 174 | options.set_output_folder(args.value_of("output_dir").unwrap()); 175 | options.ensure_output_folder().unwrap(); 176 | if let Some(gamma) = args.value_of("gamma") { 177 | if let Ok(gamma) = gamma.parse() { 178 | options.render_option_mut().set_gamma(gamma); 179 | } 180 | } 181 | if let Some(lit) = args.value_of("env_light") { 182 | if let Ok(lit) = lit.parse() { 183 | options.render_option_mut().set_env_light(lit); 184 | } 185 | } 186 | if let Some(thread) = args.value_of("thread") { 187 | if let Ok(thread) = thread.parse() { 188 | if thread <= MAX_THREAD { 189 | options.set_thread_num(thread); 190 | } 191 | } 192 | } 193 | options 194 | }; 195 | 196 | let app = Arc::new(application::Application::new(options)); 197 | let time = Instant::now(); 198 | let list = app.list_files(); 199 | application::Application::alloc_tasks(app, list); 200 | let time = Instant::now() - time; 201 | log::info!("> used {}ms", time.as_millis()); 202 | 203 | }, 204 | 205 | "tile" => { 206 | let options = { 207 | let input_folder = PathBuf::from(args.value_of("input_dir").unwrap()); 208 | let output_folder = PathBuf::from(args.value_of("output_dir").unwrap()); 209 | let path_mode = tilegen::PathMode::from_str(args.value_of("path_mode").unwrap()).unwrap(); 210 | let mut options = tilegen::TileGeneratorOptions::new(input_folder, output_folder, path_mode); 211 | if let Some(value) = args.value_of("filter") { 212 | options.set_filter(value); 213 | } 214 | if args.is_present("use_multi_thread") { 215 | options.set_multi_thread_mode(true); 216 | } 217 | if args.is_present("check_exist") { 218 | options.set_check_exist(true); 219 | } 220 | options 221 | }; 222 | let app = tilegen::TileGenerator::new(options); 223 | let time = Instant::now(); 224 | let list = app.list_files(); 225 | log::info!("> source: {} tiles", list.len()); 226 | app.generate_tile(list); 227 | let time = Instant::now() - time; 228 | log::info!("> used {}ms", time.as_millis()); 229 | } 230 | 231 | #[cfg(feature = "service")] 232 | "renderserver" => { 233 | let options = { 234 | let mut options = service::RenderServerOptions::default(); 235 | if let Some(host) = args.value_of("host") { 236 | options.set_host(host); 237 | } 238 | if let Some(max_tasks) = args.value_of("max_tasks") { 239 | if let Ok(num) = max_tasks.parse() { 240 | options.set_max_tasks(num); 241 | } 242 | } 243 | if let Some(workers) = args.value_of("workers") { 244 | if let Ok(num) = workers.parse() { 245 | options.set_workers(num); 246 | } 247 | } 248 | options.set_compress( 249 | args.is_present("compress") 250 | ); 251 | if let Some(tls) = args.value_of("tls") { 252 | let a: Vec<_> = tls.splitn(2, ',').collect(); 253 | if a.len() == 2 { 254 | options.set_tls(PathBuf::from(a[0]), PathBuf::from(a[1])); 255 | } 256 | } 257 | options 258 | }; 259 | service::RenderService::new(options).start(); 260 | } 261 | 262 | _ => { 263 | unimplemented!() 264 | } 265 | } 266 | } 267 | 268 | 269 | -------------------------------------------------------------------------------- /src/render/control.rs: -------------------------------------------------------------------------------- 1 | pub struct Control { 2 | pub version: u32, 3 | } 4 | 5 | impl Default for Control { 6 | 7 | fn default() -> Self { 8 | Control { 9 | version: 1 10 | } 11 | } 12 | } 13 | 14 | impl Control { 15 | 16 | pub fn modify_by(&mut self, line: &str) -> Result<(), usize> { 17 | let mut sp = line.splitn(2, ':'); 18 | let tuple = (sp.next().ok_or_else(|| line.len())?, sp.next().ok_or_else(|| line.len())?); 19 | match tuple.0 { 20 | "version" => { 21 | self.version = tuple.1.parse().map_err(|e| "version".len())?; 22 | }, 23 | _ => { 24 | 25 | } 26 | } 27 | Ok(()) 28 | } 29 | } -------------------------------------------------------------------------------- /src/render/data.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | pub const TILESIZE: (u32, u32) = (256, 256); 4 | 5 | pub trait View<'a> { 6 | type EN; 7 | type LN; 8 | 9 | fn element(&self, x: u32, z: u32) -> Self::EN; 10 | 11 | fn surface(&self, element: Self::EN) -> Self::LN; 12 | 13 | fn seafloor(&self, element: Self::EN) -> Self::LN; 14 | 15 | fn transparent(&self, element: Self::EN) -> Self::LN; 16 | 17 | fn foliage(&self, element: Self::EN) -> Self::LN; 18 | 19 | fn biome(&self, element: Self::EN) -> u16; 20 | 21 | fn height(&self, layer: Self::LN) -> u8; 22 | 23 | fn blockstate_id(&self, layer: Self::LN) -> u16; 24 | 25 | fn light(&self, layer: Self::LN) -> u8; 26 | 27 | fn skylight(&self, layer: Self::LN) -> u8; 28 | 29 | fn blocklight(&self, layer: Self::LN) -> u8; 30 | } 31 | 32 | 33 | #[derive(Clone, Copy)] 34 | pub struct LayerNode<'a> { 35 | ptr: *const u8, 36 | _life: PhantomData<&'a [u8]> 37 | } 38 | 39 | impl<'a> LayerNode<'a> { 40 | 41 | fn new(element: ElementNode<'a>, offset: usize) -> LayerNode<'a> { 42 | LayerNode { 43 | ptr: element.ptr.wrapping_offset(offset as isize), 44 | _life: element._life 45 | } 46 | } 47 | 48 | unsafe fn get(&self, offset: usize) -> u8 { 49 | *(self.ptr.wrapping_offset(offset as isize)) 50 | } 51 | } 52 | 53 | 54 | #[derive(Clone, Copy)] 55 | pub struct ElementNode<'a> { 56 | ptr: *const u8, 57 | _life: PhantomData<&'a [u8]> 58 | } 59 | 60 | impl<'a> ElementNode<'a> { 61 | 62 | fn new(raw: &'a [u8], offset: usize) -> ElementNode<'a> { 63 | ElementNode { 64 | ptr: raw.as_ptr().wrapping_offset(offset as isize), 65 | _life: PhantomData 66 | } 67 | } 68 | 69 | unsafe fn get(&self, offset: usize) -> u8 { 70 | *(self.ptr.wrapping_offset(offset as isize)) 71 | } 72 | } 73 | 74 | 75 | /** 76 | * 77 | */ 78 | 79 | pub struct V1TileView<'a> { 80 | raw: &'a[u8], 81 | } 82 | 83 | impl<'a> View<'a> for V1TileView<'a> { 84 | type LN = LayerNode<'a>; 85 | type EN = ElementNode<'a>; 86 | 87 | fn element(&self, x: u32, z: u32) -> Self::EN { 88 | let offset = ((x + z * TILESIZE.0) * 18) as usize; 89 | ElementNode::new(self.raw, offset) 90 | } 91 | 92 | fn surface(&self, element: Self::EN) -> Self::LN { 93 | LayerNode::new(element, 0) 94 | } 95 | 96 | fn seafloor(&self, element: Self::EN) -> Self::LN { 97 | LayerNode::new(element, 4) 98 | } 99 | 100 | fn transparent(&self, element: Self::EN) -> Self::LN { 101 | LayerNode::new(element, 8) 102 | } 103 | 104 | fn foliage(&self, element: Self::EN) -> Self::LN { 105 | LayerNode::new(element, 12) 106 | } 107 | 108 | fn biome(&self, element: Self::EN) -> u16 { 109 | unsafe { 110 | ((element.get(16) as u16) << 8) | (element.get(17) as u16) 111 | } 112 | } 113 | 114 | fn height(&self, layer: Self::LN) -> u8 { 115 | unsafe { 116 | layer.get(0) 117 | } 118 | } 119 | 120 | fn blockstate_id(&self, layer: Self::LN) -> u16 { 121 | unsafe { 122 | ((layer.get(1) as u16) << 8) | (layer.get(2) as u16) 123 | } 124 | } 125 | 126 | fn light(&self, layer: Self::LN) -> u8 { 127 | unsafe { 128 | layer.get(3) 129 | } 130 | } 131 | 132 | fn skylight(&self, layer: Self::LN) -> u8 { 133 | (self.light(layer) & 0xF0) >> 4 134 | } 135 | 136 | fn blocklight(&self, layer: Self::LN) -> u8 { 137 | self.light(layer) & 0x0F 138 | } 139 | } 140 | 141 | impl<'a> V1TileView<'a> { 142 | 143 | pub fn bind(raw: &'a [u8]) -> V1TileView<'a> { 144 | let sz = (TILESIZE.0 * TILESIZE.1 * 18) as usize; 145 | if raw.len() < sz { 146 | panic!("index out of bounds: {} > {}", sz - 1, raw.len()); 147 | } 148 | V1TileView { 149 | raw 150 | } 151 | } 152 | } 153 | 154 | 155 | /** 156 | * 157 | */ 158 | 159 | pub struct V2TileView<'a> { 160 | raw: &'a[u8], 161 | } 162 | 163 | impl<'a> View<'a> for V2TileView<'a> { 164 | type LN = LayerNode<'a>; 165 | type EN = ElementNode<'a>; 166 | 167 | fn element(&self, x: u32, z: u32) -> Self::EN { 168 | let offset = ((x + z * TILESIZE.0) * 1) as usize; 169 | ElementNode::new(self.raw, offset) 170 | } 171 | 172 | fn surface(&self, element: Self::EN) -> Self::LN { 173 | LayerNode::new(element, 0 * (TILESIZE.1 * TILESIZE.0) as usize) 174 | } 175 | 176 | fn seafloor(&self, element: Self::EN) -> Self::LN { 177 | LayerNode::new(element, 4 * (TILESIZE.1 * TILESIZE.0) as usize) 178 | } 179 | 180 | fn transparent(&self, element: Self::EN) -> Self::LN { 181 | LayerNode::new(element, 8 * (TILESIZE.1 * TILESIZE.0) as usize) 182 | } 183 | 184 | fn foliage(&self, element: Self::EN) -> Self::LN { 185 | LayerNode::new(element, 12 * (TILESIZE.1 * TILESIZE.0) as usize) 186 | } 187 | 188 | fn biome(&self, element: Self::EN) -> u16 { 189 | let step = (TILESIZE.1 * TILESIZE.0) as usize; 190 | unsafe { 191 | ((element.get(16 * step) as u16) << 8) | (element.get(17 * step) as u16) 192 | } 193 | } 194 | 195 | fn height(&self, layer: Self::LN) -> u8 { 196 | unsafe { 197 | layer.get(0 * (TILESIZE.1 * TILESIZE.0) as usize) 198 | } 199 | } 200 | 201 | fn blockstate_id(&self, layer: Self::LN) -> u16 { 202 | let step = (TILESIZE.1 * TILESIZE.0) as usize; 203 | unsafe { 204 | ((layer.get(1 * step) as u16) << 8) | (layer.get(2 * step) as u16) 205 | } 206 | } 207 | 208 | fn light(&self, layer: Self::LN) -> u8 { 209 | unsafe { 210 | layer.get(3 * (TILESIZE.1 * TILESIZE.0) as usize) 211 | } 212 | } 213 | 214 | fn skylight(&self, layer: Self::LN) -> u8 { 215 | (self.light(layer) & 0xF0) >> 4 216 | } 217 | 218 | fn blocklight(&self, layer: Self::LN) -> u8 { 219 | self.light(layer) & 0x0F 220 | } 221 | } 222 | 223 | impl<'a> V2TileView<'a> { 224 | 225 | pub fn bind(raw: &'a [u8]) -> V2TileView<'a> { 226 | let sz = (TILESIZE.0 * TILESIZE.1 * 18) as usize; 227 | if raw.len() < sz { 228 | panic!("index out of bounds: {} > {}", sz - 1, raw.len()); 229 | } 230 | V2TileView { 231 | raw 232 | } 233 | } 234 | } -------------------------------------------------------------------------------- /src/render/key.rs: -------------------------------------------------------------------------------- 1 | use std::str::Split; 2 | use std::convert::TryFrom; 3 | 4 | use crate::color::biome::InnerColor; 5 | 6 | 7 | pub struct BlockProps { 8 | 9 | pub air: bool, 10 | 11 | pub water: bool, 12 | 13 | pub waterlogged: bool, 14 | 15 | pub biome_color: InnerColor, 16 | } 17 | 18 | impl BlockProps { 19 | 20 | pub fn new() -> Self { 21 | BlockProps { 22 | air: true, 23 | water: false, 24 | waterlogged: false, 25 | biome_color: InnerColor::None, 26 | } 27 | } 28 | 29 | pub fn new_from<'a, I: Iterator>(name: &'a str, state: I) -> Self { 30 | let mut waterlogged = false; 31 | for s in state { 32 | let mut it = s.split('='); 33 | if it.next() == Some("waterlogged") { 34 | if it.next() == Some("true") { 35 | waterlogged = true; 36 | } 37 | } 38 | } 39 | BlockProps { 40 | air: name == "minecraft:air", 41 | water: name == "minecraft:water", 42 | waterlogged, 43 | biome_color: InnerColor::from(name) 44 | } 45 | } 46 | } 47 | 48 | #[derive(Debug)] 49 | pub struct KeyLine<'a> { 50 | pub id: usize, 51 | pub name: &'a str, 52 | pub state: Option<&'a str>, 53 | } 54 | 55 | impl<'a> TryFrom<&'a str> for KeyLine<'a> { 56 | type Error = usize; 57 | 58 | fn try_from(value: &'a str) -> Result { 59 | let mut state = None; 60 | 61 | let mut p = value; 62 | let mut pos = 0; 63 | 64 | let i = p.find(' ').ok_or(pos)?; 65 | let id = p[0..i].parse().map_err(|e| pos)?; 66 | p = &p[i+1..]; 67 | pos += i + 1; 68 | 69 | let i = p.find('{').ok_or(pos)?; 70 | let s = &p[0..i]; 71 | if s != "Block" { 72 | return Err(pos); 73 | } 74 | p = &p[i+1..]; 75 | pos += i + 1; 76 | 77 | let i = p.find('}').ok_or(pos)?; 78 | let name = &p[0..i]; 79 | p = &p[i+1..]; 80 | pos += i + 1; 81 | 82 | if p.starts_with('[') { 83 | p = &p[1..]; 84 | pos += 1; 85 | let i = p.find(']').ok_or(pos)?; 86 | state = Some(&p[0..i]); 87 | } 88 | 89 | Ok(KeyLine { 90 | id, 91 | name, 92 | state 93 | }) 94 | } 95 | } 96 | 97 | 98 | pub struct SplitIter<'a>(Option>); 99 | 100 | impl<'a> From> for SplitIter<'a> { 101 | 102 | fn from(value: Option<&'a str>) -> Self { 103 | SplitIter(value.map(|s| s.split(','))) 104 | } 105 | } 106 | 107 | impl<'a> Iterator for SplitIter<'a> { 108 | type Item = &'a str; 109 | 110 | fn next(&mut self) -> Option { 111 | if let Some(it) = &mut self.0 { 112 | it.next() 113 | } else { 114 | None 115 | } 116 | } 117 | } -------------------------------------------------------------------------------- /src/render/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod data; 2 | pub mod key; 3 | pub mod tile; 4 | pub mod control; 5 | 6 | use image::Pixel; 7 | use image::Rgba; 8 | use image::RgbaImage; 9 | 10 | use crate::color::biome::Biome; 11 | use crate::color::ColorManager; 12 | use crate::color::BakedColorManager; 13 | use data::TILESIZE; 14 | use data::View; 15 | use tile::Tile; 16 | 17 | 18 | pub type GEResult = Result>; 19 | 20 | pub struct RenderOptions { 21 | gamma: f32, 22 | env_light: u8, 23 | } 24 | 25 | impl Default for RenderOptions { 26 | 27 | fn default() -> Self { 28 | RenderOptions { 29 | gamma: 1.0, 30 | env_light: 15, 31 | } 32 | } 33 | } 34 | 35 | impl RenderOptions { 36 | 37 | pub fn set_gamma(&mut self, gamma: f32) { 38 | if gamma > 0.0 { 39 | self.gamma = gamma; 40 | } 41 | } 42 | 43 | pub fn set_env_light(&mut self, light: u8) { 44 | self.env_light = std::cmp::min(light, 15); 45 | } 46 | } 47 | 48 | 49 | pub fn gamma_correction(c: &mut Rgba, gamma: f32) { 50 | let r = c[0] as f32 / 255.0; 51 | let g = c[1] as f32 / 255.0; 52 | let b = c[2] as f32 / 255.0; 53 | let r = r.powf(1.0 / gamma); 54 | let g = g.powf(1.0 / gamma); 55 | let b = b.powf(1.0 / gamma); 56 | c[0] = (r * 255.0) as u8; 57 | c[1] = (g * 255.0) as u8; 58 | c[2] = (b * 255.0) as u8; 59 | } 60 | 61 | pub fn light_modify(c: &mut Rgba, light: u8) { 62 | let r = c[0] as u16; 63 | let g = c[1] as u16; 64 | let b = c[2] as u16; 65 | let light = (light & 0x0F) as u16; 66 | let r = r * light / 15; 67 | let g = g * light / 15; 68 | let b = b * light / 15; 69 | c[0] = r as u8; 70 | c[1] = g as u8; 71 | c[2] = b as u8; 72 | } 73 | 74 | 75 | pub fn render(tile: Tile, mgr: &BakedColorManager, options: &RenderOptions) -> RgbaImage { 76 | let mut panel = RgbaImage::new(TILESIZE.0, TILESIZE.1); 77 | let boxed_view = tile.view(); 78 | let view = boxed_view.as_ref(); 79 | for x in 0 .. TILESIZE.0 { 80 | for z in 0 .. TILESIZE.1 { 81 | 82 | let element = view.element(x, z); 83 | let biome = Biome(view.biome(element) as usize); 84 | let mut surface = { 85 | let layer = view.surface(element); 86 | if view.height(layer) > 0 { 87 | let (c, props) = tile.get_color(view.blockstate_id(layer)); 88 | let mut c = mgr.get_modified_color(c.clone(), &props.biome_color, view.height(layer) as i32, &biome, props.waterlogged); 89 | let light = std::cmp::max(view.blocklight(layer), options.env_light); 90 | light_modify(&mut c, light); 91 | (c, view.height(layer)) 92 | } else { 93 | (Rgba::from([0, 0, 0, 0]), 0) 94 | } 95 | }; 96 | let color = if surface.1 > 0 { 97 | let mut seafloor = { 98 | let layer = view.seafloor(element); 99 | if view.height(layer) > 0 { 100 | let (c, props) = tile.get_color(view.blockstate_id(layer)); 101 | let mut c = mgr.get_modified_color(c.clone(), &props.biome_color, view.height(layer) as i32, &biome, props.waterlogged); 102 | let light = std::cmp::max(view.blocklight(layer), options.env_light); 103 | light_modify(&mut c, light); 104 | (c, view.height(layer)) 105 | } else { 106 | (Rgba::from([0, 0, 0, 0]), 0) 107 | } 108 | }; 109 | let mut transparent = { 110 | let layer = view.transparent(element); 111 | if view.height(layer) > 0 { 112 | let (c, props) = tile.get_color(view.blockstate_id(layer)); 113 | let mut c = mgr.get_modified_color(c.clone(), &props.biome_color, view.height(layer) as i32, &biome, props.waterlogged); 114 | let light = std::cmp::max(view.blocklight(layer), options.env_light); 115 | light_modify(&mut c, light); 116 | (c, view.height(layer)) 117 | } else { 118 | (Rgba::from([0, 0, 0, 0]), 0) 119 | } 120 | }; 121 | let mut foliage = { 122 | let layer = view.foliage(element); 123 | if view.height(layer) > 0 { 124 | let (c, props) = tile.get_color(view.blockstate_id(layer)); 125 | let mut c = mgr.get_modified_color(c.clone(), &props.biome_color, view.height(layer) as i32, &biome, props.waterlogged); 126 | let light = std::cmp::max(view.blocklight(layer), options.env_light); 127 | light_modify(&mut c, light); 128 | (c, view.height(layer)) 129 | } else { 130 | (Rgba::from([0, 0, 0, 0]), 0) 131 | } 132 | }; 133 | if options.gamma != 1.0 { 134 | gamma_correction(&mut surface.0, options.gamma); 135 | gamma_correction(&mut seafloor.0, options.gamma); 136 | gamma_correction(&mut transparent.0, options.gamma); 137 | gamma_correction(&mut foliage.0, options.gamma); 138 | } 139 | let mut color = if seafloor.1 > 0 { 140 | let mut color = seafloor.0; 141 | if foliage.1 > 0 && foliage.1 <= surface.1 { 142 | blend(&mut color, &foliage.0); 143 | } 144 | if transparent.1 > 0 && transparent.1 <= surface.1 { 145 | blend(&mut color, &transparent.0); 146 | } 147 | blend(&mut color, &surface.0); 148 | color 149 | } else { 150 | surface.0 151 | }; 152 | if foliage.1 > 0 && foliage.1 > surface.1 { 153 | blend(&mut color, &foliage.0); 154 | } 155 | if transparent.1 > 0 && transparent.1 > surface.1 { 156 | blend(&mut color, &transparent.0); 157 | } 158 | color 159 | } else { 160 | Rgba::from([0, 0, 0, 0]) 161 | }; 162 | 163 | panel.put_pixel(x, z, color); 164 | } 165 | } 166 | panel 167 | } 168 | 169 | fn blend(bg: &mut P, fg: &P) { 170 | bg.blend(fg); 171 | } -------------------------------------------------------------------------------- /src/render/tile.rs: -------------------------------------------------------------------------------- 1 | use std::io::Read; 2 | use std::io::Seek; 3 | use std::convert::TryFrom; 4 | 5 | use log; 6 | use zip::ZipArchive; 7 | use image::Rgba; 8 | 9 | use crate::color::ColorManager; 10 | use crate::color::BakedColorManager; 11 | use super::data::TILESIZE; 12 | use super::data::View; 13 | use super::data::V1TileView; 14 | use super::data::V2TileView; 15 | use super::data::ElementNode; 16 | use super::data::LayerNode; 17 | use super::key::BlockProps; 18 | use super::key::KeyLine; 19 | use super::key::SplitIter; 20 | use super::control::Control; 21 | 22 | 23 | pub type GEResult = Result>; 24 | 25 | pub struct Tile { 26 | 27 | id: (i32, i32), 28 | 29 | data: Vec, 30 | 31 | key: Vec<(Rgba, BlockProps)>, 32 | 33 | control: Control, 34 | } 35 | 36 | impl Tile { 37 | 38 | pub fn load(reader: R, id: (i32, i32), mgr: &BakedColorManager) -> GEResult { 39 | let mut zip = ZipArchive::new(reader).map_err(Box::new)?; 40 | 41 | let mut data = Vec::new(); 42 | let n = zip.by_name("data").map_err(Box::new)?.read_to_end(&mut data).map_err(Box::new)?; 43 | if n != TILESIZE.0 as usize * TILESIZE.1 as usize * 18 { 44 | return Err(Box::new(std::io::Error::new(std::io::ErrorKind::InvalidData, "data"))) 45 | } 46 | 47 | let mut key = Vec::new(); 48 | let mut key_string = String::new(); 49 | let n = zip.by_name("key").map_err(Box::new)?.read_to_string(&mut key_string).map_err(Box::new)?; 50 | for line in key_string.lines() { 51 | match KeyLine::try_from(line) { 52 | Ok(k) => { 53 | let props = BlockProps::new_from(k.name, SplitIter::from(k.state)); 54 | let model = mgr.get_basic_color(k.name, SplitIter::from(k.state), props.waterlogged); 55 | 56 | key.push((model, props)); 57 | }, 58 | Err(e) => { 59 | log::warn!("parse error: `{}` @{}", line, e); //TODO: log 60 | key.push((Rgba::from([0,0,0,0]), BlockProps::new())) 61 | } 62 | } 63 | } 64 | 65 | let mut control = Control::default(); 66 | if let Ok(mut ifile) = zip.by_name("control") { 67 | let mut s = String::new(); 68 | if let Ok(n) = ifile.read_to_string(&mut s) { 69 | for line in s.lines() { 70 | if let Err(e) = control.modify_by(line) { 71 | log::warn!("parse error: `{}` @{}", line, e); 72 | break; 73 | } 74 | } 75 | } 76 | } 77 | 78 | Ok(Tile { 79 | id, 80 | data, 81 | key, 82 | control 83 | }) 84 | } 85 | 86 | pub fn view<'a>(&'a self) -> Box, LN=LayerNode<'a>> + 'a> { 87 | match self.control.version { 88 | 1 => Box::new(V1TileView::bind(self.data.as_slice())), 89 | 2 => Box::new(V2TileView::bind(self.data.as_slice())), 90 | _ => { 91 | log::warn!("unexpected control{{version:{}}} , use `1`", self.control.version); 92 | Box::new(V1TileView::bind(self.data.as_slice())) 93 | } 94 | } 95 | } 96 | 97 | pub fn get_color(&self, id: u16) -> &(Rgba, BlockProps) { 98 | &self.key[(id - 1) as usize] 99 | } 100 | } -------------------------------------------------------------------------------- /src/service.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::io; 3 | use std::io::Cursor; 4 | use std::io::BufReader; 5 | use std::fs::File; 6 | use std::sync::Arc; 7 | use std::sync::atomic::Ordering; 8 | use std::sync::atomic::AtomicUsize; 9 | 10 | use bytes::Bytes; 11 | use bytes::BytesMut; 12 | use bytes::BufMut; 13 | use actix_rt::System; 14 | use actix_web::middleware; 15 | use actix_web::web; 16 | use actix_web::HttpServer; 17 | use actix_web::App; 18 | use actix_web::HttpResponse; 19 | use actix_web::Error as ActixError; 20 | use actix_web::http::header; 21 | use actix_files::Files; 22 | use actix_multipart::Multipart; 23 | use futures::StreamExt; 24 | use futures::TryStreamExt; 25 | use serde::Deserialize; 26 | use serde::de::Deserializer; 27 | use serde::de::Visitor; 28 | use serde::de::MapAccess; 29 | use rustls::ServerConfig; 30 | use rustls::NoClientAuth; 31 | use rustls::internal::pemfile; 32 | 33 | use super::render::RenderOptions; 34 | use super::render; 35 | use super::render::tile::Tile; 36 | use super::color::BakedColorManager; 37 | use super::application; 38 | 39 | 40 | 41 | const MAX_TILE_SIZE: usize = 256 * 1024; 42 | 43 | #[derive(Clone)] 44 | pub struct RenderServerOptions { 45 | host: String, 46 | workers: usize, 47 | max_tasks: usize, 48 | compress: bool, 49 | tls: Option<(PathBuf, PathBuf)>, 50 | } 51 | 52 | impl Default for RenderServerOptions { 53 | 54 | fn default() -> Self { 55 | RenderServerOptions { 56 | host: String::from("0.0.0.0:8080"), 57 | workers: num_cpus::get(), 58 | max_tasks: 128, 59 | compress: false, 60 | tls: None 61 | } 62 | } 63 | } 64 | 65 | impl RenderServerOptions { 66 | 67 | pub fn set_host(&mut self, host: &str) { 68 | self.host = String::from(host); 69 | } 70 | 71 | pub fn set_workers(&mut self, num: usize) { 72 | self.workers = num; 73 | } 74 | 75 | pub fn set_max_tasks(&mut self, num: usize) { 76 | self.max_tasks = num; 77 | } 78 | 79 | pub fn set_compress(&mut self, compress: bool) { 80 | self.compress = compress; 81 | } 82 | 83 | pub fn set_tls(&mut self, cert_file: PathBuf, key_file: PathBuf) { 84 | self.tls = Some((cert_file, key_file)); 85 | } 86 | } 87 | 88 | 89 | pub struct RenderService { 90 | options: RenderServerOptions, 91 | colormgr: Arc, 92 | working: AtomicUsize, 93 | } 94 | 95 | impl RenderService { 96 | 97 | pub fn new(options: RenderServerOptions) -> Self { 98 | RenderService { 99 | colormgr: Arc::new(application::build_colormanager()), 100 | working: AtomicUsize::new(0), 101 | options 102 | } 103 | } 104 | 105 | pub fn start(self) { 106 | System::builder() 107 | .name("RenderService") 108 | .build() 109 | .block_on(run_service(self)) 110 | .unwrap(); 111 | } 112 | 113 | } 114 | 115 | 116 | impl<'de> Deserialize<'de> for RenderOptions { 117 | 118 | fn deserialize(deserializer: D) -> Result 119 | where 120 | D: Deserializer<'de>, 121 | { 122 | use std::fmt; 123 | use serde::de; 124 | 125 | struct InnerVisitor; 126 | 127 | impl<'de> Visitor<'de> for InnerVisitor { 128 | type Value = RenderOptions; 129 | 130 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 131 | formatter.write_str("struct RenderOptions") 132 | } 133 | 134 | fn visit_map(self, mut map: V) -> Result 135 | where 136 | V: MapAccess<'de>, 137 | { 138 | let mut res = RenderOptions::default(); 139 | while let Some(key) = map.next_key::()? { 140 | match key.as_str() { 141 | "gamma" => { 142 | res.set_gamma(map.next_value()?) 143 | }, 144 | "light" => { 145 | res.set_env_light(map.next_value()?) 146 | }, 147 | _ => { 148 | return Err(de::Error::unknown_field(key.as_str(), FIELDS)); 149 | } 150 | } 151 | } 152 | Ok(res) 153 | } 154 | } 155 | 156 | const FIELDS: &'static [&'static str] = &["light", "gamma"]; 157 | deserializer.deserialize_struct("RenderOptions", FIELDS, InnerVisitor) 158 | } 159 | } 160 | 161 | 162 | async fn run_service(service: RenderService) -> io::Result<()> { 163 | 164 | const ACTIX_LOG_FORMAT: &'static str = "%a \"%r\" %s \"%{User-Agent}i\" %D"; 165 | 166 | let options = service.options.clone(); 167 | let service = web::Data::new(service); 168 | 169 | let mut webfileroot = application::curdir(); 170 | webfileroot.push("web"); 171 | 172 | let payloadcfg = web::PayloadConfig::new(MAX_TILE_SIZE * 3 / 2); 173 | 174 | let tls_cfg = { 175 | if let Some((cert_file, key_file)) = options.tls { 176 | let mut config = ServerConfig::new(NoClientAuth::new()); 177 | let cert_file = &mut BufReader::new(File::open(cert_file.as_path())?); 178 | let key_file = &mut BufReader::new(File::open(key_file.as_path())?); 179 | let cert_chain = pemfile::certs(cert_file).map_err(|_e| io::Error::from(io::ErrorKind::InvalidData))?; 180 | let mut keys = pemfile::rsa_private_keys(key_file).map_err(|_e| io::Error::from(io::ErrorKind::InvalidData))?; 181 | config.set_single_cert(cert_chain, keys.remove(0)).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; 182 | Some(config) 183 | } else { 184 | None 185 | } 186 | }; 187 | 188 | if options.compress { 189 | let server = HttpServer::new(move || { 190 | App::new() 191 | .wrap(middleware::Logger::new(ACTIX_LOG_FORMAT)) 192 | .wrap(middleware::Compress::default()) 193 | .app_data(payloadcfg.clone()) 194 | .app_data(service.clone()) 195 | .service( 196 | web::resource("/render") 197 | .route(web::post().to(render)) 198 | ) 199 | .service( 200 | Files::new("/web", webfileroot.as_path()).index_file("index.html") 201 | ) 202 | }) 203 | .workers(options.workers); 204 | if let Some(tls_cfg) = tls_cfg { 205 | server 206 | .bind_rustls(options.host, tls_cfg)? 207 | .run() 208 | .await 209 | } else { 210 | server 211 | .bind(options.host)? 212 | .run() 213 | .await 214 | } 215 | } else { 216 | let server = HttpServer::new(move || { 217 | App::new() 218 | .wrap(middleware::Logger::new(ACTIX_LOG_FORMAT)) 219 | .app_data(payloadcfg.clone()) 220 | .app_data(webfileroot.clone()) 221 | .app_data(service.clone()) 222 | .service( 223 | web::resource("/render") 224 | .route(web::post().to(render)) 225 | ) 226 | .service( 227 | Files::new("/static", webfileroot.as_path()).index_file("index.html") 228 | ) 229 | }) 230 | .workers(options.workers); 231 | if let Some(tls_cfg) = tls_cfg { 232 | server 233 | .bind_rustls(options.host, tls_cfg)? 234 | .run() 235 | .await 236 | } else { 237 | server 238 | .bind(options.host)? 239 | .run() 240 | .await 241 | } 242 | } 243 | } 244 | 245 | 246 | async fn render(s: web::Data, query: web::Query, mut payload: Multipart) -> Result { 247 | 248 | struct StringifyError(String); 249 | 250 | impl std::fmt::Debug for StringifyError { 251 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 252 | f.write_str(self.0.as_str()) 253 | } 254 | } 255 | 256 | const EXT: &'static str = ".zip"; 257 | 258 | let parse_tile_id = |filename: &str| -> Option<(i32, i32)> { 259 | if filename.ends_with(EXT) { 260 | let filename = &filename[0 .. filename.len() - EXT.len()]; 261 | let mut sp = filename.splitn(2, ','); 262 | let x: i32 = sp.next()?.parse().ok()?; 263 | let z: i32 = sp.next()?.parse().ok()?; 264 | Some((x, z)) 265 | } else { 266 | None 267 | } 268 | }; 269 | 270 | if s.working.load(Ordering::SeqCst) >= s.options.max_tasks { 271 | return Ok(HttpResponse::TooManyRequests().into()) 272 | } 273 | 274 | let render_options = query.into_inner(); 275 | if let Ok(Some(mut field)) = payload.try_next().await { 276 | let content_type = field.content_disposition().ok_or_else(|| actix_web::error::ParseError::Incomplete)?; 277 | 278 | s.working.fetch_add(1, Ordering::SeqCst); 279 | 280 | let mut tile_id = (0, 0); 281 | let mut some_tile_id = None; 282 | if let Some(filename0) = content_type.get_filename() { 283 | some_tile_id = parse_tile_id(filename0); 284 | if let Some(v) = &some_tile_id { 285 | tile_id = v.clone(); 286 | } 287 | } 288 | let mut buf = BytesMut::with_capacity(MAX_TILE_SIZE); 289 | while let Some(chunk) = field.next().await { 290 | let data = chunk.unwrap(); 291 | if data.len() + buf.len() > MAX_TILE_SIZE { 292 | s.working.fetch_sub(1, Ordering::SeqCst); 293 | return Ok( 294 | HttpResponse::PayloadTooLarge() 295 | .into() 296 | ); 297 | } 298 | buf.put(data); 299 | } 300 | let mgr = s.colormgr.clone(); 301 | let r = web::block(move || -> Result { 302 | let ifile = Cursor::new(buf); 303 | let tile = Tile::load(ifile, tile_id, &mgr).map_err(|e| e.to_string())?; 304 | let pic = render::render(tile, &mgr, &render_options); 305 | let mut ofile = Vec::with_capacity((pic.width() * pic.height() * 4 / 3) as usize); 306 | image::DynamicImage::ImageRgba8(pic).write_to(&mut ofile, image::ImageFormat::Png).map_err(|e| e.to_string())?; 307 | Ok(Bytes::from(ofile)) 308 | }) 309 | .await; 310 | 311 | s.working.fetch_sub(1, Ordering::SeqCst); 312 | 313 | match r { 314 | Ok(buf) => { 315 | let mut builder = HttpResponse::Ok(); 316 | builder.set(header::ContentType::png()); 317 | if let Some((x, z)) = some_tile_id { 318 | builder.set(header::ContentDisposition { 319 | disposition: header::DispositionType::Attachment, 320 | parameters: vec![ 321 | header::DispositionParam::Filename(format!("{},{}.png", x, z)) 322 | ] 323 | }); 324 | } 325 | let builder = builder.body(buf); 326 | return Ok( 327 | builder.into() 328 | ); 329 | } 330 | Err(e) => { 331 | return Ok( 332 | HttpResponse::BadRequest() 333 | .body(e.to_string()) 334 | .into() 335 | ); 336 | } 337 | } 338 | } 339 | Ok(HttpResponse::NotFound().into()) 340 | } 341 | 342 | 343 | // mod test { 344 | 345 | // use super::*; 346 | 347 | // #[test] 348 | // fn test_server() { 349 | // std::env::set_var("RUST_LOG", "actix_server=info,actix_web=info"); 350 | // env_logger::init(); 351 | // let options = RenderServerOptions::default(); 352 | // RenderService::new(options).start(); 353 | // } 354 | 355 | 356 | // } 357 | 358 | 359 | -------------------------------------------------------------------------------- /src/tilegen/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod pathgen; 2 | pub mod tile; 3 | 4 | use std::io; 5 | use std::str::FromStr; 6 | use std::path::Path; 7 | use std::path::PathBuf; 8 | use std::collections::HashMap; 9 | use std::sync::Arc; 10 | use std::thread; 11 | use std::thread::Builder; 12 | 13 | 14 | use image::imageops::FilterType; 15 | 16 | use tile::LoadableImage; 17 | use tile::TileId; 18 | use tile::TileQTreeIterator; 19 | use pathgen::PathGenerator; 20 | 21 | pub fn merge_branch(root: TileId, cache: &mut HashMap, path_gen: &dyn PathGenerator, filter: FilterType, check: bool) { 22 | for tile_id in TileQTreeIterator::new(root, 0) { 23 | if tile_id.is_origin() { 24 | if let Some(img) = cache.get_mut(&tile_id) { 25 | img.ensure(); 26 | let p = path_gen.generate(tile_id.x, tile_id.z, tile_id.scale); 27 | match img.save(&p, check) { 28 | Err(e) => { 29 | log::warn!("[{}] tile {} @{} fail: {}", thread::current().name().unwrap_or_default(), tile_id, p.display(), e); 30 | }, 31 | Ok(b) => { 32 | if b { 33 | log::info!("[{}] tile {} generated", thread::current().name().unwrap_or_default(), tile_id); 34 | } 35 | } 36 | } 37 | } 38 | } else { 39 | let bl = cache.remove(&tile_id.bottomleft()).unwrap_or_default(); 40 | let br = cache.remove(&tile_id.bottomright()).unwrap_or_default(); 41 | let tl = cache.remove(&tile_id.topleft()).unwrap_or_default(); 42 | let tr = cache.remove(&tile_id.topright()).unwrap_or_default(); 43 | let img = LoadableImage::merge(&tl, &tr, &bl, &br, filter); 44 | let p = path_gen.generate(tile_id.x, tile_id.z, tile_id.scale); 45 | match img.save(&p, check) { 46 | Err(e) => { 47 | log::warn!("[{}] tile {} @{} fail: {}", thread::current().name().unwrap_or_default(), tile_id, p.display(), e); 48 | }, 49 | Ok(b) => { 50 | if b { 51 | log::info!("[{}] tile {} generated", thread::current().name().unwrap_or_default(), tile_id); 52 | } 53 | } 54 | } 55 | cache.insert(tile_id, img); 56 | } 57 | } 58 | } 59 | 60 | 61 | pub struct TileGeneratorOptions { 62 | filter: FilterType, 63 | multi_thread_mode: bool, 64 | input_folder: PathBuf, 65 | output_folder: PathBuf, 66 | path_mode: PathMode, 67 | check: bool, 68 | } 69 | 70 | impl TileGeneratorOptions { 71 | 72 | pub fn new(input_folder: PathBuf, output_folder: PathBuf, path_mode: PathMode) -> Self { 73 | TileGeneratorOptions { 74 | filter: FilterType::Nearest, 75 | multi_thread_mode: false, 76 | input_folder, 77 | output_folder, 78 | path_mode, 79 | check: false, 80 | } 81 | } 82 | 83 | pub fn set_filter(&mut self, filter: &str) { 84 | match filter { 85 | "nearest" => self.filter = FilterType::Nearest, 86 | "triangle" => self.filter = FilterType::Triangle, 87 | "gaussian" => self.filter = FilterType::Gaussian, 88 | "catmullrom" => self.filter = FilterType::CatmullRom, 89 | "lanczos3" => self.filter = FilterType::Lanczos3, 90 | _ => { } 91 | } 92 | } 93 | 94 | pub fn set_multi_thread_mode(&mut self, mode: bool) { 95 | self.multi_thread_mode = mode; 96 | } 97 | 98 | pub fn set_check_exist(&mut self, check: bool) { 99 | self.check = check; 100 | } 101 | 102 | // pub fn build_path_generator(&self, mode: &str) -> Option> { 103 | // use pathgen::Layer; 104 | 105 | // let mut sp = mode.splitn(2, ':'); 106 | // match sp.next()? { 107 | // "layer" => { 108 | // let mut sp = sp.next()?.splitn(3, ','); 109 | // let start: i32 = sp.next()?.parse().ok()?; 110 | // let step: i32 = sp.next()?.parse().ok()?; 111 | // let stop: i32 = sp.next()?.parse().ok()?; 112 | // Some(Arc::new(Layer::new(start, step, stop, self.output_folder.to_path_buf()))) 113 | // } 114 | // _ => None, 115 | // } 116 | // } 117 | } 118 | 119 | 120 | pub struct Bound { 121 | pub xmin: i32, 122 | pub xmax: i32, 123 | pub zmin: i32, 124 | pub zmax: i32 125 | } 126 | 127 | impl Bound { 128 | 129 | pub fn new() -> Self { 130 | Bound { 131 | xmin: 0, 132 | xmax: -1, 133 | zmin: 0, 134 | zmax: -1, 135 | } 136 | } 137 | 138 | pub fn extend(&mut self, tile: &TileId) { 139 | if tile.x < self.xmin { 140 | self.xmin = tile.x; 141 | } 142 | if tile.x > self.xmax { 143 | self.xmax = tile.x; 144 | } 145 | if tile.z < self.zmin { 146 | self.zmin = tile.z; 147 | } 148 | if tile.z > self.zmax { 149 | self.zmax = tile.z; 150 | } 151 | } 152 | } 153 | 154 | 155 | pub fn ceil_log2(x: i32) -> i32 { 156 | 32 - (x - 1).leading_zeros() as i32 157 | } 158 | 159 | #[derive(Debug)] 160 | pub enum PathMode { 161 | Layer { 162 | reverse: bool, 163 | min_zoom: Option, 164 | max_zoom: Option, 165 | }, 166 | Tree { 167 | 168 | } 169 | } 170 | 171 | impl FromStr for PathMode { 172 | type Err = io::Error; 173 | 174 | fn from_str(s: &str) -> Result { 175 | let mut mode_sp = s.splitn(2, ':'); 176 | match mode_sp.next().unwrap() { 177 | "layer+" => { 178 | if let Some(params) = mode_sp.next() { 179 | let mut params_sp = params.splitn(2, ','); 180 | let min_zoom = if let Some(z) = params_sp.next() { 181 | Some(z.parse().map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, "minZoom"))?) 182 | } else { 183 | None 184 | }; 185 | let max_zoom = if let Some(z) = params_sp.next() { 186 | Some(z.parse().map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, "maxZoom"))?) 187 | } else { 188 | None 189 | }; 190 | Ok(Self::Layer { 191 | reverse: false, 192 | min_zoom, 193 | max_zoom 194 | }) 195 | } else { 196 | Ok(Self::Layer { 197 | reverse: false, 198 | min_zoom: None, 199 | max_zoom: None 200 | }) 201 | } 202 | }, 203 | "layer-" => { 204 | if let Some(params) = mode_sp.next() { 205 | let mut params_sp = params.splitn(2, ','); 206 | let mut max_zoom = if let Some(z) = params_sp.next() { 207 | Some(z.parse().map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, "maxZoom"))?) 208 | } else { 209 | None 210 | }; 211 | let min_zoom = if let Some(z) = params_sp.next() { 212 | Some(z.parse().map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, "minZoom"))?) 213 | } else { 214 | let tmp = max_zoom; 215 | max_zoom = None; 216 | tmp 217 | }; 218 | Ok(Self::Layer { 219 | reverse: true, 220 | min_zoom, 221 | max_zoom 222 | }) 223 | } else { 224 | Ok(Self::Layer { 225 | reverse: true, 226 | min_zoom: None, 227 | max_zoom: None 228 | }) 229 | } 230 | }, 231 | "tree" => { 232 | unimplemented!(); 233 | }, 234 | _ => { 235 | Err(io::Error::new(io::ErrorKind::InvalidInput, "unsupported")) 236 | } 237 | } 238 | 239 | } 240 | 241 | } 242 | 243 | impl PathMode { 244 | 245 | pub fn extract(&self, bound: &Bound, root: &Path) -> Arc { 246 | use std::cmp::max; 247 | use pathgen::Layer; 248 | 249 | match self { 250 | Self::Layer { reverse, min_zoom, max_zoom } => { 251 | let min_zoom = min_zoom.unwrap_or(0); 252 | let max_zoom = if let Some(z) = max_zoom { 253 | *z 254 | } else { 255 | min_zoom + ceil_log2(max(max(bound.xmin.abs(), bound.xmax.abs()), max(bound.zmin.abs(), bound.zmax.abs()))) 256 | }; 257 | if *reverse { 258 | Arc::new(Layer::new(max_zoom, -1, min_zoom, PathBuf::from(root))) 259 | } else { 260 | Arc::new(Layer::new(min_zoom, 1, max_zoom, PathBuf::from(root))) 261 | } 262 | }, 263 | Self::Tree { } => { 264 | unimplemented!() 265 | } 266 | } 267 | } 268 | } 269 | 270 | 271 | pub struct TileGenerator { 272 | options: TileGeneratorOptions, 273 | } 274 | 275 | impl TileGenerator { 276 | 277 | pub fn new(options: TileGeneratorOptions) -> Self { 278 | TileGenerator { 279 | options, 280 | } 281 | } 282 | 283 | pub fn list_files(&self) -> HashMap { 284 | const EXT: &'static str = ".png"; 285 | if let Ok(read_dir) = self.options.input_folder.read_dir() { 286 | read_dir.filter_map(|entry| { 287 | let src = entry.ok()?.path(); 288 | let filename = src.file_name()?.to_str()?; 289 | if !filename.ends_with(EXT) { 290 | return None; 291 | } 292 | let filename = &filename[0 .. filename.len() - EXT.len()]; 293 | let mut sp = filename.splitn(2, ','); 294 | let x: i32 = sp.next()?.parse().ok()?; 295 | let z: i32 = sp.next()?.parse().ok()?; 296 | Some((TileId::new(0, x, z), LoadableImage::new(src))) 297 | }).collect() 298 | } else { 299 | HashMap::new() 300 | } 301 | } 302 | 303 | pub fn generate_tile(&self, cache: HashMap) { 304 | let mut parts = vec![ 305 | (-1, -1, HashMap::new()), 306 | (0, -1, HashMap::new()), 307 | (-1, 0, HashMap::new()), 308 | (0, 0, HashMap::new()) 309 | ]; 310 | let mut bound = Bound::new(); 311 | for (tile, image) in cache.into_iter() { 312 | bound.extend(&tile); 313 | parts[tile.side()].2.insert(tile, image); 314 | } 315 | let path_gen = self.options.path_mode.extract(&bound, self.options.output_folder.as_path()); 316 | let mut ths = Vec::new(); 317 | for (x, z, mut cache_part) in parts.into_iter() { 318 | if cache_part.len() > 0 { 319 | let path_gen = path_gen.clone(); 320 | let root = TileId::new(path_gen.get_max_scale(), x, z); 321 | let filter = self.options.filter.clone(); 322 | let check = self.options.check; 323 | let tasks = move || { 324 | let path_gen = path_gen.as_ref(); 325 | merge_branch(root, &mut cache_part, path_gen, filter, check) 326 | }; 327 | if self.options.multi_thread_mode { 328 | let th = Builder::new().name(format!("work-({},{})", x, z)).spawn(tasks).unwrap(); 329 | ths.push(th); 330 | } else { 331 | tasks(); 332 | } 333 | } 334 | } 335 | for th in ths { 336 | th.join().unwrap(); 337 | } 338 | } 339 | } 340 | 341 | 342 | mod test { 343 | 344 | #[test] 345 | fn test_path_mode_parse() { 346 | use super::Bound; 347 | use super::PathMode; 348 | use std::str::FromStr; 349 | use std::path::PathBuf; 350 | 351 | let mut b = Bound::new(); 352 | b.xmin = -32; 353 | b.xmax = 50; 354 | b.zmin = -43; 355 | b.zmax = 50; 356 | 357 | let p = PathBuf::new(); 358 | 359 | for input in &[ 360 | "layer+", 361 | "layer+:0", 362 | "layer+:2", 363 | "layer+:2,6", 364 | "layer-", 365 | "layer-:0", 366 | "layer-:2", 367 | "layer-:6,2", 368 | ] { 369 | match PathMode::from_str(input) { 370 | Ok(m) => { 371 | println!("{:?}", m); 372 | let layer = m.extract(&b, &p); 373 | println!("{:?}", layer.get_max_scale()); 374 | }, 375 | Err(e) => { 376 | println!("{}", e); 377 | } 378 | } 379 | } 380 | } 381 | 382 | } -------------------------------------------------------------------------------- /src/tilegen/pathgen.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | 4 | pub trait PathGenerator { 5 | 6 | fn get_max_scale(&self) -> i32; 7 | 8 | fn generate(&self, x: i32, z: i32, scale: i32 /* scale level increase from 0 */) -> PathBuf; 9 | 10 | } 11 | 12 | 13 | pub struct Layer { 14 | root: PathBuf, 15 | start: i32, 16 | step: i32, 17 | max_scale: i32 18 | } 19 | 20 | impl Layer { 21 | 22 | pub fn new(start: i32, step: i32, stop: i32, root: PathBuf) -> Self { 23 | Layer { 24 | root, 25 | start, 26 | step, 27 | max_scale: (stop - start) / step 28 | } 29 | } 30 | } 31 | 32 | impl PathGenerator for Layer { 33 | 34 | fn get_max_scale(&self) -> i32 { 35 | self.max_scale 36 | } 37 | 38 | fn generate(&self, x: i32, z: i32, scale: i32) -> PathBuf { 39 | let zoom = self.start + self.step * scale; 40 | let mut pathbuf = self.root.to_path_buf(); 41 | pathbuf.push(format!("{}", zoom)); 42 | pathbuf.push(format!("{},{}.png", x, z)); 43 | pathbuf 44 | } 45 | 46 | } 47 | 48 | 49 | pub struct Tree { 50 | root: PathBuf, 51 | max_scale: i32, 52 | topleft: String, 53 | topright: String, 54 | bottomleft: String, 55 | bottomright: String 56 | } 57 | 58 | -------------------------------------------------------------------------------- /src/tilegen/qtree.rs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/tilegen/tile.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::path::PathBuf; 3 | use std::fs; 4 | use std::fs::File; 5 | use std::hash::Hash; 6 | use std::ops::Deref; 7 | use std::cmp::Ordering; 8 | 9 | use image; 10 | use image::Pixel; 11 | use image::DynamicImage; 12 | use image::ImageBuffer; 13 | use image::RgbaImage; 14 | use image::ImageFormat; 15 | use image::ImageResult; 16 | use image::ImageError; 17 | use image::imageops; 18 | use image::imageops::FilterType; 19 | 20 | 21 | pub enum LoadableImage { 22 | Unloaded(PathBuf), 23 | Image(RgbaImage), 24 | Empty, 25 | } 26 | 27 | impl Default for LoadableImage { 28 | 29 | fn default() -> Self { 30 | Self::Empty 31 | } 32 | } 33 | 34 | fn load_png>(path: P) -> ImageResult { 35 | use std::io::BufReader; 36 | 37 | let ifile = File::open(path).map_err(ImageError::from)?; 38 | image::load(BufReader::new(ifile), ImageFormat::Png) 39 | } 40 | 41 | fn cmp_image_buffer(img1: &ImageBuffer, img2: &ImageBuffer) -> bool 42 | where 43 | P: Pixel + 'static + PartialEq, 44 | C1: Deref, 45 | C2: Deref 46 | { 47 | for (p1, p2) in img1.pixels().zip(img2.pixels()) { 48 | if p1 != p2 { 49 | return false; 50 | } 51 | } 52 | return true; 53 | } 54 | 55 | impl LoadableImage { 56 | 57 | pub fn new(path: PathBuf) -> Self { 58 | Self::Unloaded(path) 59 | } 60 | 61 | #[inline] 62 | pub fn is_image(&self) -> bool { 63 | match self { 64 | Self::Image(_) => true, 65 | _ => false, 66 | } 67 | } 68 | 69 | pub fn ensure(&mut self) { 70 | 71 | 72 | if let Self::Unloaded(path) = self { 73 | let img = if let Ok(image) = load_png(path.as_path()) { 74 | if let DynamicImage::ImageRgba8(image) = image { 75 | if image.width() != image.height() { 76 | Self::Empty 77 | } else { 78 | Self::Image(image) 79 | } 80 | } else { 81 | Self::Empty 82 | } 83 | } else { 84 | Self::Empty 85 | }; 86 | *self = img; 87 | } 88 | } 89 | 90 | pub fn save(&self, path: &PathBuf, check: bool) -> ImageResult { 91 | use image::buffer::ConvertBuffer; 92 | use image::RgbImage; 93 | 94 | if let Self::Image(img) = self { 95 | let mut full = true; 96 | for pixel in img.pixels() { 97 | if pixel[3] < 255 { 98 | full = false; 99 | break; 100 | } 101 | } 102 | if let Some(parent) = path.parent() { 103 | fs::create_dir_all(parent).map_err(ImageError::from)?; 104 | } 105 | if full { 106 | let img: RgbImage = img.convert(); 107 | if check { 108 | if let Ok(old) = load_png(path.as_path()) { 109 | if let DynamicImage::ImageRgb8(old) = &old { 110 | if img.cmp(old) == Ordering::Equal { 111 | return Ok(false); 112 | } 113 | } 114 | } 115 | } 116 | img.save_with_format(path.as_path(), ImageFormat::Png)?; 117 | } else { 118 | if check { 119 | if let Ok(old) = load_png(path.as_path()) { 120 | if let DynamicImage::ImageRgba8(old) = &old { 121 | if img.cmp(old) == Ordering::Equal { 122 | return Ok(false); 123 | } 124 | } 125 | } 126 | } 127 | img.save_with_format(path.as_path(), ImageFormat::Png)?; 128 | } 129 | Ok(true) 130 | } else { 131 | Ok(false) 132 | } 133 | } 134 | 135 | pub fn merge(topleft: &Self, topright: &Self, bottomleft: &Self, bottomright: &Self, filter: FilterType) -> Self { 136 | let refs = [topleft, topright, bottomleft, bottomright]; //00 01 10 11 137 | let mut w = 0; 138 | let mut h = 0; 139 | for p in refs.iter() { 140 | if let Self::Image(img) = *p { 141 | w = img.width(); 142 | h = img.height(); 143 | break; 144 | } 145 | } 146 | if w * h == 0 { 147 | Self::Empty 148 | } else { 149 | let mut combine = RgbaImage::new(2 * w, 2 * h); 150 | for (i, p) in refs.iter().enumerate() { 151 | if let Self::Image(img) = *p { 152 | let i = i as u32; 153 | let ox = ((i & 0x1) >> 0) * w; 154 | let oy = ((i & 0x2) >> 1) * h; 155 | imageops::replace(&mut combine, img, ox, oy); 156 | } 157 | } 158 | Self::Image(imageops::resize(&combine, w, h, filter)) 159 | } 160 | } 161 | 162 | } 163 | 164 | #[derive(Clone, Hash, PartialEq, Eq, Debug)] 165 | pub struct TileId { 166 | pub scale: i32, 167 | pub x: i32, 168 | pub z: i32, 169 | } 170 | 171 | impl std::fmt::Display for TileId { 172 | 173 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 174 | f.write_fmt(format_args!("({},{})-[{}]", self.x, self.z, self.scale)) 175 | } 176 | } 177 | 178 | impl TileId { 179 | 180 | pub fn new(scale: i32, x: i32, z: i32) -> Self { 181 | TileId { 182 | scale, 183 | x, 184 | z, 185 | } 186 | } 187 | 188 | pub fn topleft(&self) -> Self { 189 | TileId { 190 | x: self.x * 2 + 0, 191 | z: self.z * 2 + 0, 192 | scale: self.scale - 1, 193 | } 194 | } 195 | 196 | pub fn topright(&self) -> Self { 197 | TileId { 198 | x: self.x * 2 + 1, 199 | z: self.z * 2 + 0, 200 | scale: self.scale - 1, 201 | } 202 | } 203 | 204 | pub fn bottomleft(&self) -> Self { 205 | TileId { 206 | x: self.x * 2 + 0, 207 | z: self.z * 2 + 1, 208 | scale: self.scale - 1, 209 | } 210 | } 211 | 212 | pub fn bottomright(&self) -> Self { 213 | TileId { 214 | x: self.x * 2 + 1, 215 | z: self.z * 2 + 1, 216 | scale: self.scale - 1, 217 | } 218 | } 219 | 220 | pub fn parent(&self) -> Self { 221 | TileId { 222 | x: self.x / 2, 223 | z: self.z / 2, 224 | scale: self.scale + 1, 225 | } 226 | } 227 | 228 | pub fn is_origin(&self) -> bool { 229 | self.scale == 0 230 | } 231 | 232 | pub fn side(&self) -> usize { 233 | let mut i = 0; 234 | if self.x >= 0 { 235 | i |= 0x1; 236 | } 237 | if self.z >= 0 { 238 | i |= 0x2; 239 | } 240 | i // [topleft,topright,bottomleft,bottomright] 241 | } 242 | } 243 | 244 | 245 | #[derive(Clone)] 246 | struct TileQTreeIndexNode { 247 | tile_id: TileId, 248 | parent_index: isize, 249 | child_id: usize, 250 | } 251 | 252 | impl TileQTreeIndexNode { 253 | pub fn new(tile_id: TileId, parent_index: isize) -> Self { 254 | TileQTreeIndexNode { 255 | tile_id, 256 | parent_index, 257 | child_id: 0 258 | } 259 | } 260 | } 261 | 262 | 263 | pub struct TileQTreeIterator { 264 | stack: Vec, 265 | stop_layer: i32, 266 | } 267 | 268 | impl TileQTreeIterator { 269 | 270 | pub fn new(root: TileId, stop_layer: i32) -> Self { 271 | let mut stack = Vec::new(); 272 | stack.push(TileQTreeIndexNode::new(root, -1)); 273 | TileQTreeIterator { 274 | stack, 275 | stop_layer 276 | } 277 | } 278 | } 279 | 280 | impl Iterator for TileQTreeIterator { 281 | type Item = TileId; 282 | 283 | fn next(&mut self) -> Option { 284 | loop { 285 | let TileQTreeIndexNode{ tile_id, parent_index, child_id } = self.stack.last()?.clone(); 286 | let current_index = self.stack.len() as isize - 1; 287 | if tile_id.scale <= self.stop_layer || child_id > 3 { 288 | if parent_index >= 0 { 289 | let parent = &mut self.stack[parent_index as usize]; 290 | parent.child_id += 1; 291 | } 292 | self.stack.pop(); 293 | return Some(tile_id); 294 | } else { 295 | self.stack.push(TileQTreeIndexNode::new(tile_id.bottomright(), current_index)); 296 | self.stack.push(TileQTreeIndexNode::new(tile_id.bottomleft(), current_index)); 297 | self.stack.push(TileQTreeIndexNode::new(tile_id.topright(), current_index)); 298 | self.stack.push(TileQTreeIndexNode::new(tile_id.topleft(), current_index)); 299 | } 300 | } 301 | } 302 | } 303 | 304 | 305 | 306 | 307 | mod test { 308 | 309 | #[test] 310 | fn test_tile_qtree() { 311 | use super::{TileId, TileQTreeIterator}; 312 | 313 | let root = TileId::new(3, 0, 0); 314 | for tid in TileQTreeIterator::new(root, 1) { 315 | println!("{:?}", tid); 316 | } 317 | } 318 | 319 | } -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Voxelmap-Cache Renderer 7 | 8 | 9 | 10 | 16 | 17 |
18 |

使用说明

19 |
    20 |
  1. `light`选择环境光照,默认为15
  2. 21 |
  3. `cache-files`选择需要渲染的cache文件;文件一般位于`.minecraft[/versions/<version>]/.mods/mamiyaotaru/voxelmap/cache/<server>/<world>/overworld/`;可多选
  4. 22 |
  5. 点击`Submit`上传,并等待图片回传
  6. 23 |
  7. 当所有图片回传结束,点击`Save`,生成打包的图片下载`Download`;点击`Download`下载
  8. 24 |
25 |
26 | 27 |
28 |
29 | 30 | 31 |
32 |
33 | 34 | 35 |
36 | 37 |
38 | 39 |
40 | 41 |
42 | 43 |
44 | 45 |
46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /web/index.js: -------------------------------------------------------------------------------- 1 | window.onload = function() { 2 | console.log("ready!") 3 | let zip = null; 4 | document.querySelector("#submit").addEventListener("click", (e) => { 5 | let params = { 6 | light: document.querySelector("#light").value 7 | }; 8 | document.querySelector("#display").innerHTML = ""; 9 | let files = document.querySelector("#files").files; 10 | zip = new JSZip(); 11 | let target_size = getSize(files.length); 12 | for(let i = 0; i < files.length; i++) { 13 | let file = files[i]; 14 | render_api(file, params) 15 | .then((r) => { 16 | let src = window.URL.createObjectURL(r.blob); 17 | let img = document.createElement("img"); 18 | img.src = src; 19 | img.width = target_size; 20 | img.height = target_size; 21 | document.querySelector("#display").appendChild(img); 22 | let id = r.id; 23 | zip.file(id.x + ',' + id.z + ".png", r.blob); 24 | }) 25 | .catch((r) => { 26 | let id = r.id; 27 | if(id instanceof Object) { 28 | id = "tile(" + id.x + "," + z + ")"; 29 | } 30 | let img = document.createElement("img"); 31 | img.src = "#"; 32 | img.width = target_size; 33 | img.height = target_size; 34 | img.alt = id + ' ' + r.status; 35 | document.querySelector("#display").appendChild(img); 36 | }) 37 | 38 | 39 | } 40 | }) 41 | document.querySelector("#save").addEventListener("click", (ev) => { 42 | if(zip !== null) { 43 | let div = document.createElement("div"); 44 | div.innerText = "calculating..."; 45 | document.querySelector("#saving").appendChild(div); 46 | zip.generateAsync({type: "blob"}) 47 | .then((content) => { 48 | div.remove(); 49 | let link = document.createElement("a"); 50 | link.href = window.URL.createObjectURL(content); 51 | link.innerText = "DOWNLOAD" 52 | link.download = "test.zip" 53 | link.addEventListener("click", (ev) => { 54 | link.remove(); 55 | }) 56 | document.querySelector("#saving").appendChild(link); 57 | }) 58 | .catch((e) => { 59 | div.remove(); 60 | console.log(e); 61 | }) 62 | } 63 | }) 64 | } 65 | 66 | function getSize(n) { 67 | let tw = window.innerWidth; 68 | let th = window.innerHeight; 69 | let sz = 256; 70 | for(; sz > 64; sz = sz / 2) { 71 | let lw = Math.floor(tw / sz); 72 | let lh = Math.floor(th / sz); 73 | if(lw * lh >= n) { 74 | break; 75 | } 76 | } 77 | return sz; 78 | } 79 | // function upload(file) { 80 | // let form = new FormData(); 81 | // form.append("tile", file); 82 | // let req = new XMLHttpRequest(); 83 | // req.addEventListener("load", function(ev) { 84 | // let blob = this.response; 85 | // let src = window.URL.createObjectURL(blob) 86 | // // let img = document.createElement("img"); 87 | // // img.src = src; 88 | // // img.onload = function() { 89 | // // document.querySelector("#display").appendChild(img) 90 | // // } 91 | // let a = document.createElement("a"); 92 | // a.innerText = file.name.replace(".zip", ".png"); 93 | // a.href = src; 94 | // a.download = file.name.replace(".zip", ".png"); 95 | // let d = document.createElement("div"); 96 | // d.appendChild(a); 97 | // document.querySelector("#display").appendChild(d); 98 | // window.setTimeout(function() { 99 | // a.click(); 100 | // }, 10) 101 | // }); 102 | // req.responseType = "blob" 103 | // req.open("POST", "/render") 104 | // req.send(form) 105 | // } 106 | 107 | 108 | /** 109 | * 110 | * @param {File} file 111 | * @param {Object} params 112 | */ 113 | function render_api(file, params) { 114 | const pattern = /(-?\d+),(-?\d+)\.zip/; 115 | let data = new FormData() 116 | data.append("tile", file) 117 | let url = "/render"; 118 | if(params instanceof Object) { 119 | let query = new Array(); 120 | for(let key in params) { 121 | let value = params[key] 122 | query.push(key + "=" + value) 123 | } 124 | if(query.length > 0) { 125 | url += ('?' + query.join('&')) 126 | } 127 | } 128 | let id = pattern.exec(file.name) 129 | if(id !== null) { 130 | id = { 131 | x: id[1], 132 | z: id[2] 133 | }; 134 | } else { 135 | id = file.name; 136 | } 137 | return new Promise(function (resolve, reject) { 138 | let req = new XMLHttpRequest(); 139 | req.onload = function(ev) { 140 | if(this.status == 200) { 141 | let blob = this.response 142 | resolve({ 143 | id, 144 | blob 145 | }) 146 | } else { 147 | reject({ 148 | id, 149 | status: this.status, 150 | message: this.responseText, 151 | }) 152 | } 153 | } 154 | req.onerror = function(ev) { 155 | reject({ 156 | id, 157 | status: this.status, 158 | message: this.responseText, 159 | }) 160 | } 161 | req.responseType = "blob"; 162 | req.open("POST", url); 163 | req.send(data) 164 | }); 165 | } --------------------------------------------------------------------------------