├── .gitignore ├── demo ├── demo.js ├── .DS_Store ├── models │ ├── .DS_Store │ ├── deer.vox │ ├── monu7.vox │ ├── monu8.vox │ └── sphere.vox ├── webpack.config.js ├── global.css ├── index.html └── demo-page │ └── demo-page.js ├── .npmignore ├── .DS_Store ├── img └── screenshot.png ├── index.html ├── README.md ├── LICENSE ├── package.json ├── .github └── workflows │ └── codeql-analysis.yml └── vox-viewer.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /demo/demo.js: -------------------------------------------------------------------------------- 1 | 2 | import './demo-page/demo-page'; -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | img 3 | demo/dist 4 | demo/models -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlorianFe/vox-viewer/HEAD/.DS_Store -------------------------------------------------------------------------------- /demo/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlorianFe/vox-viewer/HEAD/demo/.DS_Store -------------------------------------------------------------------------------- /img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlorianFe/vox-viewer/HEAD/img/screenshot.png -------------------------------------------------------------------------------- /demo/models/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlorianFe/vox-viewer/HEAD/demo/models/.DS_Store -------------------------------------------------------------------------------- /demo/models/deer.vox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlorianFe/vox-viewer/HEAD/demo/models/deer.vox -------------------------------------------------------------------------------- /demo/models/monu7.vox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlorianFe/vox-viewer/HEAD/demo/models/monu7.vox -------------------------------------------------------------------------------- /demo/models/monu8.vox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlorianFe/vox-viewer/HEAD/demo/models/monu8.vox -------------------------------------------------------------------------------- /demo/models/sphere.vox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlorianFe/vox-viewer/HEAD/demo/models/sphere.vox -------------------------------------------------------------------------------- /demo/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | mode: "development", 5 | entry: "./demo/demo.js", 6 | output: { 7 | filename: "demo.js", 8 | path: path.join(__dirname, "/dist"), 9 | }, 10 | resolve: { 11 | modules: ["node_modules"], 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | voxel-viewer 7 | 8 | 9 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /demo/global.css: -------------------------------------------------------------------------------- 1 | 2 | html, body { 3 | width: 100%; 4 | height: 100vh; 5 | margin: 0; 6 | padding: 0; 7 | font-family: 'Roboto', sans-serif; 8 | overflow: hidden; 9 | 10 | -webkit-touch-callout: none; /* iOS Safari */ 11 | -webkit-user-select: none; /* Safari */ 12 | -khtml-user-select: none; /* Konqueror HTML */ 13 | -moz-user-select: none; /* Old versions of Firefox */ 14 | -ms-user-select: none; /* Internet Explorer/Edge */ 15 | user-select: none; /* Non-prefixed version, currently supported by Chrome, Opera and Firefox */ 16 | } 17 | 18 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | vox-viewer demo 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # \ 2 | 3 | uses [\](https://github.com/GoogleWebComponents/model-viewer) and acts like it, but takes .vox files 4 | which are exported by [Magicka Voxel](https://ephtracy.github.io/) as source instead of GLTF files. It also uses 5 | [voxel-triangulation.js](https://github.com/FlorianFe/voxel-triangulation.js) to build a mesh out of the voxel model and [vox-reader.js](https://github.com/FlorianFe/vox-reader.js) to parse vox files. 6 | 7 | ## 🖼 Preview 8 | 9 | ![screenshot](https://raw.githubusercontent.com/FlorianFe/vox-viewer/master/img/screenshot.png) 10 | 11 | ## 👀 Demo 12 | 13 | You can see a live demo [here](https://florianfe.github.io/vox-viewer/demo/) 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Florian Fechner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vox-viewer", 3 | "version": "1.4.0", 4 | "description": "like model-viewer but for .vox models", 5 | "main": "vox-viewer.js", 6 | "scripts": { 7 | "build-demo": "webpack --config demo/webpack.config.js", 8 | "start": "webpack --config demo/webpack.config.js && sirv demo", 9 | "watch": "webpack --config demo/webpack.config.js --watch" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/florianfe/vox-viewer.git" 14 | }, 15 | "keywords": [ 16 | "voxel", 17 | "magickavoxel", 18 | "model-viewer", 19 | "graphics", 20 | "model", 21 | "graphics", 22 | "vox", 23 | "model" 24 | ], 25 | "author": "Florian Fechner", 26 | "license": "MIT", 27 | "dependencies": { 28 | "@google/model-viewer": "^1.12.0", 29 | "lit": "^2.2.5", 30 | "ndarray": "^1.0.19", 31 | "three": "^0.141.0", 32 | "underscore": "^1.13.3", 33 | "vox-reader": "4.0.1", 34 | "voxel-triangulation": "^1.3.6", 35 | "zeros": "^1.0.0" 36 | }, 37 | "devDependencies": { 38 | "sirv": "^2.0.2", 39 | "sirv-cli": "^2.0.2", 40 | "terser-webpack-plugin": "^5.3.1", 41 | "webpack": "^5.72.1", 42 | "webpack-cli": "^4.9.2" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '32 15 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /demo/demo-page/demo-page.js: -------------------------------------------------------------------------------- 1 | import "../../vox-viewer"; 2 | import { LitElement, html } from "lit-element"; 3 | 4 | class DemoPage extends LitElement { 5 | static get is() { 6 | return "demo-page"; 7 | } 8 | 9 | static get properties() { 10 | return { 11 | selectedModel: { type: String }, 12 | selectedMode: { type: String }, 13 | }; 14 | } 15 | 16 | constructor() { 17 | super(); 18 | this.selectedMode = "examples"; 19 | this.selectedModel = "./models/deer.vox"; 20 | } 21 | 22 | onModelSelected() { 23 | const modelSelection = this.shadowRoot.querySelector("#model-selection"); 24 | 25 | this.selectedModel = modelSelection.value; 26 | } 27 | 28 | onModeSelected() { 29 | const modeSelection = this.shadowRoot.querySelector("#mode-selection"); 30 | 31 | this.selectedMode = modeSelection.value; 32 | 33 | if (this.selectedMode === "examples") { 34 | this.selectedModel = "./models/deer.vox"; 35 | } 36 | } 37 | 38 | onCustomModelUpload(e) { 39 | let files = e.target.files; 40 | let f = files[0]; 41 | 42 | let reader = new FileReader(); 43 | 44 | reader.onload = ((file) => { 45 | return (e) => { 46 | const string = e.target.result; 47 | const blob = new Blob([string]); 48 | const url = URL.createObjectURL(blob); 49 | 50 | this.selectedModel = url; 51 | }; 52 | })(f); 53 | 54 | reader.readAsArrayBuffer(f); 55 | } 56 | 57 | render() { 58 | return html` 59 | 90 | 91 |
92 |
93 | Mode:
94 | 102 |
103 | 104 |
105 | ${this.selectedMode === "custom" 106 | ? html`
107 | Own Model:
108 | 113 |
` 114 | : html`
115 | Model:
116 | 126 |
`} 127 |
128 |
129 | 130 |
131 | 138 |
139 | `; 140 | } 141 | } 142 | 143 | customElements.define(DemoPage.is, DemoPage); 144 | -------------------------------------------------------------------------------- /vox-viewer.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html } from "lit"; 2 | import voxelTriangulation from "voxel-triangulation"; 3 | import { flatten } from "ramda"; 4 | import { 5 | BufferGeometry, 6 | BufferAttribute, 7 | MeshStandardMaterial, 8 | Mesh, 9 | } from "three"; 10 | import { GLTFExporter } from "three/examples/jsm/exporters/GLTFExporter.js"; 11 | import readVox from "vox-reader"; 12 | import zeros from "zeros"; 13 | 14 | import "@google/model-viewer"; 15 | import { isArray } from "underscore"; 16 | 17 | /** 18 | * `vox-viewer` 19 | * displays voxel data 20 | * 21 | * @customElement 22 | * @polymer 23 | * @demo demo/index.html 24 | */ 25 | 26 | const MAX_VALUE_OF_A_BYTE = 255; 27 | 28 | class VoxViewer extends LitElement { 29 | static get is() { 30 | return "vox-viewer"; 31 | } 32 | 33 | static get properties() { 34 | return { 35 | src: { type: String }, 36 | 37 | alt: { type: String }, 38 | ar: { type: Boolean }, 39 | autoRotate: { type: Boolean, attribute: "auto-rotate" }, 40 | autoRotateDelay: { type: Number, attribute: "auto-rotate-delay" }, 41 | autoplay: { type: Boolean }, 42 | backgroundColor: { type: String, attribute: "background-color" }, 43 | backgroundImage: { type: String, attribute: "background-image" }, 44 | cameraControls: { type: Boolean, attribute: "camera-controls" }, 45 | cameraOrbit: { type: String, attribute: "camera-orbit" }, 46 | cameraTarget: { type: String, attribute: "camera-target" }, 47 | environmentImage: { type: String, attribute: "environment-image" }, 48 | exposure: { type: Number }, 49 | fieldOfView: { type: String, attribute: "field-of-view" }, 50 | interactionPolicy: { type: String }, 51 | interactionPrompt: { type: String }, 52 | interactionPromptStyle: { type: String }, 53 | interactionPromptTreshold: { type: Number }, 54 | 55 | preload: { type: Boolean }, 56 | reveal: { type: String }, 57 | shadowIntensity: { type: Number, attribute: "shadow-intensity" }, 58 | unstableWebxr: { type: Boolean, attribute: "unstable-webxr" }, 59 | }; 60 | } 61 | 62 | constructor() { 63 | super(); 64 | 65 | this.alt = "a voxel model"; // changed! 66 | } 67 | 68 | get currentTime() { 69 | return this.shadowRoot.querySelector("#model-viewer").currentTime; 70 | } 71 | get paused() { 72 | return this.shadowRoot.querySelector("#model-viewer").paused; 73 | } 74 | 75 | getCameraOrbit() { 76 | return this.shadowRoot.querySelector("#model-viewer").getCameraOrbit(); 77 | } 78 | getFieldOfView() { 79 | return this.shadowRoot.querySelector("#model-viewer").getFieldOfView(); 80 | } 81 | jumpCameraToGoal() { 82 | this.shadowRoot.querySelector("#model-viewer").jumpCameraToGoal(); 83 | } 84 | play() { 85 | this.shadowRoot.querySelector("#model-viewer").play(); 86 | } 87 | pause() { 88 | this.shadowRoot.querySelector("#model-viewer").pause(); 89 | } 90 | resetTurntableRotation() { 91 | this.shadowRoot.querySelector("#model-viewer").resetTurntableRotation(); 92 | } 93 | toDataURL(type, encoderOptions) { 94 | return this.shadowRoot 95 | .querySelector("#model-viewer") 96 | .toDataURL(type, encoderOptions); 97 | } 98 | 99 | updated(changedProperties) { 100 | if (changedProperties.has("src")) { 101 | this.initialized = false; 102 | this.loadVoxModel(this.src, changedProperties); 103 | } 104 | 105 | this.setup(changedProperties); 106 | } 107 | 108 | setup(changedProperties) { 109 | changedProperties.forEach((_, propertyName) => { 110 | if (this[propertyName] != undefined) { 111 | if (propertyName !== "src") { 112 | this.shadowRoot.querySelector("#model-viewer")[propertyName] = 113 | this[propertyName]; 114 | } 115 | } 116 | }); 117 | } 118 | 119 | async loadVoxModel(fileURL, changedProperties) { 120 | if (fileURL.slice(0, 5) == "blob:") { 121 | const response = await fetch(fileURL); 122 | const arrayBuffer = await response.arrayBuffer(); 123 | 124 | this.processVoxContent(arrayBuffer, changedProperties); 125 | } else { 126 | let request = new XMLHttpRequest(); 127 | request.responseType = "arraybuffer"; 128 | request.open("GET", fileURL, true); 129 | request.onreadystatechange = () => { 130 | if (request.readyState === 4 && request.status == "200") { 131 | this.processVoxContent(request.response, changedProperties); 132 | } 133 | }; 134 | 135 | request.send(null); 136 | } 137 | } 138 | 139 | processVoxContent(voxContent, changedProperties) { 140 | const u8intArrayContent = new Uint8Array(voxContent); 141 | 142 | let vox = readVox(u8intArrayContent); 143 | 144 | let hasMultipleLayers = isArray(vox.xyzi); 145 | 146 | let voxelData = vox.xyzi.values; 147 | let size = vox.size; 148 | let rgba = vox.rgba.values; 149 | 150 | if (hasMultipleLayers) { 151 | voxelData = flatten(vox.xyzi.map((xyzi) => xyzi.values)); 152 | 153 | size = { 154 | x: Math.max(...vox.size.map((s) => s.x)), 155 | y: Math.max(...vox.size.map((s) => s.y)), 156 | z: Math.max(...vox.size.map((s) => s.z)), 157 | }; 158 | 159 | rgba = vox.rgba.values; 160 | } 161 | 162 | let componentizedColores = rgba.map((c) => [c.r, c.g, c.b]); 163 | let voxels = zeros([size.x, size.y, size.z]); 164 | 165 | voxelData.forEach(({ x, y, z, i }) => voxels.set(x, y, z, i)); 166 | 167 | voxels = voxels.transpose(1, 2, 0); 168 | 169 | let { vertices, normals, indices, voxelValues } = 170 | voxelTriangulation(voxels); 171 | 172 | let normalizedColors = componentizedColores.map((color) => 173 | color.map((c) => c / MAX_VALUE_OF_A_BYTE) 174 | ); 175 | 176 | let gammaCorrectedColors = normalizedColors.map((color) => 177 | color.map((c) => Math.pow(c, 2.2)) 178 | ); 179 | 180 | let alignedColors = [[0, 0, 0], ...gammaCorrectedColors]; 181 | let flattenedColors = flatten(voxelValues.map((v) => alignedColors[v])); 182 | 183 | let geometry = new BufferGeometry(); 184 | 185 | geometry.setAttribute( 186 | "position", 187 | new BufferAttribute(new Float32Array(vertices), 3) 188 | ); 189 | geometry.setAttribute( 190 | "normal", 191 | new BufferAttribute(new Float32Array(normals), 3) 192 | ); 193 | geometry.setAttribute( 194 | "color", 195 | new BufferAttribute(new Float32Array(flattenedColors), 3) 196 | ); 197 | geometry.setIndex(new BufferAttribute(new Uint32Array(indices), 1)); 198 | 199 | let material = new MeshStandardMaterial({ 200 | roughness: 1.0, 201 | metalness: 0.0, 202 | }); 203 | let mesh = new Mesh(geometry, material); 204 | let exporter = new GLTFExporter(); 205 | 206 | exporter.parse(mesh, (json) => { 207 | let string = JSON.stringify(json); 208 | let blob = new Blob([string], { type: "text/plain" }); 209 | let url = URL.createObjectURL(blob); 210 | 211 | this.shadowRoot.querySelector("#model-viewer").src = url; 212 | 213 | this.setup(changedProperties); 214 | }); 215 | } 216 | 217 | render() { 218 | return html` 219 | 230 | 231 | 232 | `; 233 | } 234 | } 235 | 236 | window.customElements.define("vox-viewer", VoxViewer); 237 | --------------------------------------------------------------------------------