├── .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 | 
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 |
--------------------------------------------------------------------------------