├── .babelrc
├── .browserslistrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .nvmrc
├── .prettierignore
├── .prettierrc.yaml
├── .stylelintrc.json
├── .travis.yml
├── .vscode
├── extensions.json
└── settings.json
├── LICENSE
├── README.md
├── __mocks__
├── fileMock.js
└── styleMock.js
├── __tests__
└── application.js
├── config
├── README.md
├── webpack.common.js
├── webpack.dev.js
└── webpack.prod.js
├── cypress.json
├── cypress
├── fixtures
│ └── example.json
├── integration
│ └── application
│ │ └── app.spec.js
├── plugins
│ └── index.js
├── support
│ ├── commands.js
│ └── index.js
└── utils.js
├── demo.gif
├── glslTransformer.js
├── jest.config.js
├── package.json
├── renovate.json
├── setupJestDomTests.js
├── src
├── css
│ ├── README.md
│ ├── components
│ │ ├── README.md
│ │ ├── footer.css
│ │ ├── index.css
│ │ ├── messages.css
│ │ ├── navbar.css
│ │ ├── tooltip.css
│ │ └── transfer-demo-grid.css
│ ├── debug-stack.css
│ ├── debug.css
│ ├── defaults.css
│ ├── font-face-rules.css
│ ├── helpers.css
│ ├── index.css
│ ├── layouts
│ │ ├── README.md
│ │ ├── box.css
│ │ ├── cluster.css
│ │ ├── index.css
│ │ └── stack.css
│ ├── media-queries.css
│ ├── typography.css
│ └── variables.css
├── fonts
│ └── Syne
│ │ ├── README.md
│ │ ├── Syne-Extra.ttf
│ │ ├── Syne-Extra.woff
│ │ ├── Syne-Extra.woff2
│ │ ├── Syne-Regular.ttf
│ │ ├── Syne-Regular.woff
│ │ └── Syne-Regular.woff2
├── glsl
│ ├── README.md
│ ├── fragmentShader.glsl
│ └── vertexShader.glsl
├── html
│ ├── documents
│ │ ├── 404.html
│ │ ├── README.md
│ │ ├── about.html
│ │ ├── index.html
│ │ ├── offscreen-bitmaprenderer.html
│ │ └── offscreen-transfer.html
│ └── fragments
│ │ ├── README.md
│ │ ├── footer.html
│ │ ├── head.html
│ │ └── navbar.html
├── js
│ ├── 404.js
│ ├── README.md
│ ├── about.js
│ ├── application.js
│ ├── bitmap-demo.js
│ ├── components
│ │ └── navbar.js
│ ├── constants.js
│ ├── helpers.js
│ ├── index.js
│ ├── transfer-demo.js
│ ├── vendor
│ │ ├── Detector.js
│ │ ├── OBJLoader2.js
│ │ ├── dat.gui.min.js
│ │ └── obj2
│ │ │ ├── shared
│ │ │ ├── MaterialHandler.js
│ │ │ └── MeshReceiver.js
│ │ │ └── worker
│ │ │ └── parallel
│ │ │ └── OBJLoader2Parser.js
│ ├── worker-actions.js
│ └── workers
│ │ ├── bitmap-worker.js
│ │ └── transfer-worker.js
├── models
│ ├── README.md
│ └── male02.obj
└── textures
│ ├── checkerboard.jpg
│ └── star.png
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "corejs": "3.0.0",
7 | "useBuiltIns": "entry"
8 | }
9 | ]
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | not dead
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*.{css,html,js}]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_size = 2
7 | indent_style = space
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | src/js/vendor
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["plugin:prettier/recommended"],
3 | "parserOptions": {
4 | "ecmaVersion": 6,
5 | "sourceType": "module"
6 | },
7 | "rules": {
8 | "require-jsdoc": "warn"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /build/*
2 | # unignore the favicon
3 | !/build/favicon.ico
4 |
5 | # dependencies
6 | /node_modules/
7 |
8 | # tests results
9 | /coverage/
10 | /cypress/videos/
11 |
12 | *.log
13 |
14 | # environment variables
15 | .envrc
16 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 11.10.1
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | package.json
2 | package-lock.json
3 | node_modules/
4 | dist/
5 |
--------------------------------------------------------------------------------
/.prettierrc.yaml:
--------------------------------------------------------------------------------
1 | printWidth: 80
2 | semi: true
3 | singleQuote: false
4 | trailingComma: "es5"
5 | tabWidth: 2
6 |
--------------------------------------------------------------------------------
/.stylelintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["stylelint-config-standard"],
3 | "rules": {
4 | "unit-whitelist": ["em", "rem", "vh", "vw", "%"]
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | sudo: false
3 | node_js:
4 | - "lts/*"
5 | - "stable"
6 | cache: yarn
7 | install:
8 | - yarn install
9 | - yarn global add codecov
10 | script:
11 | - yarn run ci
12 | after_success:
13 | - codecov
14 | notifications:
15 | email:
16 | on_success: change
17 | on_failure: always
18 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "christian-kohler.npm-intellisense",
4 | "davidanson.vscode-markdownlint",
5 | "EditorConfig.EditorConfig",
6 | "eg2.vscode-npm-script",
7 | "esbenp.prettier-vscode",
8 | "joelday.docthis",
9 | "mgmcdermott.vscode-language-babel",
10 | "slevesque.shader",
11 | "shinnn.stylelint"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "editor.rulers": [
4 | 80,
5 | 120
6 | ],
7 | "files.exclude": {
8 | "node_modules/": true
9 | }
10 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 jackaljack
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # three.js-es6-webpack-starter
2 |
3 | [](https://travis-ci.org/jackdbd/threejs-es6-webpack-starter) [](https://renovateapp.com/) [](https://github.com/prettier/prettier)
4 |
5 | Three.js ES6 starter project with a sane webpack configuration.
6 |
7 | 
8 |
9 | ## Features
10 |
11 | - ES6 support with [babel-loader](https://github.com/babel/babel-loader)
12 | - JS linting + code formatting with [eslint](https://eslint.org/) and [prettier](https://github.com/prettier/prettier)
13 | - Offscreen canvas rendering in a web worker with [OffscreenCanvas](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas)
14 | - CSS support with [style-loader](https://github.com/webpack-contrib/style-loader)
15 | and [css-loader](https://github.com/webpack-contrib/css-loader)
16 | - CSS linting with [stylelint](https://stylelint.io/)
17 | - Controls with [orbit-controls-es6](https://www.npmjs.com/package/orbit-controls-es6)
18 | - GUI with [dat.GUI](https://github.com/dataarts/dat.gui)
19 | - Tests with [jest](https://jestjs.io/en/)
20 | - Webpack configuration with:
21 | - [@packtracker/webpack-plugin](https://github.com/packtracker/webpack-plugin) (bundle sizes [here](https://app.packtracker.io/organizations/129/projects/110))
22 | - [circular-dependency-plugin](https://github.com/aackerman/circular-dependency-plugin)
23 | - [clean-webpack-plugin](https://github.com/johnagan/clean-webpack-plugin)
24 | - [compression-webpack-plugin](https://github.com/webpack-contrib/compression-webpack-plugin)
25 | - [duplicate-package-checker-webpack-plugin](https://github.com/darrenscerri/duplicate-package-checker-webpack-plugin)
26 | - [favicons-webpack-plugin](https://github.com/jantimon/favicons-webpack-plugin)
27 | - [html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin)
28 | - [mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin)
29 | - [speed-measure-webpack-plugin](https://github.com/stephencookdev/speed-measure-webpack-plugin)
30 | - [terser-webpack-plugin](https://github.com/webpack-contrib/terser-webpack-plugin/)
31 | - [webpack-bundle-analyzer](https://github.com/th0r/webpack-bundle-analyzer)
32 | - [webpack-glsl-loader](https://github.com/grieve/webpack-glsl-loader)
33 | - [worker-loader](https://github.com/webpack-contrib/worker-loader)
34 |
35 | ## Installation
36 |
37 | ```shell
38 | git clone git@github.com:jackdbd/threejs-es6-webpack-starter.git
39 | cd threejs-es6-webpack-starter
40 | yarn
41 | ```
42 |
43 | ## Usage (development)
44 |
45 | Run `webpack-dev-server` (js/css bundles will be served from memory)
46 |
47 | ```shell
48 | yarn start
49 | ```
50 |
51 | Go to `localhost:8080` to see your project live!
52 |
53 | ## Usage (production)
54 |
55 | Generate all js/css bundles
56 |
57 | ```shell
58 | yarn build
59 | ```
60 |
61 | ## Other
62 |
63 | Analyze webpack bundles offline:
64 |
65 | ```shell
66 | yarn build # to generate build/stats.json
67 | yarn stats # uses webpack-bundle-analyzer as CLI
68 | ```
69 |
70 | or push to a CI (e.g. [Travis CI](https://travis-ci.com/)), let it build your project and analyze your bundles online at [packtracker.io](https://packtracker.io/).
71 |
72 | Check outdated dependencies with [npm-check-updates](https://github.com/tjunnone/npm-check-updates):
73 |
74 | ```shell
75 | yarn ncu
76 | ```
77 |
78 | Update all outdated dependencies at once:
79 |
80 | ```shell
81 | yarn ncuu
82 | ```
83 |
84 | Or let [updtr](https://github.com/peerigon/updtr) update all your dependencies for you:
85 |
86 | ```shell
87 | yarn updtr
88 | ```
89 |
90 | ## Credits
91 |
92 | The setup of this starter project was inspired by two snippets on Codepen: [this one](http://codepen.io/mo4_9/pen/VjqRQX) and [this one](https://codepen.io/iamphill/pen/jPYorE).
93 |
94 | I understood how to work with lights and camera helpers thanks to
95 | [this snippet](http://jsfiddle.net/f17Lz5ux/5131/) on JSFiddle.
96 |
97 | The code for `vertexShader.glsl` and `fragmentShader.glsl` is taken from
98 | [this blog post](http://blog.cjgammon.com/threejs-custom-shader-material).
99 |
100 | The star used in the particle system is the PNG preview of [this image](https://commons.wikimedia.org/wiki/File:Star_icon-72a7cf.svg) by Offnfopt
101 | (Public domain or CC0, via Wikimedia Commons).
102 |
--------------------------------------------------------------------------------
/__mocks__/fileMock.js:
--------------------------------------------------------------------------------
1 | module.exports = "test-file-stub";
2 |
--------------------------------------------------------------------------------
/__mocks__/styleMock.js:
--------------------------------------------------------------------------------
1 | module.exports = {};
2 |
--------------------------------------------------------------------------------
/__tests__/application.js:
--------------------------------------------------------------------------------
1 | import { Application } from "../src/js/application";
2 |
3 | const prepareDOM = () => {
4 | const main = document.createElement("main");
5 | const figure = document.createElement("figure");
6 | const outerDiv = document.createElement("div");
7 | outerDiv.classList.add("canvas-container-outer");
8 | const innerDiv = document.createElement("div");
9 | innerDiv.setAttribute("class", "canvas-container-inner");
10 | outerDiv.appendChild(innerDiv);
11 | figure.appendChild(outerDiv);
12 | main.appendChild(figure);
13 | document.body.appendChild(main);
14 | };
15 |
16 | describe("Three.js application", () => {
17 | let windowAlert;
18 |
19 | beforeAll(() => {
20 | // alert is not available in Jest DOM, so we provide a mock implementation.
21 | windowAlert = jest.spyOn(window, "alert");
22 | windowAlert.mockImplementation(() => {});
23 | });
24 |
25 | beforeEach(() => {
26 | prepareDOM();
27 | });
28 |
29 | afterEach(() => {
30 | windowAlert.mockReset();
31 | // Remove all body's children to make sure the tests are independent
32 | const body = document.querySelector("body");
33 | while (body.firstChild) {
34 | body.removeChild(body.firstChild);
35 | }
36 | });
37 |
38 | it("shows an error message when WebGL is not supported", () => {
39 | new Application();
40 | const container = document.querySelector("main .canvas-container-inner");
41 | const el = container.firstChild;
42 | expect(el.id).toBe("webgl-error-message");
43 | const message =
44 | "Your browser does not seem to support WebGL. Find out how to get it here.";
45 | expect(el).toHaveTextContent(message);
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/config/README.md:
--------------------------------------------------------------------------------
1 | # Config
2 |
3 | Webpack configuration files for `development` and `production` mode.
4 |
--------------------------------------------------------------------------------
/config/webpack.common.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const CircularDependencyPlugin = require("circular-dependency-plugin");
3 | const { DefinePlugin } = require("webpack");
4 | const DuplicatePackageCheckerPlugin = require("duplicate-package-checker-webpack-plugin");
5 | const FaviconsWebpackPlugin = require("favicons-webpack-plugin");
6 | const HtmlWebpackPlugin = require("html-webpack-plugin");
7 | const MiniCssExtractPlugin = require("mini-css-extract-plugin");
8 | const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
9 | const WorkerPlugin = require("worker-plugin");
10 |
11 | // The path used in each rule is resolved starting from `context`.
12 | // GOTCHA: use path.resolve for the path to the source files, and path.join for
13 | // the output files.
14 | const rules = [
15 | // Rule for html documents and document fragments
16 | {
17 | test: /\.html$/,
18 | include: [path.resolve("src", "html")],
19 | loader: "html-loader",
20 | options: {
21 | // Required to use expressions in HTML documents and fragments.
22 | // https://webpack.js.org/loaders/html-loader/#interpolate
23 | interpolate: true,
24 | minimize: {
25 | removeComments: true,
26 | },
27 | },
28 | },
29 | // Rule for JS files. Web workers are bundled by worker-plugin.
30 | {
31 | test: /\.(js|jsx)$/,
32 | include: [path.resolve("src", "js")],
33 | exclude: [
34 | path.resolve("node_modules"),
35 | path.resolve("src", "js", "workers"),
36 | ],
37 | use: {
38 | loader: "babel-loader",
39 | },
40 | },
41 | // Rule for stylesheets (.sass, .scss, .css)
42 | {
43 | test: /\.(sa|sc|c)ss$/,
44 | include: [path.resolve("src", "css")],
45 | use: [
46 | {
47 | loader: MiniCssExtractPlugin.loader,
48 | options: {
49 | // avoid using CSS modules
50 | modules: false,
51 | sourceMap: true,
52 | },
53 | },
54 | "css-loader",
55 | "sass-loader",
56 | ],
57 | },
58 | // Rule for shaders
59 | {
60 | test: /\.glsl$/,
61 | use: [
62 | {
63 | loader: "webpack-glsl-loader",
64 | },
65 | ],
66 | },
67 | // Rule for font files
68 | {
69 | test: /\.(ttf|woff|woff2)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
70 | include: [path.resolve("src", "fonts")],
71 | use: {
72 | loader: "file-loader",
73 | options: {
74 | name: path.join("fonts", "[name].[ext]"),
75 | },
76 | },
77 | },
78 | // Rule for 3D model files
79 | {
80 | test: /\.(obj)$/,
81 | include: [path.resolve("src", "models")],
82 | use: {
83 | loader: "file-loader",
84 | options: {
85 | name: path.join("models", "[name].[ext]"),
86 | },
87 | },
88 | },
89 | // Rule for textures (images)
90 | {
91 | test: /.(jpe?g|png)$/i,
92 | include: path.resolve("src", "textures"),
93 | loaders: [
94 | {
95 | loader: "file-loader",
96 | options: {
97 | name: path.join("textures", "[name].[hash].[ext]"),
98 | },
99 | },
100 | {
101 | loader: "image-webpack-loader",
102 | query: {
103 | gifsicle: {
104 | interlaced: false,
105 | },
106 | mozjpeg: {
107 | progressive: true,
108 | quality: 85,
109 | },
110 | pngquant: {
111 | quality: [0.65, 0.9],
112 | speed: 4,
113 | },
114 | },
115 | },
116 | ],
117 | },
118 | ];
119 |
120 | /**
121 | * Webpack config shared between development and production environments.
122 | */
123 | const commonConfigFn = (env = {}, argv = {}) => {
124 | if (!env.hasOwnProperty("publicUrl")) {
125 | throw new Error("env must have the `publicUrl` property");
126 | }
127 | if (!argv.hasOwnProperty("mode")) {
128 | throw new Error(
129 | "argv must have the `mode` property ('development' or 'production')"
130 | );
131 | }
132 | console.log(`Prepare ${argv.mode.toUpperCase()} build`);
133 |
134 | const APP_NAME = `Three.js ES6 Webpack 4 Project Starter (${argv.mode})`;
135 | const PUBLIC_URL = env.publicUrl;
136 |
137 | // In production html-webpack-plugin should automatically minify HTML
138 | // documents with html-minifier-terser and these parameters, but it doesn't.
139 | // Setting minify to true also does not work for me; only setting minify as an
140 | // object does.
141 | // https://github.com/jantimon/html-webpack-plugin#minification
142 | const minify =
143 | argv.mode === "production"
144 | ? {
145 | collapseWhitespace: true,
146 | removeComments: true,
147 | removeRedundantAttributes: true,
148 | removeScriptTypeAttributes: true,
149 | removeStyleLinkTypeAttributes: true,
150 | useShortDoctype: true,
151 | }
152 | : false;
153 |
154 | const plugins = [
155 | new CircularDependencyPlugin({
156 | exclude: /node_modules/,
157 | failOnError: true,
158 | }),
159 | new DefinePlugin({
160 | APP_NAME: JSON.stringify(APP_NAME),
161 | AUTHOR: JSON.stringify("Giacomo Debidda"),
162 | PUBLIC_URL: JSON.stringify(PUBLIC_URL),
163 | }),
164 | new DuplicatePackageCheckerPlugin({
165 | emitError: false,
166 | showHelp: true,
167 | strict: false,
168 | verbose: true,
169 | }),
170 | new HtmlWebpackPlugin({
171 | chunks: ["commons", "home", "runtime", "styles", "vendor"],
172 | filename: "index.html",
173 | hash: false,
174 | minify,
175 | template: path.resolve("src", "html", "documents", "index.html"),
176 | }),
177 | new HtmlWebpackPlugin({
178 | chunks: ["bitmap-demo", "commons", "runtime", "styles", "vendor"],
179 | filename: "offscreen-bitmaprenderer.html",
180 | hash: false,
181 | minify,
182 | template: path.resolve(
183 | "src",
184 | "html",
185 | "documents",
186 | "offscreen-bitmaprenderer.html"
187 | ),
188 | }),
189 | new HtmlWebpackPlugin({
190 | chunks: ["commons", "runtime", "styles", "transfer-demo", "vendor"],
191 | filename: "offscreen-transfer.html",
192 | hash: false,
193 | minify,
194 | template: path.resolve(
195 | "src",
196 | "html",
197 | "documents",
198 | "offscreen-transfer.html"
199 | ),
200 | }),
201 | new HtmlWebpackPlugin({
202 | chunks: ["about", "commons", "runtime", "styles"],
203 | filename: "about.html",
204 | hash: false,
205 | minify,
206 | template: path.resolve("src", "html", "documents", "about.html"),
207 | }),
208 | new HtmlWebpackPlugin({
209 | chunks: ["404", "commons", "runtime", "styles"],
210 | filename: "404.html",
211 | hash: false,
212 | minify,
213 | template: path.resolve("src", "html", "documents", "404.html"),
214 | }),
215 | // html-webpack-plugin must come BEFORE favicons-webpack-plugin in the
216 | // plugins array.
217 | // https://github.com/jantimon/favicons-webpack-plugin#html-injection
218 | new FaviconsWebpackPlugin({
219 | // `inject: true` seems not working, so I have a
document fragment
220 | // where I manually reference the public path to the favicons.
221 | inject: false,
222 | logo: path.resolve("src", "textures", "star.png"),
223 | prefix: "favicons/",
224 | title: APP_NAME,
225 | }),
226 | new MiniCssExtractPlugin({
227 | chunkFilename: "[name].[contenthash].css",
228 | filename: "[name].[contenthash].css",
229 | }),
230 | // Create a bundle for each web worker. This is cool, but at the moment it
231 | // seems not possible to use a fixed name for this bundle. It would be
232 | // useful because with a fixed filename we could inject a resource hint like
233 | //
234 | // in the template . Keep in mind that if the name has a hash (for
235 | // cache busting) we need something like WebpackManifestPlugin to find the
236 | // generated filename.
237 | // https://github.com/GoogleChromeLabs/worker-plugin/issues/19
238 | // https://web.dev/module-workers/#preload-workers-with-modulepreload
239 | new WorkerPlugin(),
240 | ];
241 |
242 | const config = {
243 | context: path.resolve(__dirname, ".."),
244 |
245 | // The path to each entry point is resolved starting from `context`.
246 | entry: {
247 | "404": path.resolve("src", "js", "404.js"),
248 | about: path.resolve("src", "js", "about.js"),
249 | "bitmap-demo": path.resolve("src", "js", "bitmap-demo.js"),
250 | home: path.resolve("src", "js", "index.js"),
251 | "transfer-demo": path.resolve("src", "js", "transfer-demo.js"),
252 | },
253 | mode: argv.mode,
254 | module: {
255 | rules,
256 | },
257 | output: {
258 | // For HTTP cache busting we want an hash to appear in the asset filename.
259 | // We could use html-webpack-plugin `hash: true` to have the hash added as
260 | // a query string. However, `some-file.some-hash.js` is a better idea than
261 | // `some-file.js?some-hash`. Here is why:
262 | // http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/
263 | // So we add the hash in the filename here and use `hash: false` with
264 | // html-webpack-plugin.
265 | filename: "[name].[hash].js",
266 | // The output path is resolved starting from `context`
267 | path: path.resolve("build"),
268 | // publicPath: "/",
269 | sourceMapFilename: "[file].map",
270 | },
271 | plugins,
272 | performance: {
273 | assetFilter: assetFilename => {
274 | // Silence warnings for big source maps (default) and font files.
275 | // To reduce .ttf file size, check the link below.
276 | // https://www.cnx-software.com/2010/02/19/reducing-truetype-font-file-size-for-embedded-systems/
277 | return !/\.map$/.test(assetFilename) && !assetFilename.endsWith(".ttf");
278 | },
279 | hints: "warning",
280 | },
281 | resolve: {
282 | alias: {
283 | // orbit-controls-es6 declares a version of three different from the one
284 | // used by this application. This would cause three to be duplicated in
285 | // the bundle. One way to avoid this issue is to use resolve.alias.
286 | // With resolve.alias we are telling Webpack to route any package
287 | // references to a single specified path.
288 | // Note: Aliasing packages with different major versions may break your
289 | // app. Use only if you're sure that all required versions are
290 | // compatible, at least in the context of your app
291 | // https://github.com/darrenscerri/duplicate-package-checker-webpack-plugin#resolving-duplicate-packages-in-your-bundle
292 | three: path.resolve("node_modules", "three"),
293 | },
294 | extensions: [".js"],
295 | },
296 | target: "web",
297 | };
298 |
299 | // console.log("=== Webpack config ===", config);
300 | const smp = new SpeedMeasurePlugin({
301 | // granularLoaderData: true,
302 | });
303 |
304 | return smp.wrap(config);
305 | };
306 |
307 | module.exports = {
308 | commonConfigFn,
309 | };
310 |
--------------------------------------------------------------------------------
/config/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const { HotModuleReplacementPlugin } = require("webpack");
2 | const merge = require("webpack-merge");
3 |
4 | const { commonConfigFn } = require("./webpack.common");
5 |
6 | const devServer = {
7 | compress: true,
8 | host: "localhost",
9 | inline: true,
10 | open: true,
11 | // openPage: ["bitmap-canvas-demo.html", "transfer-canvas-demo.html"],
12 | openPage: "index.html",
13 | port: 8080,
14 | stats: {
15 | chunks: false,
16 | colors: true,
17 | modules: false,
18 | reasons: true,
19 | },
20 | // writeToDisk: true,
21 | };
22 |
23 | module.exports = (env = {}, argv = {}) => {
24 | const devEnv = Object.assign(env, { publicUrl: "" });
25 | const devArgv = Object.assign(argv, { mode: "development" });
26 |
27 | const plugins = [new HotModuleReplacementPlugin()];
28 |
29 | const finalWebpackConfig = merge(commonConfigFn(devEnv, argv), {
30 | devServer,
31 | devtool: "cheap-source-map",
32 | mode: devArgv.mode,
33 | plugins,
34 | });
35 | // console.log("=== finalWebpackConfig ===", finalWebpackConfig);
36 | return finalWebpackConfig;
37 | };
38 |
--------------------------------------------------------------------------------
/config/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const merge = require("webpack-merge");
3 | const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
4 | const { CleanWebpackPlugin } = require("clean-webpack-plugin");
5 | const CompressionPlugin = require("compression-webpack-plugin");
6 | const ManifestPlugin = require("webpack-manifest-plugin");
7 | const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
8 | const PacktrackerPlugin = require("@packtracker/webpack-plugin");
9 | const { ProgressPlugin } = require("webpack");
10 | const TerserPlugin = require("terser-webpack-plugin");
11 |
12 | const { commonConfigFn } = require("./webpack.common");
13 |
14 | const optimization = {
15 | minimizer: [
16 | // Minify JS
17 | new TerserPlugin({
18 | // Enable file caching (doesn't work with webpack 5)
19 | cache: true,
20 | extractComments: true,
21 | // Use multi-process parallel running (speeds up the build)
22 | parallel: true,
23 | // Use source maps to map error message locations to modules (slows down the build)
24 | sourceMap: true,
25 | }),
26 | // Minify CSS. Strangely enough, I need to re-instantiate this plugin in the
27 | // plugins section.
28 | // optimize-css-assets-webpack-plugin uses cssnano as css processor
29 | new OptimizeCSSAssetsPlugin({
30 | cssProcessor: require("cssnano"),
31 | // https://cssnano.co/optimisations/
32 | cssProcessorPluginOptions: {
33 | preset: ["default", { discardComments: { removeAll: true } }],
34 | },
35 | }),
36 | ],
37 | // https://webpack.js.org/configuration/optimization/#optimizationmoduleids
38 | // moduleIds: "hashed",
39 | // Creates a single chunk that contains the Webpack's runtime and it is shared
40 | // among all generated chunks.
41 | // https://webpack.js.org/configuration/optimization/#optimizationruntimechunk
42 | runtimeChunk: "single",
43 | // Configuration for SplitChunksPlugin (webpack internal plugin).
44 | // https://webpack.js.org/plugins/split-chunks-plugin/#split-chunks-example-2
45 | splitChunks: {
46 | cacheGroups: {
47 | // Do not enforce the creation of the chunks belonging to the `commons`
48 | // cacheGroup, but create a chunk for this cachGroup only if it is shared
49 | // by 2+ chunks, no matter how small this chunk is.
50 | // https://webpack.js.org/plugins/split-chunks-plugin/#splitchunksminsize
51 | commons: {
52 | minChunks: 2,
53 | minSize: 0,
54 | // https://webpack.js.org/plugins/split-chunks-plugin/#splitchunksname
55 | name: "commons",
56 | priority: 0,
57 | // It would be cool to indicate in this chunk's name the name of the
58 | // chunks where it is used, but this would make the configuration of
59 | // HtmlWebpackPlugin more tedious and hard to maintain.
60 | // name(module, chunks, cacheGroupKey) {
61 | // const moduleFileName = module
62 | // .identifier()
63 | // .split("/")
64 | // .reduceRight(item => item);
65 | // const allChunksNames = chunks.map(item => item.name).join("~");
66 | // return `${cacheGroupKey}-[${allChunksNames}]-${moduleFileName}`;
67 | // },
68 | test: /\.js$/,
69 | },
70 | // Most of the pages use the same stylesheets, so enforce the creation of
71 | // a single chunk for all CSS files.
72 | // https://webpack.js.org/plugins/mini-css-extract-plugin/#extracting-all-css-in-a-single-file
73 | styles: {
74 | enforce: true,
75 | name: "styles",
76 | priority: 10,
77 | test: /\.css$/,
78 | },
79 | // Enforce the creation of a single chunk for vendor code (vendor), so
80 | // it's easier to visually inspect how big our dependencies are (e.g. by
81 | // using webpack-bundle-analyzer).
82 | vendor: {
83 | enforce: true,
84 | name: "vendor",
85 | priority: 20,
86 | // test: /[\\/]node_modules[\\/]/,
87 | test(module, chunks) {
88 | // `module.resource` contains the absolute path of the file on disk.
89 | // Note the usage of `path.sep` instead of / or \, for cross-platform
90 | // compatibility.
91 | // console.log("=== module.resource ===", module.resource);
92 | return (
93 | module.resource &&
94 | module.resource.endsWith(".js") &&
95 | (module.resource.includes(`${path.sep}node_modules${path.sep}`) ||
96 | module.resource.includes("vendor"))
97 | );
98 | },
99 | },
100 | },
101 | // Include all types of chunks (async and non-async chunks).
102 | // https://webpack.js.org/plugins/split-chunks-plugin/#splitchunkschunks
103 | chunks: "all",
104 | // It is recommended to set splitChunks.name to false for production builds
105 | // so that it doesn't change names unnecessarily.
106 | name: false,
107 | },
108 | };
109 |
110 | module.exports = (env = {}, argv = {}) => {
111 | const prodEnv = Object.assign(env, {
112 | publicUrl: "https://jackdbd.github.io/threejs-es6-webpack-starter",
113 | });
114 | const prodArgv = Object.assign(argv, { mode: "production" });
115 |
116 | const plugins = [
117 | new CleanWebpackPlugin({
118 | cleanStaleWebpackAssets: true,
119 | verbose: true,
120 | }),
121 | new CompressionPlugin({
122 | algorithm: "gzip",
123 | filename: "[path].gz[query]",
124 | test: /\.(css|html|js|svg|ttf)$/,
125 | threshold: 10240,
126 | minRatio: 0.8,
127 | }),
128 | new CompressionPlugin({
129 | algorithm: "brotliCompress",
130 | compressionOptions: { level: 11 },
131 | filename: "[path].br[query]",
132 | test: /\.(css|html|js|svg|ttf)$/,
133 | threshold: 10240,
134 | minRatio: 0.8,
135 | }),
136 | new ProgressPlugin({
137 | activeModules: true,
138 | profile: true,
139 | }),
140 | new OptimizeCSSAssetsPlugin({
141 | cssProcessor: require("cssnano"),
142 | cssProcessorPluginOptions: {
143 | preset: ["default", { discardComments: { removeAll: true } }],
144 | },
145 | }),
146 | new BundleAnalyzerPlugin({
147 | analyzerMode: "disabled",
148 | generateStatsFile: true,
149 | statsFilename: "stats.json",
150 | }),
151 | new PacktrackerPlugin({
152 | // https://docs.packtracker.io/faq#why-cant-the-plugin-determine-my-branch-name
153 | branch: process.env.TRAVIS_BRANCH,
154 | fail_build: true,
155 | project_token: "2464bed1-d810-4af6-a615-877420f902b2",
156 | // upload stats.json only in CI
157 | upload: process.env.CI === "true",
158 | }),
159 | new ManifestPlugin(),
160 | ];
161 |
162 | const finalWebpackConfig = merge(commonConfigFn(prodEnv, prodArgv), {
163 | devtool: "source-map",
164 | mode: prodArgv.mode,
165 | optimization,
166 | plugins,
167 | });
168 | // console.log("=== finalWebpackConfig ===", finalWebpackConfig.plugins[8]);
169 | return finalWebpackConfig;
170 | };
171 |
--------------------------------------------------------------------------------
/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "baseUrl": "http://localhost:8080",
3 | "env": {
4 | "test-environment-variable": "123"
5 | },
6 | "projectId": "7trrtp"
7 | }
8 |
--------------------------------------------------------------------------------
/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
--------------------------------------------------------------------------------
/cypress/integration/application/app.spec.js:
--------------------------------------------------------------------------------
1 | import { dragFromTo, zoomInAt } from "../../utils";
2 |
3 | describe("Three.js application", () => {
4 | const viewportWidth = 800;
5 | const viewportHeight = 600;
6 |
7 | beforeEach(() => {
8 | cy.visit("/");
9 | cy.viewport(viewportWidth, viewportHeight);
10 | // console.log(
11 | // "test-environment-variable",
12 | // Cypress.env("test-environment-variable")
13 | // );
14 | // console.log("browser", Cypress.browser);
15 | });
16 |
17 | context("Link to About page", () => {
18 | const selector = "[data-cy=link-to-about-page]";
19 | it("is in the DOM", () => {
20 | cy.get(selector)
21 | .invoke("attr", "href")
22 | .should("contain", "about");
23 | });
24 |
25 | it("navigates to about.html if clicked ", () => {
26 | cy.get(selector).click();
27 | cy.url().should("include", "about.html");
28 | });
29 | });
30 |
31 | context("Tooltip", () => {
32 | const selector = "[data-cy=tooltip]";
33 | it("exists but it is invisible at the startup", () => {
34 | cy.get(selector)
35 | .should("exist")
36 | .and("be.hidden");
37 | });
38 | });
39 |
40 | context("WebGL canvas' container", () => {
41 | const selector = "[data-cy=canvas-container]";
42 |
43 | it("exists and it is visible", () => {
44 | cy.get(selector)
45 | .should("exist")
46 | .and("be.visible");
47 | });
48 |
49 | it("renders the view from the top after a mouse drag towards the bottom", () => {
50 | dragFromTo(selector, [100, 100], [100, 400]);
51 | });
52 |
53 | it("zooms in the cube in the center", () => {
54 | const x = viewportWidth / 2;
55 | const y = viewportHeight / 2;
56 |
57 | const mousemoveOptions = {
58 | clientX: x,
59 | clientY: y,
60 | };
61 |
62 | const wheelOptions = {
63 | clientX: x,
64 | clientY: y,
65 | deltaX: 0,
66 | deltaY: -53,
67 | deltaZ: 0,
68 | deltaMode: 0,
69 | };
70 |
71 | // TODO: I tried to assert that when mousemove-ing on the cube, the
72 | // tooltip becomes visible
73 | cy.get(selector)
74 | .trigger("mousemove", mousemoveOptions)
75 | .trigger("wheel", wheelOptions)
76 | .trigger("mousemove", mousemoveOptions);
77 | });
78 | });
79 | });
80 |
--------------------------------------------------------------------------------
/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example plugins/index.js can be used to load plugins
3 | //
4 | // You can change the location of this file or turn off loading
5 | // the plugins file with the 'pluginsFile' configuration option.
6 | //
7 | // You can read more here:
8 | // https://on.cypress.io/plugins-guide
9 | // ***********************************************************
10 |
11 | // This function is called when a project is opened or re-opened (e.g. due to
12 | // the project's config changing)
13 |
14 | module.exports = (on, config) => {
15 | // `on` is used to hook into various events Cypress emits
16 | // `config` is the resolved Cypress config
17 | }
18 |
--------------------------------------------------------------------------------
/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add("login", (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This is will overwrite an existing command --
25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
26 |
--------------------------------------------------------------------------------
/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/cypress/utils.js:
--------------------------------------------------------------------------------
1 | export const dragFromTo = (selector, fromPos, toPos) => {
2 | const [x0, y0] = fromPos;
3 | const [x1, y1] = toPos;
4 | cy.get(selector)
5 | .trigger("mousedown", { button: 0, buttons: 1, clientX: x0, clientY: y0 })
6 | .trigger("mousemove", { button: 0, buttons: 1, clientX: x1, clientY: y1 })
7 | .trigger("mouseup", { button: 0, buttons: 0 });
8 | };
9 |
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackdbd/threejs-es6-webpack-starter/e4b00bbbfdb93016fbd486ec4a4b3456587b61fd/demo.gif
--------------------------------------------------------------------------------
/glslTransformer.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 |
3 | /**
4 | * Transformer for `.glsl` files.
5 | *
6 | * A jest transformer is a module that provides a synchronous function for
7 | * transforming source files.
8 | * In jest tests we don't use the original `.glsl` files used in the app (like
9 | * we don't any other static asset such as `css` stylesheets and `png` images).
10 | *
11 | * To recap, in order to handle any static asset in jest we have two options:
12 | *
13 | * 1) mock the original file by providing a file stub (specified in the
14 | * `moduleNameMapper` config option);
15 | * 2) mock the original file by transforming it using a trasformer (specified in
16 | * the `transform` config option).
17 | *
18 | * @see https://jestjs.io/docs/en/configuration#transform-object-string-pathtotransformer-pathtotransformer-object
19 | */
20 | const transformer = {
21 | process(src, filename, jestConfig, options) {
22 | const baseFilename = JSON.stringify(path.basename(filename));
23 | const msg = `${baseFilename} was transformed because it matched a file pattern specified in jest config transform and jest automock was set to ${jestConfig.automock}.`;
24 | console.log(msg);
25 | return `module.exports = ${baseFilename};`;
26 | },
27 | };
28 |
29 | module.exports = transformer;
30 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | const config = {
2 | automock: false,
3 | collectCoverage: true,
4 | collectCoverageFrom: ["/src/js/*.{js,ts}"],
5 | coverageDirectory: "/coverage/",
6 | /*
7 | * Configure Jest to provide stubs for static assets such as stylesheets and
8 | * images. Usually, these files aren't particularly useful in tests so we can
9 | * mock them out.
10 | */
11 | moduleNameMapper: {
12 | "\\.(jpg|jpeg|png)$": "/__mocks__/fileMock.js",
13 | "\\.(css)$": "/__mocks__/styleMock.js",
14 | },
15 | modulePathIgnorePatterns: [
16 | "/build/",
17 | "/coverage/",
18 | "/node_modules/",
19 | ],
20 | runner: "jest-runner",
21 | // Setup files to run immediately before executing the test code.
22 | setupFiles: [],
23 | /**
24 | * Setup files to run immediately after the test framework has been installed
25 | * in the environment.
26 | */
27 | setupFilesAfterEnv: ["/setupJestDomTests.js"],
28 | testEnvironment: "jsdom",
29 | testURL: "http://localhost",
30 | testRegex: "__tests__/.*\\.(js|ts)$",
31 | transform: {
32 | "^.+\\.[t|j]sx?$": "babel-jest",
33 | "\\.(glsl)$": "/glslTransformer.js",
34 | },
35 | transformIgnorePatterns: ["[/\\\\]node_modules[/\\\\].+\\.(js|ts)$"],
36 | watchPlugins: [],
37 | };
38 |
39 | module.exports = config;
40 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "threejs-es6-webpack-starter",
3 | "description": "A starter project for Three.js with support for ES6 and Webpack 4",
4 | "author": "jackdbd (http://www.giacomodebidda.com/)",
5 | "homepage": "https://jackdbd.github.io/threejs-es6-webpack-starter",
6 | "repository": "git@github.com:jackdbd/threejs-es6-webpack-starter.git",
7 | "version": "5.1.0",
8 | "main": "./src/js/index.js",
9 | "license": "MIT",
10 | "private": true,
11 | "engines": {
12 | "node": ">=11.10.1"
13 | },
14 | "scripts": {
15 | "build": "yarn prebuild && webpack --config ./config/webpack.prod.js",
16 | "ci": "yarn test && yarn coverage && yarn build",
17 | "coverage": "codecov",
18 | "deploy": "gh-pages -d build",
19 | "lint": "yarn lint:css && yarn lint:js",
20 | "lint:css": "stylelint --config .stylelintrc.json --formatter verbose './src/**/*.css'",
21 | "lint:js": "eslint 'src/**/*.{js,ts}' --fix --debug",
22 | "ncu": "ncu",
23 | "ncuu": "ncu --upgrade",
24 | "nuke": "rimraf node_modules && rm yarn.lock",
25 | "prebuild": "yarn lint",
26 | "predeploy": "yarn test && yarn build",
27 | "start": "webpack-dev-server --config ./config/webpack.dev.js",
28 | "static": "chromium-browser build/index.html",
29 | "stats": "webpack-bundle-analyzer build/stats.json --port 8888",
30 | "test": "yarn test:unit",
31 | "test:e2e": "concurrently \"yarn start\" \"yarn test:e2e:cli\" --kill-others",
32 | "test:unit": "jest --verbose --no-cache",
33 | "test:e2e:cli": "cypress run --browser chromium --key da1be047-979c-4b0f-bacf-fef804c7c6bb",
34 | "test:e2e:dashboard": "cypress open",
35 | "updtr": "updtr --use yarn --test-stdout",
36 | "webhint": "hint https://jackdbd.github.io/threejs-es6-webpack-starter/",
37 | "webhint:report": "python -m webbrowser ~/repos/threejs-es6-webpack-starter/hint-report/https-jackdbd-github-io-threejs-es6-webpack-starter/index.html"
38 | },
39 | "dependencies": {
40 | "gltf-loader-ts": "0.3.1",
41 | "orbit-controls-es6": "2.0.1",
42 | "three": "0.113.2",
43 | "three.interaction": "0.2.2"
44 | },
45 | "devDependencies": {
46 | "@babel/core": "7.8.6",
47 | "@babel/preset-env": "7.8.6",
48 | "@hint/configuration-web-recommended": "8.0.2",
49 | "@packtracker/webpack-plugin": "2.2.0",
50 | "@testing-library/jest-dom": "5.1.1",
51 | "babel-jest": "25.1.0",
52 | "babel-loader": "8.0.6",
53 | "circular-dependency-plugin": "5.2.0",
54 | "clean-webpack-plugin": "3.0.0",
55 | "compression-webpack-plugin": "3.1.0",
56 | "concurrently": "5.1.0",
57 | "css-loader": "3.4.2",
58 | "cypress": "4.1.0",
59 | "duplicate-package-checker-webpack-plugin": "3.0.0",
60 | "eslint": "6.8.0",
61 | "eslint-config-prettier": "6.10.0",
62 | "eslint-plugin-prettier": "3.1.2",
63 | "favicons-webpack-plugin": "2.1.0",
64 | "file-loader": "4.2.0",
65 | "gh-pages": "2.2.0",
66 | "hint": "6.0.3",
67 | "html-loader": "0.5.5",
68 | "html-webpack-plugin": "3.2.0",
69 | "image-webpack-loader": "6.0.0",
70 | "jest": "25.1.0",
71 | "jest-cli": "25.1.0",
72 | "jest-dom": "4.0.0",
73 | "mini-css-extract-plugin": "0.9.0",
74 | "node-sass": "4.13.1",
75 | "npm-check-updates": "4.0.2",
76 | "optimize-css-assets-webpack-plugin": "5.0.3",
77 | "prettier": "1.19.1",
78 | "rimraf": "3.0.2",
79 | "sass-loader": "8.0.2",
80 | "speed-measure-webpack-plugin": "1.3.1",
81 | "style-loader": "1.1.3",
82 | "stylelint": "13.2.0",
83 | "stylelint-config-standard": "20.0.0",
84 | "terser-webpack-plugin": "2.3.5",
85 | "updtr": "3.1.0",
86 | "webpack": "4.41.6",
87 | "webpack-bundle-analyzer": "3.6.0",
88 | "webpack-cli": "3.3.11",
89 | "webpack-dev-server": "3.10.3",
90 | "webpack-glsl-loader": "1.0.1",
91 | "webpack-manifest-plugin": "2.2.0",
92 | "webpack-merge": "4.2.2",
93 | "worker-plugin": "3.2.0"
94 | },
95 | "keywords": [
96 | "babel",
97 | "boilerplate",
98 | "es6",
99 | "glsl",
100 | "threejs",
101 | "three.js",
102 | "webpack",
103 | "webgl"
104 | ],
105 | "bugs": {
106 | "url": "https://github.com/jackdbd/threejs-es6-webpack-starter/issues"
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["config:base", "schedule:monthly"]
3 | }
4 |
--------------------------------------------------------------------------------
/setupJestDomTests.js:
--------------------------------------------------------------------------------
1 | import "@testing-library/jest-dom/extend-expect";
2 |
--------------------------------------------------------------------------------
/src/css/README.md:
--------------------------------------------------------------------------------
1 | # CSS
2 |
3 | These files are organized in this way:
4 |
5 | - [layouts/](./layouts/README.md): layout primitives whose only concern is the page/component layout.
6 | - [components/](./components/README.md): visual primitives.
7 | - [debug.css](./debug.css): file to inspect the page without all of its styles (i.e. aestetics) but with no change to its layout.
8 |
--------------------------------------------------------------------------------
/src/css/components/README.md:
--------------------------------------------------------------------------------
1 | # Components
2 |
3 | Set of visual primitives.
4 |
--------------------------------------------------------------------------------
/src/css/components/footer.css:
--------------------------------------------------------------------------------
1 | footer {
2 | display: flex;
3 | justify-content: center;
4 | }
5 |
--------------------------------------------------------------------------------
/src/css/components/index.css:
--------------------------------------------------------------------------------
1 | @import "./footer.css";
2 | @import "./messages.css";
3 | @import "./navbar.css";
4 | @import "./tooltip.css";
5 | @import "./transfer-demo-grid.css";
6 |
--------------------------------------------------------------------------------
/src/css/components/messages.css:
--------------------------------------------------------------------------------
1 | .messages {
2 | grid-area: messages;
3 | }
4 |
5 | .messages ol {
6 | background-color: white;
7 | margin: 0;
8 | max-height: 25vh;
9 | overflow: auto;
10 | }
11 |
--------------------------------------------------------------------------------
/src/css/components/navbar.css:
--------------------------------------------------------------------------------
1 | .nav-grid {
2 | display: grid;
3 | grid-template-areas: "home hamburger";
4 | }
5 |
6 | .nav-grid-pages {
7 | display: none;
8 | grid-area: pages;
9 | }
10 |
11 | .nav-grid--expanded {
12 | grid-template-areas:
13 | "home hamburger"
14 | "pages pages";
15 | }
16 |
17 | .nav-grid-home {
18 | align-items: center;
19 | display: flex;
20 | grid-area: home;
21 | margin-right: auto;
22 | }
23 |
24 | .nav-grid-home a,
25 | .nav-grid-pages li {
26 | line-height: normal;
27 | }
28 |
29 | .nav-grid-hamburger {
30 | grid-area: hamburger;
31 | margin-left: auto;
32 | }
33 |
34 | .nav-grid-hamburger button {
35 | background: none;
36 | border: none;
37 | color: var(--color-font-primary);
38 | outline: 0.1rem solid var(--color-highlighted);
39 | }
40 |
41 | .nav-grid--expanded .nav-grid-pages {
42 | display: flex;
43 | flex-direction: column;
44 | }
45 |
46 | .nav-grid--expanded .nav-grid-pages a {
47 | display: block;
48 | }
49 |
50 | .nav-grid--expanded .nav-grid-pages > li + li {
51 | margin-top: var(--geometry-space-small);
52 | }
53 |
--------------------------------------------------------------------------------
/src/css/components/tooltip.css:
--------------------------------------------------------------------------------
1 | .tooltip {
2 | background: var(--color-background-secondary);
3 | border: 0;
4 | border-radius: 1em;
5 | font-size: 1rem;
6 | font-weight: bold;
7 | height: auto;
8 | padding: 0.5rem;
9 | position: absolute;
10 | text-align: center;
11 | visibility: hidden;
12 | width: auto;
13 | }
14 |
15 | .tooltip:hover {
16 | opacity: 0.8;
17 | }
18 |
--------------------------------------------------------------------------------
/src/css/components/transfer-demo-grid.css:
--------------------------------------------------------------------------------
1 | .transfer-demo-grid {
2 | display: grid;
3 | grid-row-gap: var(--geometry-space-medium);
4 | grid-template-areas:
5 | "buttons"
6 | "scene"
7 | "messages";
8 | }
9 |
10 | .transfer-demo-grid h3 {
11 | color: var(--color-font-primary);
12 | }
13 |
--------------------------------------------------------------------------------
/src/css/debug-stack.css:
--------------------------------------------------------------------------------
1 | .stack {
2 | background: hsla(210, 100%, 50%, 0.5) !important;
3 | box-shadow: none !important;
4 | color: hsla(210, 100%, 100%, 0.9) !important;
5 | outline: 0.1rem solid hsla(30, 100%, 50%, 0.5) !important;
6 | }
7 |
8 | .stack > * {
9 | background: hsla(220, 100%, 50%, 0.5) !important;
10 | outline: 0.25rem solid hsla(40, 100%, 50%, 0.5) !important;
11 | }
12 |
--------------------------------------------------------------------------------
/src/css/debug.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Import this file to see the structure of a page.
3 | *
4 | * https://www.freecodecamp.org/news/heres-my-favorite-weird-trick-to-debug-css-88529aa5a6a3/
5 | */
6 | *:not(path):not(g) {
7 | background: hsla(210, 100%, 50%, 0.5) !important;
8 | box-shadow: none !important;
9 | color: hsla(210, 100%, 100%, 0.9) !important;
10 | outline: 0.25rem solid hsla(210, 100%, 100%, 0.5) !important;
11 | }
12 |
13 | /* div {
14 | outline: 0.1rem solid red !important;
15 | } */
16 |
17 | /* footer {
18 | outline: 0.1rem solid fuchsia !important;
19 | } */
20 |
21 | /* p {
22 | outline: 0.1rem solid purple !important;
23 | } */
24 |
25 | /* ul {
26 | outline: 0.1rem solid lawngreen !important;
27 | } */
28 |
--------------------------------------------------------------------------------
/src/css/defaults.css:
--------------------------------------------------------------------------------
1 | canvas {
2 | outline: 0.1rem solid var(--color-highlighted);
3 | }
4 |
5 | button {
6 | background: none;
7 | border-color: var(--color-highlighted);
8 | border-style: solid;
9 | }
10 |
--------------------------------------------------------------------------------
/src/css/font-face-rules.css:
--------------------------------------------------------------------------------
1 | /**
2 | * @font-face rules
3 | *
4 | * If you have multiple font formats, put most modern formats first:
5 | * .woff2 -> .woff -> .ttf
6 | * .woff2 and .woff are compressed, so use them over .ttf
7 | * If you can't find woff2/woff, convert ttf fonts with an online converter:
8 | * https://font-converter.net/en
9 | * https://transfonter.org/
10 | *
11 | * A single font-family can have multiple fonts (e.g. for bold, italic).
12 | */
13 | @font-face {
14 | font-family: "Syne-Extra";
15 | font-style: normal;
16 | font-weight: normal;
17 | /* stylelint-disable declaration-colon-newline-after */
18 | src: url("../fonts/Syne/Syne-Extra.woff2") format("woff2"),
19 | url("../fonts/Syne/Syne-Extra.woff") format("woff"),
20 | url("../fonts/Syne/Syne-Extra.ttf") format("truetype");
21 | /* stylelint-enable declaration-colon-newline-after */
22 | }
23 |
24 | @font-face {
25 | font-family: "Syne-Regular";
26 | font-style: normal;
27 | font-weight: normal;
28 | /* stylelint-disable declaration-colon-newline-after */
29 | src: url("../fonts/Syne/Syne-Regular.woff2") format("woff2"),
30 | url("../fonts/Syne/Syne-Regular.woff") format("woff"),
31 | url("../fonts/Syne/Syne-Regular.ttf") format("truetype");
32 | /* stylelint-enable declaration-colon-newline-after */
33 | }
34 |
35 | /* other cool fonts Syncopate, Quicksand, Aclonica */
36 |
--------------------------------------------------------------------------------
/src/css/helpers.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Grid element that keep placing on the same row its children, then it places
3 | * on subsequent rows the children that would otherwise overflow.
4 | * Set the --child-min-width CSS custom property to control how wide each child
5 | * should AT LEAST be.
6 | *
7 | * @example
8 | *
9 | *
Child 0
10 | *
Child 1
11 | *
Child 2
12 | *
13 | *
14 | * .my-class .grid-template-columns\:auto-fit {
15 | * --child-min-width: 500px;
16 | * }
17 | *
18 | * @see
19 | * https://css-tricks.com/auto-sizing-columns-css-grid-auto-fill-vs-auto-fit/
20 | */
21 | .grid-template-columns\:auto-fit {
22 | /* stylelint-disable-next-line unit-whitelist */
23 | --child-min-width: 321px;
24 |
25 | display: grid;
26 | grid-column-gap: 1rem;
27 | grid-row-gap: 1rem;
28 | /* stylelint-disable-next-line unit-whitelist */
29 | grid-template-columns: repeat(auto-fit, minmax(var(--child-min-width), 1fr));
30 | }
31 |
32 | /**
33 | * @see
34 | * http://wellcaffeinated.net/articles/2012/12/10/very-simple-css-only-proportional-resizing-of-elements
35 | */
36 | .preserve-aspect-ratio {
37 | --aspect: calc(4 / 3);
38 |
39 | padding-top: calc(100% / var(--aspect));
40 | position: relative;
41 | width: calc(100% - var(--grid-column-gap));
42 | }
43 |
44 | .preserve-aspect-ratio > * {
45 | bottom: 0;
46 | left: 0;
47 | position: absolute;
48 | right: 0;
49 | top: 0;
50 | }
51 |
52 | .preserve-aspect-ratio > * > * {
53 | height: 100%;
54 | width: 100%;
55 | }
56 |
57 | /**
58 | * Lobotomized owl to set margin-left for all children but the last one.
59 | *
60 | * This will probably no longer be necessary when browsers will implement `gap`
61 | * for flex containers.
62 | * https://caniuse.com/#search=gap
63 | *
64 | * @example
65 | *
66 | * Child 0
67 | * Child 1
68 | * Child 2 (this one has no margin-left)
69 | *
70 | *
71 | * ul.lobotomized-owl\:margin-left {
72 | * --space: 1.5rem;
73 | * }
74 | *
75 | * @see
76 | * https://alistapart.com/article/axiomatic-css-and-lobotomized-owls/
77 | */
78 | .lobotomized-owl\:margin-left > * + * {
79 | margin-left: var(--space);
80 | }
81 |
82 | .site {
83 | background-color: var(--color-background-primary);
84 | display: flex;
85 | flex-direction: column;
86 | min-height: 100vh;
87 | }
88 |
89 | .site > main {
90 | background-color: var(--color-background-secondary);
91 | flex: 1 0 auto;
92 | }
93 |
94 | .ghost-button {
95 | color: var(--color-font-primary);
96 | outline: 0.1rem solid var(--color-highlighted);
97 | padding: 0 var(--geometry-space-small);
98 | text-decoration: none;
99 | }
100 |
101 | .ghost-button:hover {
102 | color: var(--color-highlighted);
103 | text-decoration: underline;
104 | }
105 |
106 | .no-margins {
107 | margin: 0;
108 | }
109 |
110 | .single-responsive-element {
111 | left: 0;
112 | position: relative;
113 | width: 100%;
114 | }
115 |
116 | .display\:none {
117 | display: none;
118 | }
119 |
--------------------------------------------------------------------------------
/src/css/index.css:
--------------------------------------------------------------------------------
1 | /**
2 | * CSS files for the application.
3 | *
4 | * @font-face rules should be defined first in your main CSS file
5 | *
6 | * @see
7 | * https://alligator.io/css/font-face/
8 | */
9 | @import "../css/font-face-rules.css";
10 | @import "../css/variables.css";
11 | @import "../css/typography.css";
12 | @import "../css/layouts/index.css";
13 | @import "../css/components/index.css";
14 | @import "../css/helpers.css";
15 | @import "../css/defaults.css";
16 | @import "../css/media-queries.css";
17 |
--------------------------------------------------------------------------------
/src/css/layouts/README.md:
--------------------------------------------------------------------------------
1 | # Layouts
2 |
3 | Set of [layout primitives](https://absolutely.every-layout.dev/layouts/).
4 |
5 | These files' only concern is layout (arrangement of boxes).
6 |
7 | They should not be used for branding/aesthetic (fonts, colors, shadows, etc).
8 |
--------------------------------------------------------------------------------
/src/css/layouts/box.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Box layout
3 | *
4 | * A box element applies a padding on all sides. Set the --space CSS custom
5 | * property to set the amount of this padding.
6 | *
7 | * @example
8 | *
9 | *
Child (there is padding around me)
10 | *
11 | *
12 | * div.box {
13 | * --space: 3rem;
14 | * }
15 | *
16 | * @see
17 | * https://absolutely.every-layout.dev/layouts/box/
18 | */
19 |
20 | .box {
21 | --space: var(--geometry-space-small);
22 |
23 | padding: var(--space);
24 | }
25 |
--------------------------------------------------------------------------------
/src/css/layouts/cluster.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Cluster layout
3 | *
4 | * A cluster element is a container that lets its grandchildren wrap on the next
5 | * row when they would otherwise overflow. Set the --space CSS custom property
6 | * to control the space between its grandchildren.
7 | *
8 | * @example
9 | *
10 | *
11 | *
Logo (grandchild 0)
12 | *
List of links (grandchild 1)
13 | *
14 | *
15 | *
16 | * nav.cluster {
17 | * --space: 3rem;
18 | * }
19 | *
20 | * @see
21 | * https://absolutely.every-layout.dev/layouts/cluster/
22 | */
23 |
24 | .cluster {
25 | --space: 1rem;
26 |
27 | overflow: hidden;
28 | }
29 |
30 | .cluster > * {
31 | align-items: center;
32 | display: flex;
33 | flex-wrap: wrap;
34 | justify-content: flex-start;
35 | margin: calc(var(--space) / 2 * -1);
36 | }
37 |
38 | .cluster > * > * {
39 | margin: calc(var(--space) / 2);
40 | }
41 |
--------------------------------------------------------------------------------
/src/css/layouts/index.css:
--------------------------------------------------------------------------------
1 | @import "./box.css";
2 | @import "./cluster.css";
3 | @import "./stack.css";
4 |
--------------------------------------------------------------------------------
/src/css/layouts/stack.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Stack layout
3 | *
4 | * A stack element is a container that controls the vertical margins of its
5 | * children. Set the --space CSS custom property to control the vertical space
6 | * between its children.
7 | *
8 | * @example
9 | *
10 | *
Child 0
11 | *
Child 1
12 | *
Child 2
13 | *
14 | *
15 | * div.stack {
16 | * --space: 3rem;
17 | * }
18 | *
19 | * @see
20 | * https://absolutely.every-layout.dev/layouts/stack/
21 | */
22 | .stack {
23 | --space: 1rem;
24 |
25 | display: flex;
26 | flex-direction: column;
27 | justify-content: flex-start;
28 | }
29 |
30 | .stack > * {
31 | margin-bottom: 0;
32 | margin-top: 0;
33 | }
34 |
35 | .stack > * + * {
36 | margin-top: var(--space);
37 | }
38 |
--------------------------------------------------------------------------------
/src/css/media-queries.css:
--------------------------------------------------------------------------------
1 | /* stylelint-disable unit-whitelist */
2 | @media only screen and (min-width: 321px) {
3 | .single-responsive-element {
4 | left: 5%;
5 | width: 90%;
6 | }
7 | }
8 |
9 | @media only screen and (min-width: 1300px) {
10 | .single-responsive-element {
11 | left: 10%;
12 | width: 80%;
13 | }
14 | }
15 |
16 | @media only screen and (min-width: 1500px) {
17 | .single-responsive-element {
18 | left: 12.5%;
19 | width: 75%;
20 | }
21 | }
22 |
23 | @media only screen and (min-width: 1700px) {
24 | .single-responsive-element {
25 | left: 15%;
26 | width: 70%;
27 | }
28 | }
29 |
30 | @media only screen and (min-width: 1900px) {
31 | .single-responsive-element {
32 | left: 17.5%;
33 | width: 65%;
34 | }
35 | }
36 |
37 | @media only screen and (min-width: 2200px) {
38 | .single-responsive-element {
39 | left: 22.5%;
40 | width: 55%;
41 | }
42 | }
43 |
44 | @media only screen and (min-width: 600px) {
45 | .nav-grid {
46 | grid-template-areas: "home pages";
47 | }
48 |
49 | .nav-grid-hamburger {
50 | display: none;
51 | }
52 |
53 | .nav-grid-pages {
54 | display: flex;
55 | justify-self: flex-end;
56 | }
57 | }
58 |
59 | @media only screen and (min-width: 800px) {
60 | .transfer-demo-grid {
61 | grid-template-areas:
62 | "buttons buttons"
63 | "scene messages";
64 | grid-template-columns: 1fr 1fr;
65 | }
66 |
67 | .transfer-demo-grid .messages {
68 | max-height: 50vh;
69 | }
70 | }
71 |
72 | /* stylelint-enable unit-whitelist */
73 |
--------------------------------------------------------------------------------
/src/css/typography.css:
--------------------------------------------------------------------------------
1 | h1,
2 | h2,
3 | h3,
4 | h4,
5 | h5,
6 | h6 {
7 | font-family: var(--typography-font-family-headline);
8 | }
9 |
10 | a,
11 | button,
12 | div,
13 | figcaption,
14 | li,
15 | main,
16 | p,
17 | span {
18 | font-family: var(--typography-font-family-body);
19 | }
20 |
21 | button,
22 | figcaption,
23 | h1,
24 | h2,
25 | h3,
26 | h4,
27 | h5,
28 | h6,
29 | p,
30 | span {
31 | color: var(--color-font-primary);
32 | }
33 |
--------------------------------------------------------------------------------
/src/css/variables.css:
--------------------------------------------------------------------------------
1 | :root {
2 | /* COLORS */
3 | --color-background-primary: #374047;
4 | --color-background-secondary: #5f6e78;
5 | --color-font-primary: white;
6 | --color-highlighted: orange;
7 |
8 | /* GEOMETRY */
9 | --geometry-space-small: 1rem;
10 | --geometry-space-medium: 2rem;
11 | --geometry-space-big: 3rem;
12 |
13 | /* TYPOGRAPHY */
14 | --typography-font-family-headline: "Syne-Extra", sans-serif;
15 | --typography-font-family-body: "Syne-Regular", sans-serif;
16 |
17 | /* --typography-font-family-body: "Raleway", sans-serif; */
18 | }
19 |
--------------------------------------------------------------------------------
/src/fonts/Syne/README.md:
--------------------------------------------------------------------------------
1 | # Syne
2 |
3 | See [here](https://gitlab.com/bonjour-monde/fonderie/syne-typeface).
4 |
--------------------------------------------------------------------------------
/src/fonts/Syne/Syne-Extra.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackdbd/threejs-es6-webpack-starter/e4b00bbbfdb93016fbd486ec4a4b3456587b61fd/src/fonts/Syne/Syne-Extra.ttf
--------------------------------------------------------------------------------
/src/fonts/Syne/Syne-Extra.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackdbd/threejs-es6-webpack-starter/e4b00bbbfdb93016fbd486ec4a4b3456587b61fd/src/fonts/Syne/Syne-Extra.woff
--------------------------------------------------------------------------------
/src/fonts/Syne/Syne-Extra.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackdbd/threejs-es6-webpack-starter/e4b00bbbfdb93016fbd486ec4a4b3456587b61fd/src/fonts/Syne/Syne-Extra.woff2
--------------------------------------------------------------------------------
/src/fonts/Syne/Syne-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackdbd/threejs-es6-webpack-starter/e4b00bbbfdb93016fbd486ec4a4b3456587b61fd/src/fonts/Syne/Syne-Regular.ttf
--------------------------------------------------------------------------------
/src/fonts/Syne/Syne-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackdbd/threejs-es6-webpack-starter/e4b00bbbfdb93016fbd486ec4a4b3456587b61fd/src/fonts/Syne/Syne-Regular.woff
--------------------------------------------------------------------------------
/src/fonts/Syne/Syne-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackdbd/threejs-es6-webpack-starter/e4b00bbbfdb93016fbd486ec4a4b3456587b61fd/src/fonts/Syne/Syne-Regular.woff2
--------------------------------------------------------------------------------
/src/glsl/README.md:
--------------------------------------------------------------------------------
1 | # GLSL
2 |
3 | This directory contains glsl code for vertex shaders and fragment shaders.
4 |
--------------------------------------------------------------------------------
/src/glsl/fragmentShader.glsl:
--------------------------------------------------------------------------------
1 | uniform float delta;
2 | varying float vOpacity;
3 | varying vec3 vUv;
4 |
5 | void main() {
6 | float r = 1.0 + cos(vUv.x * delta);
7 | float g = 0.5 + sin(delta) * 0.5;
8 | float b = 0.0;
9 | vec3 rgb = vec3(r, g, b);
10 |
11 | gl_FragColor = vec4(rgb, vOpacity);
12 | }
--------------------------------------------------------------------------------
/src/glsl/vertexShader.glsl:
--------------------------------------------------------------------------------
1 | attribute float vertexDisplacement;
2 | uniform float delta;
3 | varying float vOpacity;
4 | varying vec3 vUv;
5 |
6 | void main() {
7 | vUv = position;
8 | vOpacity = vertexDisplacement;
9 |
10 | vec3 p = position;
11 |
12 | p.x += sin(vertexDisplacement) * 50.0;
13 | p.y += cos(vertexDisplacement) * 50.0;
14 |
15 | vec4 modelViewPosition = modelViewMatrix * vec4(p, 1.0);
16 | gl_Position = projectionMatrix * modelViewPosition;
17 | }
18 |
--------------------------------------------------------------------------------
/src/html/documents/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | ${require('../fragments/head.html')}
4 |
5 | ${require('../fragments/navbar.html')}
6 |
7 | Ops. We could not find that page.
8 | Back to homepage
9 |
10 | ${require('../fragments/footer.html')}
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/html/documents/README.md:
--------------------------------------------------------------------------------
1 | # HTML documents
2 |
3 | Each file in this directory represents a page in the application.
4 |
5 | This is a multi page aplication, so [each Webpack entry point](https://webpack.js.org/concepts/entry-points/#multi-page-application) associated to a page uses one of these files as a template.
6 |
7 | [html-webpack-plugin](https://webpack.js.org/plugins/html-webpack-plugin/) picks the HTML document associated to the entry point and injects the [chunks](https://github.com/jantimon/html-webpack-plugin#filtering-chunks) required for the page.
8 |
--------------------------------------------------------------------------------
/src/html/documents/about.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | ${require('../fragments/head.html')}
4 |
5 | ${require('../fragments/navbar.html')}
6 |
7 | About
8 |
9 | Examples with Three.js running in the main thread or in web workers.
10 |
11 |
12 | ${require('../fragments/footer.html')}
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/html/documents/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | ${require('../fragments/head.html')}
4 |
5 | ${require('../fragments/navbar.html')}
6 |
7 |
8 |
13 |
14 |
15 | A scene with a spotlight, a directional light, an ambient light, a
16 | particle system, a custom material and several helpers.
17 |
18 |
19 | ${require('../fragments/footer.html')}
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/html/documents/offscreen-bitmaprenderer.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | ${require('../fragments/head.html')}
4 |
5 | ${require('../fragments/navbar.html')}
6 |
7 |
8 |
9 |
10 |
17 | Low Res
18 |
19 |
20 |
27 | Medium Res
28 |
29 |
30 |
37 | High Res
38 |
39 |
40 |
41 |
Messages between main thread and web worker
42 |
43 |
44 |
45 |
46 | ${require('../fragments/footer.html')}
47 |
48 |
49 |
--------------------------------------------------------------------------------
/src/html/documents/offscreen-transfer.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | ${require('../fragments/head.html')}
4 |
5 | ${require('../fragments/navbar.html')}
6 |
7 |
8 |
9 |
10 |
11 |
12 |
16 | Start
17 |
18 |
22 | Stop
23 |
24 |
28 | Terminate worker
29 |
30 |
34 | Instantiate worker
35 |
36 |
37 |
38 |
39 |
40 |
41 |
48 | Canvas Transfer
49 |
50 |
51 |
52 |
Messages between main thread and web worker
53 |
54 |
55 |
56 |
57 |
58 | ${require('../fragments/footer.html')}
59 |
60 |
61 |
--------------------------------------------------------------------------------
/src/html/fragments/README.md:
--------------------------------------------------------------------------------
1 | # HTML document fragments
2 |
3 | Each file in this directory defines a HTML [DocumentFragment](https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment).
4 |
5 | These fragments are reused across multiple HTML documents (i.e. pages in the application) to avoid duplicate code.
6 |
7 | The fragments are injected in a HTML document thanks to [html-loader's interpolate](https://webpack.js.org/loaders/html-loader/#interpolate) option.
8 |
--------------------------------------------------------------------------------
/src/html/fragments/footer.html:
--------------------------------------------------------------------------------
1 |
28 |
--------------------------------------------------------------------------------
/src/html/fragments/head.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
${APP_NAME}
4 |
5 |
9 |
10 |
11 |
15 |
16 |
22 |
28 |
34 |
35 |
39 |
40 |
--------------------------------------------------------------------------------
/src/html/fragments/navbar.html:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
13 |
14 |
15 |
35 |
36 |
--------------------------------------------------------------------------------
/src/js/404.js:
--------------------------------------------------------------------------------
1 | import "../css/index.css";
2 |
3 | import { toggleMobileNav } from "./components/navbar";
4 |
5 | window.toggleMobileNav = toggleMobileNav;
6 |
--------------------------------------------------------------------------------
/src/js/README.md:
--------------------------------------------------------------------------------
1 | # JS
2 |
3 | These files are organized in this way:
4 |
5 | - vendor: vendor code that this application relies on
6 | - [workers/](./workers/README.md): scripts that run in background threads. There are [dedicated web workers](https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker) and [shared web workers](https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker).
--------------------------------------------------------------------------------
/src/js/about.js:
--------------------------------------------------------------------------------
1 | import "../css/index.css";
2 |
3 | import { toggleMobileNav } from "./components/navbar";
4 |
5 | window.toggleMobileNav = toggleMobileNav;
6 |
--------------------------------------------------------------------------------
/src/js/application.js:
--------------------------------------------------------------------------------
1 | import * as THREE from "three";
2 | // TODO: OrbitControls import three.js on its own, so the webpack bundle includes three.js twice!
3 | import OrbitControls from "orbit-controls-es6";
4 | import { Interaction } from "three.interaction";
5 |
6 | import * as Detector from "../js/vendor/Detector";
7 | import * as DAT from "../js/vendor/dat.gui.min";
8 |
9 | const checkerboard = require("../textures/checkerboard.jpg");
10 | const star = require("../textures/star.png");
11 | const vertexShader = require("../glsl/vertexShader.glsl");
12 | const fragmentShader = require("../glsl/fragmentShader.glsl");
13 |
14 | const CAMERA_NAME = "Perspective Camera";
15 | const DIRECTIONAL_LIGHT_NAME = "Directional Light";
16 | const SPOT_LIGHT_NAME = "Spotlight";
17 | const CUSTOM_MESH_NAME = "Custom Mesh";
18 |
19 | export class Application {
20 | constructor(opts) {
21 | this.showHelpers =
22 | opts && opts.showHelpers !== undefined ? opts.showHelpers : true;
23 | this.canvas = document.getElementById("application-canvas");
24 | this.container = document.querySelector("main .canvas-container-inner");
25 | this.createTooltip();
26 | this.textureLoader = new THREE.TextureLoader();
27 |
28 | if (Detector.webgl) {
29 | this.bindEventHandlers();
30 | this.init(this.canvas);
31 | this.render();
32 | } else {
33 | // console.warn("WebGL NOT supported in your browser!");
34 | const warning = Detector.getWebGLErrorMessage();
35 | this.container.appendChild(warning);
36 | }
37 | }
38 |
39 | /**
40 | * Bind event handlers to the Application instance.
41 | */
42 | bindEventHandlers() {
43 | this.handleClick = this.handleClick.bind(this);
44 | this.handleMouseMove = this.handleMouseMove.bind(this);
45 | this.handleResize = this.handleResize.bind(this);
46 | this.showTooltip = this.showTooltip.bind(this);
47 | this.hideTooltip = this.hideTooltip.bind(this);
48 | }
49 |
50 | init(canvas) {
51 | const showGUI = false;
52 | window.addEventListener("resize", this.handleResize);
53 | this.setupScene();
54 | this.setupRenderer(canvas);
55 | this.setupCamera();
56 | const interaction = new Interaction(this.renderer, this.scene, this.camera);
57 | this.setupLights();
58 | if (this.showHelpers) {
59 | this.setupHelpers();
60 | }
61 | this.setupRay();
62 | this.setupControls();
63 |
64 | if (showGUI) {
65 | this.setupGUI();
66 | }
67 |
68 | this.addFloor(100, 100);
69 | this.addCube(20);
70 | this.addCustomMesh();
71 |
72 | const particleSpecs = { spread: { x: 50, y: 100, z: 50 } };
73 | this.addParticleSystem(300, 5, particleSpecs);
74 |
75 | const boxSpecs = {
76 | depth: 20,
77 | height: 10,
78 | spread: { x: 20, y: 20, z: 50 },
79 | width: 5,
80 | };
81 | this.addGroupObject(10, boxSpecs);
82 | }
83 |
84 | render() {
85 | this.controls.update();
86 | this.updateCustomMesh();
87 | this.renderer.render(this.scene, this.camera);
88 | // when render is invoked via requestAnimationFrame(this.render) there is
89 | // no 'this', so either we bind it explicitly or use an es6 arrow function.
90 | // requestAnimationFrame(this.render.bind(this));
91 | requestAnimationFrame(() => this.render());
92 | }
93 |
94 | createTooltip() {
95 | const main = document.querySelector("main");
96 | if (!main) {
97 | alert(`You have no '
' tag on ythe HTML page. You need exactly ONE`);
98 | }
99 | const div = document.createElement("div");
100 | div.setAttribute("class", "tooltip");
101 | div.setAttribute("data-cy", "tooltip");
102 | main.appendChild(div);
103 | this.tooltip = div;
104 | }
105 |
106 | handleClick(event) {
107 | const [x, y] = this.getNDCCoordinates(event, true);
108 | this.raycaster.setFromCamera({ x, y }, this.camera);
109 | const intersects = this.raycaster.intersectObjects(this.scene.children);
110 |
111 | if (intersects.length > 0) {
112 | const hexColor = Math.random() * 0xffffff;
113 | const intersection = intersects[0];
114 | intersection.object.material.color.setHex(hexColor);
115 |
116 | const { direction, origin } = this.raycaster.ray;
117 | const arrow = new THREE.ArrowHelper(direction, origin, 100, hexColor);
118 | this.scene.add(arrow);
119 | }
120 | }
121 |
122 | handleMouseMove(event) {
123 | const [x, y] = this.getNDCCoordinates(event);
124 | }
125 |
126 | handleResize(event) {
127 | // console.warn(event);
128 | const { clientWidth, clientHeight } = this.container;
129 | this.camera.aspect = clientWidth / clientHeight;
130 | this.camera.updateProjectionMatrix();
131 | this.renderer.setSize(clientWidth, clientHeight);
132 | }
133 |
134 | showTooltip(interactionEvent) {
135 | const { name, uuid, type } = interactionEvent.target;
136 | const { x, y } = interactionEvent.data.global;
137 | const [xScreen, yScreen] = this.getScreenCoordinates(x, y);
138 | this.tooltip.innerHTML = `${name} (${type}) Click to cast a ray `;
139 | const style = `left: ${xScreen}px; top: ${yScreen}px; visibility: visible; opacity: 0.8`;
140 | this.tooltip.style = style;
141 | }
142 |
143 | hideTooltip(interactionEvent) {
144 | this.tooltip.style = "visibility: hidden";
145 | }
146 |
147 | /**
148 | * Setup a Three.js scene.
149 | * Setting the scene is the first Three.js-specific code to perform.
150 | */
151 | setupScene() {
152 | this.scene = new THREE.Scene();
153 | this.scene.autoUpdate = true;
154 | // Let's say we want to define the background color only once throughout the
155 | // application. This can be done in CSS. So here we use JS to get a property
156 | // defined in a CSS.
157 | const style = window.getComputedStyle(this.container);
158 | const color = new THREE.Color(style.getPropertyValue("background-color"));
159 | this.scene.background = color;
160 | this.scene.fog = null;
161 | // Any Three.js object in the scene (and the scene itself) can have a name.
162 | this.scene.name = "My Three.js Scene";
163 | }
164 |
165 | /**
166 | * Create a Three.js renderer.
167 | * We let the renderer create a canvas element where to draw its output, then
168 | * we set the canvas size, we add the canvas to the DOM and we bind event
169 | * listeners to it.
170 | */
171 | setupRenderer(canvas) {
172 | this.renderer = new THREE.WebGLRenderer({
173 | antialias: true,
174 | canvas,
175 | });
176 | // this.renderer.setClearColor(0xd3d3d3); // it's a light gray
177 | this.renderer.setClearColor(0x222222); // it's a dark gray
178 | this.renderer.setPixelRatio(window.devicePixelRatio || 1);
179 | const { clientWidth, clientHeight } = this.container;
180 | this.renderer.setSize(clientWidth, clientHeight);
181 | this.renderer.shadowMap.enabled = true;
182 | this.container.appendChild(this.renderer.domElement);
183 | this.renderer.domElement.addEventListener("click", this.handleClick);
184 | this.renderer.domElement.addEventListener(
185 | "mousemove",
186 | this.handleMouseMove
187 | );
188 | }
189 |
190 | setupCamera() {
191 | const fov = 75;
192 | const { clientWidth, clientHeight } = this.container;
193 | const aspect = clientWidth / clientHeight;
194 | const near = 0.1;
195 | const far = 10000;
196 | this.camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
197 | this.camera.name = CAMERA_NAME;
198 | this.camera.position.set(100, 100, 100);
199 | this.camera.lookAt(this.scene.position);
200 | }
201 |
202 | setupLights() {
203 | const dirLight = new THREE.DirectionalLight(0x4682b4, 1); // steelblue
204 | dirLight.name = DIRECTIONAL_LIGHT_NAME;
205 | dirLight.position.set(120, 30, -200);
206 | dirLight.castShadow = true;
207 | dirLight.shadow.camera.near = 10;
208 | this.scene.add(dirLight);
209 |
210 | const spotLight = new THREE.SpotLight(0xffaa55);
211 | spotLight.name = SPOT_LIGHT_NAME;
212 | spotLight.position.set(120, 30, 0);
213 | spotLight.castShadow = true;
214 | dirLight.shadow.camera.near = 10;
215 | this.scene.add(spotLight);
216 |
217 | const ambientLight = new THREE.AmbientLight(0xffaa55);
218 | this.scene.add(ambientLight);
219 | }
220 |
221 | setupHelpers() {
222 | const gridHelper = new THREE.GridHelper(200, 16);
223 | gridHelper.name = "Floor GridHelper";
224 | this.scene.add(gridHelper);
225 |
226 | // XYZ axes helper (XYZ axes are RGB colors, respectively)
227 | const axesHelper = new THREE.AxesHelper(75);
228 | axesHelper.name = "XYZ AzesHelper";
229 | this.scene.add(axesHelper);
230 |
231 | const dirLight = this.scene.getObjectByName(DIRECTIONAL_LIGHT_NAME);
232 |
233 | const dirLightHelper = new THREE.DirectionalLightHelper(dirLight, 10);
234 | dirLightHelper.name = `${DIRECTIONAL_LIGHT_NAME} Helper`;
235 | this.scene.add(dirLightHelper);
236 |
237 | const dirLightCameraHelper = new THREE.CameraHelper(dirLight.shadow.camera);
238 | dirLightCameraHelper.name = `${DIRECTIONAL_LIGHT_NAME} Shadow Camera Helper`;
239 | this.scene.add(dirLightCameraHelper);
240 |
241 | const spotLight = this.scene.getObjectByName(SPOT_LIGHT_NAME);
242 |
243 | const spotLightHelper = new THREE.SpotLightHelper(spotLight);
244 | spotLightHelper.name = `${SPOT_LIGHT_NAME} Helper`;
245 | this.scene.add(spotLightHelper);
246 |
247 | const spotLightCameraHelper = new THREE.CameraHelper(
248 | spotLight.shadow.camera
249 | );
250 | spotLightCameraHelper.name = `${SPOT_LIGHT_NAME} Shadow Camera Helper`;
251 | this.scene.add(spotLightCameraHelper);
252 | }
253 |
254 | setupRay() {
255 | this.raycaster = new THREE.Raycaster();
256 | }
257 |
258 | /**
259 | * Add a floor object to the scene.
260 | * Note: Three.js's TextureLoader does not support progress events.
261 | * @see https://threejs.org/docs/#api/en/loaders/TextureLoader
262 | */
263 | addFloor(width, height) {
264 | const geometry = new THREE.PlaneGeometry(width, height, 1, 1);
265 | const onLoad = texture => {
266 | texture.wrapS = THREE.RepeatWrapping;
267 | texture.wrapT = THREE.RepeatWrapping;
268 | texture.repeat.set(4, 4);
269 | const material = new THREE.MeshBasicMaterial({
270 | map: texture,
271 | side: THREE.DoubleSide,
272 | });
273 | const floor = new THREE.Mesh(geometry, material);
274 | floor.name = "Floor";
275 | floor.position.y = -0.5;
276 | floor.rotation.x = Math.PI / 2;
277 | this.scene.add(floor);
278 |
279 | floor.cursor = "pointer";
280 | floor.on("mouseover", this.showTooltip);
281 | floor.on("mouseout", this.hideTooltip);
282 | };
283 |
284 | const onProgress = undefined;
285 |
286 | const onError = event => {
287 | alert(`Impossible to load the texture ${checkerboard}`);
288 | };
289 | this.textureLoader.load(checkerboard, onLoad, onProgress, onError);
290 | }
291 |
292 | setupControls() {
293 | this.controls = new OrbitControls(this.camera, this.renderer.domElement);
294 | this.controls.enabled = true;
295 | this.controls.maxDistance = 1500;
296 | this.controls.minDistance = 0;
297 | this.controls.autoRotate = true;
298 | }
299 |
300 | setupGUI() {
301 | const gui = new DAT.GUI();
302 | gui
303 | .add(this.camera.position, "x")
304 | .name("Camera X")
305 | .min(0)
306 | .max(100);
307 | gui
308 | .add(this.camera.position, "y")
309 | .name("Camera Y")
310 | .min(0)
311 | .max(100);
312 | gui
313 | .add(this.camera.position, "z")
314 | .name("Camera Z")
315 | .min(0)
316 | .max(100);
317 | }
318 |
319 | /**
320 | * Create an object that uses custom shaders.
321 | */
322 | addCustomMesh() {
323 | this.delta = 0;
324 | const customUniforms = {
325 | delta: { value: 0 },
326 | };
327 |
328 | const material = new THREE.ShaderMaterial({
329 | vertexShader,
330 | fragmentShader,
331 | uniforms: customUniforms,
332 | });
333 |
334 | const geometry = new THREE.SphereBufferGeometry(5, 32, 32);
335 |
336 | this.vertexDisplacement = new Float32Array(
337 | geometry.attributes.position.count
338 | );
339 | for (let i = 0; i < this.vertexDisplacement.length; i += 1) {
340 | this.vertexDisplacement[i] = Math.sin(i);
341 | }
342 |
343 | geometry.setAttribute(
344 | "vertexDisplacement",
345 | new THREE.BufferAttribute(this.vertexDisplacement, 1)
346 | );
347 |
348 | const customMesh = new THREE.Mesh(geometry, material);
349 | customMesh.name = CUSTOM_MESH_NAME;
350 | customMesh.position.set(5, 5, 5);
351 | this.scene.add(customMesh);
352 | }
353 |
354 | updateCustomMesh() {
355 | this.delta += 0.1;
356 | const customMesh = this.scene.getObjectByName(CUSTOM_MESH_NAME);
357 | customMesh.material.uniforms.delta.value = 0.5 + Math.sin(this.delta) * 0.5;
358 | for (let i = 0; i < this.vertexDisplacement.length; i += 1) {
359 | this.vertexDisplacement[i] = 0.5 + Math.sin(i + this.delta) * 0.25;
360 | }
361 | // attribute buffers are not refreshed automatically. To update custom
362 | // attributes we need to set the needsUpdate flag to true
363 | customMesh.geometry.attributes.vertexDisplacement.needsUpdate = true;
364 | }
365 |
366 | addCube(side) {
367 | const geometry = new THREE.CubeGeometry(side, side, side);
368 | const material = new THREE.MeshLambertMaterial({ color: 0xfbbc05 });
369 | const cube = new THREE.Mesh(geometry, material);
370 | cube.name = "Cube";
371 | cube.position.set(0, side / 2, 0);
372 | this.scene.add(cube);
373 |
374 | cube.cursor = "pointer";
375 | cube.on("mouseover", this.showTooltip);
376 | cube.on("mouseout", this.hideTooltip);
377 | }
378 |
379 | /**
380 | * Add a particle system that uses the same texture for each particle.
381 | * The texture is asynchronously loaded.
382 | * Note: Three.js's TextureLoader does not support progress events.
383 | * @see https://threejs.org/docs/#api/en/loaders/TextureLoader
384 | */
385 | addParticleSystem(numParticles, particleSize, particleSpecs) {
386 | const geometry = new THREE.Geometry();
387 | const particles = Array(numParticles)
388 | .fill(particleSpecs)
389 | .map(makeParticle);
390 | geometry.vertices = particles;
391 |
392 | const onLoad = texture => {
393 | const material = new THREE.PointsMaterial({
394 | // alphaTest's default is 0 and the particles overlap. Any value > 0
395 | // prevents the particles from overlapping.
396 | alphaTest: 0.5,
397 | map: texture,
398 | size: particleSize,
399 | transparent: true,
400 | });
401 |
402 | const particleSystem = new THREE.Points(geometry, material);
403 | particleSystem.name = "Stars";
404 | particleSystem.position.set(-50, 50, -50);
405 | this.scene.add(particleSystem);
406 |
407 | particleSystem.cursor = "pointer";
408 | particleSystem.on("mouseover", this.showTooltip);
409 | particleSystem.on("mouseout", this.hideTooltip);
410 | };
411 |
412 | const onProgress = undefined;
413 |
414 | const onError = event => {
415 | alert(`Impossible to load the texture ${star}`);
416 | };
417 |
418 | this.textureLoader.load(star, onLoad, onProgress, onError);
419 | }
420 |
421 | /**
422 | * Add a Three.js Group object to the scene.
423 | */
424 | addGroupObject(numBoxes, boxSpecs) {
425 | const group = new THREE.Group();
426 | group.name = "Group of Boxes";
427 | const { depth, height, spread, width } = boxSpecs;
428 | const geometry = new THREE.BoxGeometry(width, height, depth);
429 |
430 | const meshes = Array(numBoxes)
431 | .fill({ geometry, spread })
432 | .map(makeMesh);
433 | for (const mesh of meshes) {
434 | group.add(mesh);
435 | }
436 | group.position.set(50, 20, 50);
437 | this.scene.add(group);
438 |
439 | group.cursor = "pointer";
440 | group.on("mouseover", this.showTooltip);
441 | group.on("mouseout", this.hideTooltip);
442 | }
443 |
444 | /**
445 | * Convert screen coordinates into Normalized Device Coordinates [-1, +1].
446 | * @see https://learnopengl.com/Getting-started/Coordinate-Systems
447 | */
448 | getNDCCoordinates(event, debug) {
449 | const {
450 | clientHeight,
451 | clientWidth,
452 | offsetLeft,
453 | offsetTop,
454 | } = this.renderer.domElement;
455 |
456 | const xRelativePx = event.clientX - offsetLeft;
457 | const x = (xRelativePx / clientWidth) * 2 - 1;
458 |
459 | const yRelativePx = event.clientY - offsetTop;
460 | const y = -(yRelativePx / clientHeight) * 2 + 1;
461 |
462 | if (debug) {
463 | const data = {
464 | "Screen Coords (px)": { x: event.screenX, y: event.screenY },
465 | "Canvas-Relative Coords (px)": { x: xRelativePx, y: yRelativePx },
466 | "NDC (adimensional)": { x, y },
467 | };
468 | console.table(data, ["x", "y"]);
469 | }
470 | return [x, y];
471 | }
472 |
473 | getScreenCoordinates(xNDC, yNDC) {
474 | // const {
475 | // clientHeight,
476 | // clientWidth,
477 | // offsetLeft,
478 | // offsetTop,
479 | // } = this.renderer.domElement;
480 |
481 | // TODO: save this.main at instantiation
482 | const main = document.querySelector(".single-responsive-element");
483 |
484 | const canvasDomRect = this.canvas.getBoundingClientRect();
485 | const mainDomRect = main.getBoundingClientRect();
486 | // console.log("canvasDomRect", canvasDomRect, "mainDomRect", mainDomRect);
487 | const x = canvasDomRect.x - mainDomRect.x;
488 | const y = canvasDomRect.y - mainDomRect.y;
489 |
490 | // const xRelativePx = ((xNDC + 1) / 2) * clientWidth;
491 | // const yRelativePx = -0.5 * (yNDC - 1) * clientHeight;
492 | // const xScreen = xRelativePx + offsetLeft;
493 | // const yScreen = yRelativePx + offsetTop;
494 | // TODO: this is not exactly right, so the ray will not be correct
495 | const xRelativePx = ((xNDC + 1) / 2) * canvasDomRect.width;
496 | const yRelativePx = -0.5 * (yNDC - 1) * canvasDomRect.height;
497 | const xScreen = xRelativePx + x;
498 | const yScreen = yRelativePx + y;
499 | return [xScreen, yScreen];
500 | }
501 | }
502 |
503 | /**
504 | * Create a particle for the particle system.
505 | */
506 | function makeParticle(d, i) {
507 | const particle = new THREE.Vector3();
508 | particle.x = THREE.Math.randFloatSpread(d.spread.x);
509 | particle.y = THREE.Math.randFloatSpread(d.spread.y);
510 | particle.z = THREE.Math.randFloatSpread(d.spread.z);
511 | return particle;
512 | }
513 |
514 | /**
515 | * Make a mesh for each Box in the GroupObject.
516 | */
517 | function makeMesh(d, i) {
518 | const material = new THREE.MeshLambertMaterial({
519 | color: Math.random() * 0xffffff,
520 | });
521 | const mesh = new THREE.Mesh(d.geometry, material);
522 | mesh.name = `Box ${i} in GroupObject`;
523 | mesh.position.x = THREE.Math.randFloatSpread(d.spread.x);
524 | mesh.position.y = THREE.Math.randFloatSpread(d.spread.y);
525 | mesh.position.z = THREE.Math.randFloatSpread(d.spread.z);
526 | mesh.rotation.x = Math.random() * 360 * (Math.PI / 180);
527 | mesh.rotation.y = Math.random() * 360 * (Math.PI / 180);
528 | mesh.rotation.z = Math.random() * 360 * (Math.PI / 180);
529 | return mesh;
530 | }
531 |
--------------------------------------------------------------------------------
/src/js/bitmap-demo.js:
--------------------------------------------------------------------------------
1 | import { MainThreadAction, WorkerAction } from "./worker-actions";
2 | import { toggleMobileNav } from "./components/navbar";
3 | import { makeLi } from "./helpers";
4 | import { CanvasIds, unsupportedOffscreenCanvasAlertMessage } from "./constants";
5 | import "../css/index.css";
6 |
7 | window.toggleMobileNav = toggleMobileNav;
8 |
9 | if (
10 | !document.getElementById(CanvasIds.BITMAP_LOW_RES).transferControlToOffscreen
11 | ) {
12 | alert(unsupportedOffscreenCanvasAlertMessage);
13 | }
14 |
15 | // TODO: preload the web worker script with resource hints. Or is it done automatically by webpack's worker-loader?
16 | // const workerUrl = document.querySelector("[rel=preload][as=script]").href;
17 | (function iife() {
18 | const NAME = "Main thread";
19 |
20 | const worker = new Worker("./workers/bitmap-worker.js", {
21 | name: "Dedicated worker global scope (bitmap worker)",
22 | type: "module",
23 | });
24 |
25 | // https://developer.mozilla.org/en-US/docs/Web/API/ImageBitmapRenderingContext
26 | const bitmapsConfig = [
27 | {
28 | ctx: document
29 | .getElementById(CanvasIds.BITMAP_LOW_RES)
30 | .getContext("bitmaprenderer"),
31 | resolution: { width: 160, height: 90 },
32 | },
33 | {
34 | ctx: document
35 | .getElementById(CanvasIds.BITMAP_MEDIUM_RES)
36 | .getContext("bitmaprenderer"),
37 | resolution: { width: 640, height: 480 },
38 | },
39 | {
40 | ctx: document
41 | .getElementById(CanvasIds.BITMAP_HIGH_RES)
42 | .getContext("bitmaprenderer"),
43 | resolution: { width: 1024, height: 768 },
44 | },
45 | ];
46 |
47 | const resolutions = bitmapsConfig.reduce((accumul, curVal) => {
48 | return [...accumul, curVal.resolution];
49 | }, []);
50 |
51 | const style = "color: green; font-weight: normal";
52 |
53 | let reqId;
54 |
55 | const messages = document.querySelector(".messages ol");
56 |
57 | const onMessage = event => {
58 | const text = `[${NAME} <-- ${event.data.source}] - ${event.data.action}`;
59 | console.log(`%c${text}`, style);
60 |
61 | const li = makeLi({ text, style });
62 | messages.appendChild(li);
63 | messages.lastChild.scrollIntoView();
64 |
65 | switch (event.data.action) {
66 | case WorkerAction.BITMAPS: {
67 | const { bitmaps } = event.data.payload;
68 | bitmapsConfig.forEach((cfg, i) => {
69 | cfg.ctx.transferFromImageBitmap(bitmaps[i]);
70 | });
71 | break;
72 | }
73 | case WorkerAction.TERMINATE_ME: {
74 | worker.terminate();
75 | console.warn(`${NAME} terminated ${event.data.source}`);
76 | // If the web worker is no longer listening, it makes no sense to keep
77 | // sending him messages in requestLoop;
78 | cancelAnimationFrame(reqId);
79 | break;
80 | }
81 | case WorkerAction.NOTIFY: {
82 | // we have already printed the message, so we simply break.
83 | break;
84 | }
85 | default: {
86 | console.warn(`${NAME} received a message that does not handle`, event);
87 | }
88 | }
89 | };
90 |
91 | // When a runtime error occurs in the worker, its onerror event handler is
92 | // called. It receives an event named error which implements the ErrorEvent
93 | // interface.
94 | // https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent
95 | let errorInWorker = undefined;
96 | const onError = event => {
97 | errorInWorker = event;
98 | };
99 |
100 | worker.onmessage = onMessage;
101 | worker.onerror = onError;
102 |
103 | const message = {
104 | action: MainThreadAction.INIT_WORKER_STATE,
105 | // width and height are for the OffscreenCanvas created by the web worker.
106 | // They will also be the width and height of the generated ImageBitmap
107 | // returned by the web-worker and rendered into the canvas that has a
108 | // `bitmaprenderer` context.
109 | payload: { width: 1024, height: 768, sceneName: "My Test Scene" },
110 | source: NAME,
111 | };
112 | worker.postMessage(message);
113 |
114 | const li = makeLi({
115 | text: `[${NAME} --> worker] ${message.action}`,
116 | style: "color: red; font-weight: normal",
117 | });
118 | messages.appendChild(li);
119 | messages.lastChild.scrollIntoView();
120 |
121 | // Up until recently, requestAnimationFrame was not available in web workers,
122 | // so using requestAnimationFrame in the main thread was one of the possible
123 | // workarounds. Now I think it would be better to move requestAnimationFrame
124 | // to the web worker, so the main thread has less work to do.
125 | const requestLoop = tick => {
126 | worker.postMessage({
127 | action: MainThreadAction.REQUEST_BITMAPS,
128 | payload: {
129 | resolutions,
130 | },
131 | source: NAME,
132 | });
133 | messages.appendChild(
134 | makeLi({
135 | text: `[${NAME} --> worker] ${MainThreadAction.REQUEST_BITMAPS}`,
136 | style: "color: red; font-weight: normal",
137 | })
138 | );
139 | messages.lastChild.scrollIntoView();
140 | reqId = requestAnimationFrame(requestLoop);
141 | if (errorInWorker) {
142 | cancelAnimationFrame(reqId);
143 | }
144 | };
145 |
146 | requestLoop();
147 | })();
148 |
--------------------------------------------------------------------------------
/src/js/components/navbar.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Toggle display of the navbar dropdown for small screens.
3 | */
4 | export function toggleMobileNav() {
5 | const nav = document.querySelector("nav");
6 | nav.classList.toggle("nav-grid--expanded");
7 | }
8 |
--------------------------------------------------------------------------------
/src/js/constants.js:
--------------------------------------------------------------------------------
1 | export const ButtonIds = Object.freeze({
2 | INSTANTIATE_WORKER: "instantiate-worker",
3 | START_RENDER_LOOP: "start-render-loop",
4 | STOP_RENDER_LOOP: "stop-render-loop",
5 | TERMINATE_WORKER: "terminate-worker",
6 | });
7 |
8 | export const CanvasIds = Object.freeze({
9 | BITMAP_LOW_RES: "low-res-bitmap-canvas",
10 | BITMAP_MEDIUM_RES: "medium-res-bitmap-canvas",
11 | BITMAP_HIGH_RES: "high-res-bitmap-canvas",
12 | TRANSFER_CONTROL: "transfer-control-canvas",
13 | });
14 |
15 | export const unsupportedOffscreenCanvasAlertMessage = `
16 | Your browser does not support transferControlToOffscreen and OffscreenCanvas.\n
17 | See here:\n
18 | https://caniuse.com/#feat=mdn-api_htmlcanvaselement_transfercontroltooffscreen\n
19 | https://caniuse.com/#feat=offscreencanvas`;
20 |
--------------------------------------------------------------------------------
/src/js/helpers.js:
--------------------------------------------------------------------------------
1 | export const makeLi = ({ text, style }) => {
2 | const li = document.createElement("li");
3 | li.innerText = text;
4 | li.style = style;
5 | return li;
6 | };
7 |
--------------------------------------------------------------------------------
/src/js/index.js:
--------------------------------------------------------------------------------
1 | import { Application } from "./application";
2 | import { toggleMobileNav } from "./components/navbar";
3 | import "../css/index.css";
4 |
5 | window.toggleMobileNav = toggleMobileNav;
6 |
7 | (function iife() {
8 | new Application();
9 | })();
10 |
--------------------------------------------------------------------------------
/src/js/transfer-demo.js:
--------------------------------------------------------------------------------
1 | import { toggleMobileNav } from "./components/navbar";
2 | import { makeLi } from "./helpers";
3 | import {
4 | ButtonIds,
5 | CanvasIds,
6 | unsupportedOffscreenCanvasAlertMessage,
7 | } from "./constants";
8 | import { MainThreadAction, WorkerAction } from "./worker-actions";
9 | import "../css/index.css";
10 |
11 | window.toggleMobileNav = toggleMobileNav;
12 |
13 | // TODO: preload the web worker script with resource hints. Or is it done automatically by webpack's worker-loader?
14 | // const workerUrl = document.querySelector("[rel=preload][as=script]").href;
15 |
16 | (function iife() {
17 | const NAME = "Main thread";
18 |
19 | let worker = new Worker("./workers/transfer-worker.js", {
20 | name: `transfer-worker-original`,
21 | type: "module",
22 | });
23 |
24 | const canvas = document.getElementById(CanvasIds.TRANSFER_CONTROL);
25 |
26 | // TODO: handle resize of the canvas
27 |
28 | const handleResize = event => {
29 | console.warn(event);
30 | // const { clientWidth, clientHeight } = this.container;
31 | console.log(
32 | "handleResize",
33 | // clientWidth,
34 | // clientHeight,
35 | this
36 | );
37 | // this.camera.aspect = clientWidth / clientHeight;
38 | // this.camera.updateProjectionMatrix();
39 | // this.renderer.setSize(clientWidth, clientHeight);
40 | };
41 |
42 | window.addEventListener("resize", handleResize);
43 |
44 | const messages = document.querySelector(".messages ol");
45 |
46 | const onMessage = event => {
47 | const text = `[${NAME} <-- ${event.data.source}] - ${event.data.action}`;
48 | const style = "color: green; font-weight: normal";
49 | console.log(`%c${text}`, style);
50 | const li = makeLi({
51 | text,
52 | style,
53 | });
54 | messages.appendChild(li);
55 | messages.lastChild.scrollIntoView();
56 |
57 | switch (event.data.action) {
58 | case WorkerAction.NOTIFY:
59 | console.log(event.data.payload.info);
60 | break;
61 | case WorkerAction.TERMINATE_ME:
62 | worker.terminate();
63 | console.warn("Main thread terminated the worker");
64 | break;
65 | default:
66 | console.warn(`${NAME} received a message that does not handle`, event);
67 | }
68 | };
69 |
70 | const onError = event => {
71 | console.error("Error in web worker", event);
72 | alert(`${event.message} - ${event.filename}. See console for more.`);
73 | };
74 |
75 | worker.onmessage = onMessage;
76 | worker.onerror = onError;
77 |
78 | // The width and height of the visible canvas will be used to set the size of
79 | // WebGL drawing buffer. BUT we cannot create a WebGLRenderingContext with
80 | // canvas.getContext("webgl") here because we need to transfer the ownership
81 | // of the canvas to the web worker's OffscreenCanvas. If we try to create the
82 | // context here we get the following error: Uncaught DOMException: Failed to
83 | // execute 'transferControlToOffscreen' on 'HTMLCanvasElement': Cannot
84 | // transfer control from a canvas that has a rendering context.
85 | // So we create the WebGL rendering context in the web worker.
86 | // https://webglfundamentals.org/webgl/lessons/webgl-resizing-the-canvas.html
87 | canvas.setAttribute("width", "1024");
88 | canvas.setAttribute("height", "768");
89 | if (!canvas.transferControlToOffscreen) {
90 | alert(unsupportedOffscreenCanvasAlertMessage);
91 | }
92 | const offscreenCanvas = canvas.transferControlToOffscreen();
93 | const message = {
94 | action: MainThreadAction.INIT_WORKER_STATE,
95 | payload: { canvas: offscreenCanvas, sceneName: "my-scene" },
96 | source: NAME,
97 | };
98 | const transfer = [offscreenCanvas];
99 | worker.postMessage(message, transfer);
100 |
101 | const styleFromWorker = "color: red; font-weight: normal";
102 | messages.appendChild(
103 | makeLi({
104 | text: `[${NAME} --> worker] ${message.action}`,
105 | style: styleFromWorker,
106 | })
107 | );
108 | messages.lastChild.scrollIntoView();
109 |
110 | const startButton = document.getElementById(ButtonIds.START_RENDER_LOOP);
111 | startButton.addEventListener("click", () => {
112 | worker.postMessage({
113 | action: MainThreadAction.START_RENDER_LOOP,
114 | source: NAME,
115 | });
116 | messages.appendChild(
117 | makeLi({
118 | text: `[${NAME} --> worker] ${MainThreadAction.START_RENDER_LOOP}`,
119 | style: styleFromWorker,
120 | })
121 | );
122 | messages.lastChild.scrollIntoView();
123 | });
124 |
125 | const stopButton = document.getElementById(ButtonIds.STOP_RENDER_LOOP);
126 | stopButton.addEventListener("click", () => {
127 | worker.postMessage({
128 | action: MainThreadAction.STOP_RENDER_LOOP,
129 | source: NAME,
130 | });
131 | messages.appendChild(
132 | makeLi({
133 | text: `[${NAME} --> worker] ${MainThreadAction.STOP_RENDER_LOOP}`,
134 | style: styleFromWorker,
135 | })
136 | );
137 | messages.lastChild.scrollIntoView();
138 | });
139 |
140 | const terminateButton = document.getElementById(ButtonIds.TERMINATE_WORKER);
141 | terminateButton.addEventListener("click", () => {
142 | worker.terminate();
143 | });
144 |
145 | const instantiateButton = document.getElementById(
146 | ButtonIds.INSTANTIATE_WORKER
147 | );
148 | instantiateButton.addEventListener("click", () => {
149 | // We create a new worker but reuse the same variable. Otherwise we would
150 | // need to redefine the onmessage and onerror handlers.
151 | const workerId = Math.ceil(Math.random() * 1000);
152 | worker = new Worker("./workers/transfer-worker.js", {
153 | name: `transfer-worker-${workerId}`,
154 | type: "module",
155 | });
156 | // It seems that there is no way of getting the control of the canvas back,
157 | // so we clone the original canvas and replace it in the DOM with the clone.
158 | // https://stackoverflow.com/a/46575483/3036129
159 | const oldCanvas = document.getElementById(CanvasIds.TRANSFER_CONTROL);
160 | const newCanvas = oldCanvas.cloneNode();
161 | oldCanvas.parentNode.replaceChild(newCanvas, oldCanvas);
162 | // The control of the new, cloned canvas belongs to the main thread, so we
163 | // can transfer it to the OffscreenCanvas controlled by the worker.
164 | const anotherOffscreenCanvas = newCanvas.transferControlToOffscreen();
165 | const msg = {
166 | action: MainThreadAction.INIT_WORKER_STATE,
167 | payload: { canvas: anotherOffscreenCanvas, sceneName: "another-scene" },
168 | source: NAME,
169 | };
170 | worker.postMessage(msg, [anotherOffscreenCanvas]);
171 | });
172 | })();
173 |
--------------------------------------------------------------------------------
/src/js/vendor/Detector.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author alteredq / http://alteredqualia.com/
3 | * @author mr.doob / http://mrdoob.com/
4 | */
5 |
6 | var Detector = {
7 | canvas: !!window.CanvasRenderingContext2D,
8 | webgl: (function() {
9 | try {
10 | var canvas = document.createElement("canvas");
11 | return !!(
12 | window.WebGLRenderingContext &&
13 | (canvas.getContext("webgl") || canvas.getContext("experimental-webgl"))
14 | );
15 | } catch (e) {
16 | return false;
17 | }
18 | })(),
19 | workers: !!window.Worker,
20 | fileapi: window.File && window.FileReader && window.FileList && window.Blob,
21 |
22 | getWebGLErrorMessage: function() {
23 | var element = document.createElement("div");
24 | element.id = "webgl-error-message";
25 | element.style.fontFamily = "monospace";
26 | element.style.fontSize = "13px";
27 | element.style.fontWeight = "normal";
28 | element.style.textAlign = "center";
29 | element.style.background = "#fff";
30 | element.style.color = "#000";
31 | element.style.padding = "1.5em";
32 | element.style.width = "400px";
33 | element.style.margin = "5em auto 0";
34 |
35 | if (!this.webgl) {
36 | element.innerHTML = window.WebGLRenderingContext
37 | ? [
38 | 'Your graphics card does not seem to support WebGL . ',
39 | 'Find out how to get it here .',
40 | ].join("\n")
41 | : [
42 | 'Your browser does not seem to support WebGL . ',
43 | 'Find out how to get it here .',
44 | ].join("\n");
45 | }
46 |
47 | return element;
48 | },
49 |
50 | addGetWebGLMessage: function(parameters) {
51 | var parent, id, element;
52 |
53 | parameters = parameters || {};
54 |
55 | parent =
56 | parameters.parent !== undefined ? parameters.parent : document.body;
57 | id = parameters.id !== undefined ? parameters.id : "oldie";
58 |
59 | element = Detector.getWebGLErrorMessage();
60 | element.id = id;
61 |
62 | parent.appendChild(element);
63 | },
64 | };
65 |
66 | // browserify support
67 | if (typeof module === "object") {
68 | module.exports = Detector;
69 | }
70 |
--------------------------------------------------------------------------------
/src/js/vendor/OBJLoader2.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kai Salmen / https://kaisalmen.de
3 | * Development repository: https://github.com/kaisalmen/WWOBJLoader
4 | */
5 |
6 | import { FileLoader, Object3D, Loader } from "three";
7 |
8 | import { OBJLoader2Parser } from "./obj2/worker/parallel/OBJLoader2Parser";
9 | import { MeshReceiver } from "./obj2/shared/MeshReceiver";
10 | import { MaterialHandler } from "./obj2/shared/MaterialHandler";
11 |
12 | /**
13 | * Creates a new OBJLoader2. Use it to load OBJ data from files or to parse OBJ data from arraybuffer or text.
14 | *
15 | * @param {LoadingManager} [manager] The loadingManager for the loader to use. Default is {@link LoadingManager}
16 | * @constructor
17 | */
18 | const OBJLoader2 = function(manager) {
19 | Loader.call(this, manager);
20 |
21 | this.parser = new OBJLoader2Parser();
22 |
23 | this.modelName = "";
24 | this.instanceNo = 0;
25 | this.baseObject3d = new Object3D();
26 |
27 | this.materialHandler = new MaterialHandler();
28 | this.meshReceiver = new MeshReceiver(this.materialHandler);
29 |
30 | // as OBJLoader2 is no longer derived from OBJLoader2Parser, we need to override the default onAssetAvailable callback
31 | let scope = this;
32 | let defaultOnAssetAvailable = function(payload) {
33 | scope._onAssetAvailable(payload);
34 | };
35 | this.parser.setCallbackOnAssetAvailable(defaultOnAssetAvailable);
36 | };
37 |
38 | OBJLoader2.OBJLOADER2_VERSION = "3.1.1";
39 | console.info("Using OBJLoader2 version: " + OBJLoader2.OBJLOADER2_VERSION);
40 |
41 | OBJLoader2.prototype = Object.assign(Object.create(Loader.prototype), {
42 | constructor: OBJLoader2,
43 |
44 | /**
45 | * See {@link OBJLoader2Parser.setLogging}
46 | * @return {OBJLoader2}
47 | */
48 | setLogging: function(enabled, debug) {
49 | this.parser.setLogging(enabled, debug);
50 | return this;
51 | },
52 |
53 | /**
54 | * See {@link OBJLoader2Parser.setMaterialPerSmoothingGroup}
55 | * @return {OBJLoader2}
56 | */
57 | setMaterialPerSmoothingGroup: function(materialPerSmoothingGroup) {
58 | this.parser.setMaterialPerSmoothingGroup(materialPerSmoothingGroup);
59 | return this;
60 | },
61 |
62 | /**
63 | * See {@link OBJLoader2Parser.setUseOAsMesh}
64 | * @return {OBJLoader2}
65 | */
66 | setUseOAsMesh: function(useOAsMesh) {
67 | this.parser.setUseOAsMesh(useOAsMesh);
68 | return this;
69 | },
70 |
71 | /**
72 | * See {@link OBJLoader2Parser.setUseIndices}
73 | * @return {OBJLoader2}
74 | */
75 | setUseIndices: function(useIndices) {
76 | this.parser.setUseIndices(useIndices);
77 | return this;
78 | },
79 |
80 | /**
81 | * See {@link OBJLoader2Parser.setDisregardNormals}
82 | * @return {OBJLoader2}
83 | */
84 | setDisregardNormals: function(disregardNormals) {
85 | this.parser.setDisregardNormals(disregardNormals);
86 | return this;
87 | },
88 |
89 | /**
90 | * Set the name of the model.
91 | *
92 | * @param {string} modelName
93 | * @return {OBJLoader2}
94 | */
95 | setModelName: function(modelName) {
96 | this.modelName = modelName ? modelName : this.modelName;
97 | return this;
98 | },
99 |
100 | /**
101 | * Set the node where the loaded objects will be attached directly.
102 | *
103 | * @param {Object3D} baseObject3d Object already attached to scenegraph where new meshes will be attached to
104 | * @return {OBJLoader2}
105 | */
106 | setBaseObject3d: function(baseObject3d) {
107 | this.baseObject3d =
108 | baseObject3d === undefined || baseObject3d === null
109 | ? this.baseObject3d
110 | : baseObject3d;
111 | return this;
112 | },
113 |
114 | /**
115 | * Add materials as associated array.
116 | *
117 | * @param {Object} materials Object with named {@link Material}
118 | * @param overrideExisting boolean Override existing material
119 | * @return {OBJLoader2}
120 | */
121 | addMaterials: function(materials, overrideExisting) {
122 | this.materialHandler.addMaterials(materials, overrideExisting);
123 | return this;
124 | },
125 |
126 | /**
127 | * See {@link OBJLoader2Parser.setCallbackOnAssetAvailable}
128 | * @return {OBJLoader2}
129 | */
130 | setCallbackOnAssetAvailable: function(onAssetAvailable) {
131 | this.parser.setCallbackOnAssetAvailable(onAssetAvailable);
132 | return this;
133 | },
134 |
135 | /**
136 | * See {@link OBJLoader2Parser.setCallbackOnProgress}
137 | * @return {OBJLoader2}
138 | */
139 | setCallbackOnProgress: function(onProgress) {
140 | this.parser.setCallbackOnProgress(onProgress);
141 | return this;
142 | },
143 |
144 | /**
145 | * See {@link OBJLoader2Parser.setCallbackOnError}
146 | * @return {OBJLoader2}
147 | */
148 | setCallbackOnError: function(onError) {
149 | this.parser.setCallbackOnError(onError);
150 | return this;
151 | },
152 |
153 | /**
154 | * See {@link OBJLoader2Parser.setCallbackOnLoad}
155 | * @return {OBJLoader2}
156 | */
157 | setCallbackOnLoad: function(onLoad) {
158 | this.parser.setCallbackOnLoad(onLoad);
159 | return this;
160 | },
161 |
162 | /**
163 | * Register a function that is called once a single mesh is available and it could be altered by the supplied function.
164 | *
165 | * @param {Function} [onMeshAlter]
166 | * @return {OBJLoader2}
167 | */
168 | setCallbackOnMeshAlter: function(onMeshAlter) {
169 | this.meshReceiver._setCallbacks(
170 | this.parser.callbacks.onProgress,
171 | onMeshAlter
172 | );
173 | return this;
174 | },
175 |
176 | /**
177 | * Register a function that is called once all materials have been loaded and they could be altered by the supplied function.
178 | *
179 | * @param {Function} [onLoadMaterials]
180 | * @return {OBJLoader2}
181 | */
182 | setCallbackOnLoadMaterials: function(onLoadMaterials) {
183 | this.materialHandler._setCallbacks(onLoadMaterials);
184 | return this;
185 | },
186 |
187 | /**
188 | * Use this convenient method to load a file at the given URL. By default the fileLoader uses an ArrayBuffer.
189 | *
190 | * @param {string} url A string containing the path/URL of the file to be loaded.
191 | * @param {function} onLoad A function to be called after loading is successfully completed. The function receives loaded Object3D as an argument.
192 | * @param {function} [onFileLoadProgress] A function to be called while the loading is in progress. The argument will be the XMLHttpRequest instance, which contains total and Integer bytes.
193 | * @param {function} [onError] A function to be called if an error occurs during loading. The function receives the error as an argument.
194 | * @param {function} [onMeshAlter] Called after every single mesh is made available by the parser
195 | */
196 | load: function(url, onLoad, onFileLoadProgress, onError, onMeshAlter) {
197 | let scope = this;
198 | if (
199 | onLoad === null ||
200 | onLoad === undefined ||
201 | !(onLoad instanceof Function)
202 | ) {
203 | let errorMessage = "onLoad is not a function! Aborting...";
204 | scope.parser.callbacks.onError(errorMessage);
205 | throw errorMessage;
206 | } else {
207 | this.parser.setCallbackOnLoad(onLoad);
208 | }
209 | if (
210 | onError === null ||
211 | onError === undefined ||
212 | !(onError instanceof Function)
213 | ) {
214 | onError = function(event) {
215 | let errorMessage = event;
216 | if (event.currentTarget && event.currentTarget.statusText !== null) {
217 | errorMessage =
218 | "Error occurred while downloading!\nurl: " +
219 | event.currentTarget.responseURL +
220 | "\nstatus: " +
221 | event.currentTarget.statusText;
222 | }
223 | scope.parser.callbacks.onError(errorMessage);
224 | };
225 | }
226 | if (!url) {
227 | onError("An invalid url was provided. Unable to continue!");
228 | }
229 |
230 | // This does not work in a web worker because window is not available.
231 | // let urlFull = new URL(url, window.location.href).href;
232 | const urlFull = new URL(url).href;
233 |
234 | let filename = urlFull;
235 | let urlParts = urlFull.split("/");
236 | if (urlParts.length > 2) {
237 | filename = urlParts[urlParts.length - 1];
238 | let urlPartsPath = urlParts.slice(0, urlParts.length - 1).join("/") + "/";
239 | if (urlPartsPath !== undefined && urlPartsPath !== null)
240 | this.path = urlPartsPath;
241 | }
242 | if (
243 | onFileLoadProgress === null ||
244 | onFileLoadProgress === undefined ||
245 | !(onFileLoadProgress instanceof Function)
246 | ) {
247 | let numericalValueRef = 0;
248 | let numericalValue = 0;
249 | onFileLoadProgress = function(event) {
250 | if (!event.lengthComputable) return;
251 |
252 | numericalValue = event.loaded / event.total;
253 | if (numericalValue > numericalValueRef) {
254 | numericalValueRef = numericalValue;
255 | let output =
256 | 'Download of "' +
257 | url +
258 | '": ' +
259 | (numericalValue * 100).toFixed(2) +
260 | "%";
261 | scope.parser.callbacks.onProgress(
262 | "progressLoad",
263 | output,
264 | numericalValue
265 | );
266 | }
267 | };
268 | }
269 |
270 | this.setCallbackOnMeshAlter(onMeshAlter);
271 | let fileLoaderOnLoad = function(content) {
272 | scope.parser.callbacks.onLoad(
273 | scope.parse(content),
274 | "OBJLoader2#load: Parsing completed"
275 | );
276 | };
277 | let fileLoader = new FileLoader(this.manager);
278 | fileLoader.setPath(this.path || this.resourcePath);
279 | fileLoader.setResponseType("arraybuffer");
280 | fileLoader.load(filename, fileLoaderOnLoad, onFileLoadProgress, onError);
281 | },
282 |
283 | /**
284 | * Parses OBJ data synchronously from arraybuffer or string and returns the {@link Object3D}.
285 | *
286 | * @param {arraybuffer|string} content OBJ data as Uint8Array or String
287 | * @return {Object3D}
288 | */
289 | parse: function(content) {
290 | // fast-fail in case of illegal data
291 | if (content === null || content === undefined) {
292 | throw "Provided content is not a valid ArrayBuffer or String. Unable to continue parsing";
293 | }
294 | if (this.parser.logging.enabled) {
295 | console.time("OBJLoader parse: " + this.modelName);
296 | }
297 |
298 | // Create default materials beforehand, but do not override previously set materials (e.g. during init)
299 | this.materialHandler.createDefaultMaterials(false);
300 |
301 | // code works directly on the material references, parser clear its materials before updating
302 | this.parser.setMaterials(this.materialHandler.getMaterials());
303 |
304 | if (content instanceof ArrayBuffer || content instanceof Uint8Array) {
305 | if (this.parser.logging.enabled) console.info("Parsing arrayBuffer...");
306 | this.parser.execute(content);
307 | } else if (typeof content === "string" || content instanceof String) {
308 | if (this.parser.logging.enabled) console.info("Parsing text...");
309 | this.parser.executeLegacy(content);
310 | } else {
311 | this.parser.callbacks.onError(
312 | "Provided content was neither of type String nor Uint8Array! Aborting..."
313 | );
314 | }
315 | if (this.parser.logging.enabled) {
316 | console.timeEnd("OBJLoader parse: " + this.modelName);
317 | }
318 | return this.baseObject3d;
319 | },
320 |
321 | _onAssetAvailable: function(payload) {
322 | if (payload.cmd !== "assetAvailable") return;
323 |
324 | if (payload.type === "mesh") {
325 | let meshes = this.meshReceiver.buildMeshes(payload);
326 | for (let mesh of meshes) {
327 | this.baseObject3d.add(mesh);
328 | }
329 | } else if (payload.type === "material") {
330 | this.materialHandler.addPayloadMaterials(payload);
331 | }
332 | },
333 | });
334 |
335 | export { OBJLoader2 };
336 |
--------------------------------------------------------------------------------
/src/js/vendor/obj2/shared/MaterialHandler.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kai Salmen / https://kaisalmen.de
3 | * Development repository: https://github.com/kaisalmen/WWOBJLoader
4 | */
5 | import {
6 | LineBasicMaterial,
7 | MaterialLoader,
8 | MeshStandardMaterial,
9 | PointsMaterial,
10 | VertexColors,
11 | } from "three";
12 |
13 | const MaterialHandler = function() {
14 | this.logging = {
15 | enabled: false,
16 | debug: false,
17 | };
18 |
19 | this.callbacks = {
20 | onLoadMaterials: null,
21 | };
22 | this.materials = {};
23 | };
24 |
25 | MaterialHandler.prototype = {
26 | constructor: MaterialHandler,
27 |
28 | /**
29 | * Enable or disable logging in general (except warn and error), plus enable or disable debug logging.
30 | *
31 | * @param {boolean} enabled True or false.
32 | * @param {boolean} debug True or false.
33 | */
34 | setLogging: function(enabled, debug) {
35 | this.logging.enabled = enabled === true;
36 | this.logging.debug = debug === true;
37 | },
38 |
39 | _setCallbacks: function(onLoadMaterials) {
40 | if (
41 | onLoadMaterials !== undefined &&
42 | onLoadMaterials !== null &&
43 | onLoadMaterials instanceof Function
44 | ) {
45 | this.callbacks.onLoadMaterials = onLoadMaterials;
46 | }
47 | },
48 |
49 | /**
50 | * Creates default materials and adds them to the materials object.
51 | *
52 | * @param overrideExisting boolean Override existing material
53 | */
54 | createDefaultMaterials: function(overrideExisting) {
55 | let defaultMaterial = new MeshStandardMaterial({ color: 0xdcf1ff });
56 | defaultMaterial.name = "defaultMaterial";
57 |
58 | let defaultVertexColorMaterial = new MeshStandardMaterial({
59 | color: 0xdcf1ff,
60 | });
61 | defaultVertexColorMaterial.name = "defaultVertexColorMaterial";
62 | defaultVertexColorMaterial.vertexColors = VertexColors;
63 |
64 | let defaultLineMaterial = new LineBasicMaterial();
65 | defaultLineMaterial.name = "defaultLineMaterial";
66 |
67 | let defaultPointMaterial = new PointsMaterial({ size: 0.1 });
68 | defaultPointMaterial.name = "defaultPointMaterial";
69 |
70 | let runtimeMaterials = {};
71 | runtimeMaterials[defaultMaterial.name] = defaultMaterial;
72 | runtimeMaterials[
73 | defaultVertexColorMaterial.name
74 | ] = defaultVertexColorMaterial;
75 | runtimeMaterials[defaultLineMaterial.name] = defaultLineMaterial;
76 | runtimeMaterials[defaultPointMaterial.name] = defaultPointMaterial;
77 |
78 | this.addMaterials(runtimeMaterials, overrideExisting);
79 | },
80 |
81 | /**
82 | * Updates the materials with contained material objects (sync) or from alteration instructions (async).
83 | *
84 | * @param {Object} materialPayload Material update instructions
85 | * @returns {Object} Map of {@link Material}
86 | */
87 | addPayloadMaterials: function(materialPayload) {
88 | let material, materialName;
89 | let materialCloneInstructions =
90 | materialPayload.materials.materialCloneInstructions;
91 | let newMaterials = {};
92 |
93 | if (
94 | materialCloneInstructions !== undefined &&
95 | materialCloneInstructions !== null
96 | ) {
97 | let materialNameOrg = materialCloneInstructions.materialNameOrg;
98 | materialNameOrg =
99 | materialNameOrg !== undefined && materialNameOrg !== null
100 | ? materialNameOrg
101 | : "";
102 | let materialOrg = this.materials[materialNameOrg];
103 | if (materialOrg) {
104 | material = materialOrg.clone();
105 |
106 | materialName = materialCloneInstructions.materialName;
107 | material.name = materialName;
108 |
109 | Object.assign(material, materialCloneInstructions.materialProperties);
110 |
111 | this.materials[materialName] = material;
112 | newMaterials[materialName] = material;
113 | } else {
114 | if (this.logging.enabled) {
115 | console.info(
116 | 'Requested material "' + materialNameOrg + '" is not available!'
117 | );
118 | }
119 | }
120 | }
121 |
122 | let materials = materialPayload.materials.serializedMaterials;
123 | if (
124 | materials !== undefined &&
125 | materials !== null &&
126 | Object.keys(materials).length > 0
127 | ) {
128 | let loader = new MaterialLoader();
129 | let materialJson;
130 | for (materialName in materials) {
131 | materialJson = materials[materialName];
132 | if (materialJson !== undefined && materialJson !== null) {
133 | material = loader.parse(materialJson);
134 | if (this.logging.enabled) {
135 | console.info(
136 | 'De-serialized material with name "' +
137 | materialName +
138 | '" will be added.'
139 | );
140 | }
141 | this.materials[materialName] = material;
142 | newMaterials[materialName] = material;
143 | }
144 | }
145 | }
146 | materials = materialPayload.materials.runtimeMaterials;
147 | newMaterials = this.addMaterials(materials, true, newMaterials);
148 |
149 | return newMaterials;
150 | },
151 |
152 | /**
153 | * Set materials loaded by any supplier of an Array of {@link Material}.
154 | *
155 | * @param materials Object with named {@link Material}
156 | * @param overrideExisting boolean Override existing material
157 | * @param newMaterials [Object] with named {@link Material}
158 | */
159 | addMaterials: function(materials, overrideExisting, newMaterials) {
160 | if (newMaterials === undefined || newMaterials === null) {
161 | newMaterials = {};
162 | }
163 | if (
164 | materials !== undefined &&
165 | materials !== null &&
166 | Object.keys(materials).length > 0
167 | ) {
168 | let material;
169 | let existingMaterial;
170 | let add;
171 | for (let materialName in materials) {
172 | material = materials[materialName];
173 | add = overrideExisting === true;
174 | if (!add) {
175 | existingMaterial = this.materials[materialName];
176 | add = existingMaterial === null || existingMaterial === undefined;
177 | }
178 | if (add) {
179 | this.materials[materialName] = material;
180 | newMaterials[materialName] = material;
181 | }
182 | if (this.logging.enabled && this.logging.debug) {
183 | console.info('Material with name "' + materialName + '" was added.');
184 | }
185 | }
186 | }
187 |
188 | if (this.callbacks.onLoadMaterials) {
189 | this.callbacks.onLoadMaterials(newMaterials);
190 | }
191 | return newMaterials;
192 | },
193 |
194 | /**
195 | * Returns the mapping object of material name and corresponding material.
196 | *
197 | * @returns {Object} Map of {@link Material}
198 | */
199 | getMaterials: function() {
200 | return this.materials;
201 | },
202 |
203 | /**
204 | *
205 | * @param {String} materialName
206 | * @returns {Material}
207 | */
208 | getMaterial: function(materialName) {
209 | return this.materials[materialName];
210 | },
211 |
212 | /**
213 | * Returns the mapping object of material name and corresponding jsonified material.
214 | *
215 | * @returns {Object} Map of Materials in JSON representation
216 | */
217 | getMaterialsJSON: function() {
218 | let materialsJSON = {};
219 | let material;
220 | for (let materialName in this.materials) {
221 | material = this.materials[materialName];
222 | materialsJSON[materialName] = material.toJSON();
223 | }
224 |
225 | return materialsJSON;
226 | },
227 |
228 | /**
229 | * Removes all materials
230 | */
231 | clearMaterials: function() {
232 | this.materials = {};
233 | },
234 | };
235 |
236 | export { MaterialHandler };
237 |
--------------------------------------------------------------------------------
/src/js/vendor/obj2/shared/MeshReceiver.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kai Salmen / https://kaisalmen.de
3 | * Development repository: https://github.com/kaisalmen/WWOBJLoader
4 | */
5 |
6 | import {
7 | BufferAttribute,
8 | BufferGeometry,
9 | LineSegments,
10 | Mesh,
11 | Points,
12 | } from "three";
13 |
14 | /**
15 | *
16 | * @param {MaterialHandler} materialHandler
17 | * @constructor
18 | */
19 | const MeshReceiver = function(materialHandler) {
20 | this.logging = {
21 | enabled: false,
22 | debug: false,
23 | };
24 |
25 | this.callbacks = {
26 | onProgress: null,
27 | onMeshAlter: null,
28 | };
29 | this.materialHandler = materialHandler;
30 | };
31 |
32 | MeshReceiver.prototype = {
33 | constructor: MeshReceiver,
34 |
35 | /**
36 | * Enable or disable logging in general (except warn and error), plus enable or disable debug logging.
37 | *
38 | * @param {boolean} enabled True or false.
39 | * @param {boolean} debug True or false.
40 | */
41 | setLogging: function(enabled, debug) {
42 | this.logging.enabled = enabled === true;
43 | this.logging.debug = debug === true;
44 | },
45 |
46 | /**
47 | *
48 | * @param {Function} onProgress
49 | * @param {Function} onMeshAlter
50 | * @private
51 | */
52 | _setCallbacks: function(onProgress, onMeshAlter) {
53 | if (
54 | onProgress !== null &&
55 | onProgress !== undefined &&
56 | onProgress instanceof Function
57 | ) {
58 | this.callbacks.onProgress = onProgress;
59 | }
60 | if (
61 | onMeshAlter !== null &&
62 | onMeshAlter !== undefined &&
63 | onMeshAlter instanceof Function
64 | ) {
65 | this.callbacks.onMeshAlter = onMeshAlter;
66 | }
67 | },
68 |
69 | /**
70 | * Builds one or multiple meshes from the data described in the payload (buffers, params, material info).
71 | *
72 | * @param {Object} meshPayload Raw mesh description (buffers, params, materials) used to build one to many meshes.
73 | * @returns {Mesh[]} mesh Array of {@link Mesh}
74 | */
75 | buildMeshes: function(meshPayload) {
76 | let meshName = meshPayload.params.meshName;
77 | let buffers = meshPayload.buffers;
78 |
79 | let bufferGeometry = new BufferGeometry();
80 | if (buffers.vertices !== undefined && buffers.vertices !== null) {
81 | bufferGeometry.setAttribute(
82 | "position",
83 | new BufferAttribute(new Float32Array(buffers.vertices), 3)
84 | );
85 | }
86 | if (buffers.indices !== undefined && buffers.indices !== null) {
87 | bufferGeometry.setIndex(
88 | new BufferAttribute(new Uint32Array(buffers.indices), 1)
89 | );
90 | }
91 | if (buffers.colors !== undefined && buffers.colors !== null) {
92 | bufferGeometry.setAttribute(
93 | "color",
94 | new BufferAttribute(new Float32Array(buffers.colors), 3)
95 | );
96 | }
97 | if (buffers.normals !== undefined && buffers.normals !== null) {
98 | bufferGeometry.setAttribute(
99 | "normal",
100 | new BufferAttribute(new Float32Array(buffers.normals), 3)
101 | );
102 | } else {
103 | bufferGeometry.computeVertexNormals();
104 | }
105 | if (buffers.uvs !== undefined && buffers.uvs !== null) {
106 | bufferGeometry.setAttribute(
107 | "uv",
108 | new BufferAttribute(new Float32Array(buffers.uvs), 2)
109 | );
110 | }
111 | if (buffers.skinIndex !== undefined && buffers.skinIndex !== null) {
112 | bufferGeometry.setAttribute(
113 | "skinIndex",
114 | new BufferAttribute(new Uint16Array(buffers.skinIndex), 4)
115 | );
116 | }
117 | if (buffers.skinWeight !== undefined && buffers.skinWeight !== null) {
118 | bufferGeometry.setAttribute(
119 | "skinWeight",
120 | new BufferAttribute(new Float32Array(buffers.skinWeight), 4)
121 | );
122 | }
123 |
124 | let material, materialName, key;
125 | let materialNames = meshPayload.materials.materialNames;
126 | let createMultiMaterial = meshPayload.materials.multiMaterial;
127 | let multiMaterials = [];
128 | for (key in materialNames) {
129 | materialName = materialNames[key];
130 | material = this.materialHandler.getMaterial(materialName);
131 | if (createMultiMaterial) multiMaterials.push(material);
132 | }
133 | if (createMultiMaterial) {
134 | material = multiMaterials;
135 | let materialGroups = meshPayload.materials.materialGroups;
136 | let materialGroup;
137 | for (key in materialGroups) {
138 | materialGroup = materialGroups[key];
139 | bufferGeometry.addGroup(
140 | materialGroup.start,
141 | materialGroup.count,
142 | materialGroup.index
143 | );
144 | }
145 | }
146 |
147 | let meshes = [];
148 | let mesh;
149 | let callbackOnMeshAlterResult;
150 | let useOrgMesh = true;
151 | let geometryType =
152 | meshPayload.geometryType === null ? 0 : meshPayload.geometryType;
153 |
154 | if (this.callbacks.onMeshAlter) {
155 | callbackOnMeshAlterResult = this.callbacks.onMeshAlter({
156 | detail: {
157 | meshName: meshName,
158 | bufferGeometry: bufferGeometry,
159 | material: material,
160 | geometryType: geometryType,
161 | },
162 | });
163 | }
164 |
165 | // here LoadedMeshUserOverride is required to be provided by the callback used to alter the results
166 | if (callbackOnMeshAlterResult) {
167 | if (callbackOnMeshAlterResult.isDisregardMesh()) {
168 | useOrgMesh = false;
169 | } else if (callbackOnMeshAlterResult.providesAlteredMeshes()) {
170 | for (let i in callbackOnMeshAlterResult.meshes) {
171 | meshes.push(callbackOnMeshAlterResult.meshes[i]);
172 | }
173 | useOrgMesh = false;
174 | }
175 | }
176 | if (useOrgMesh) {
177 | if (meshPayload.computeBoundingSphere)
178 | bufferGeometry.computeBoundingSphere();
179 | if (geometryType === 0) {
180 | mesh = new Mesh(bufferGeometry, material);
181 | } else if (geometryType === 1) {
182 | mesh = new LineSegments(bufferGeometry, material);
183 | } else {
184 | mesh = new Points(bufferGeometry, material);
185 | }
186 | mesh.name = meshName;
187 | meshes.push(mesh);
188 | }
189 |
190 | let progressMessage = meshPayload.params.meshName;
191 | if (meshes.length > 0) {
192 | let meshNames = [];
193 | for (let i in meshes) {
194 | mesh = meshes[i];
195 | meshNames[i] = mesh.name;
196 | }
197 | progressMessage +=
198 | ": Adding mesh(es) (" +
199 | meshNames.length +
200 | ": " +
201 | meshNames +
202 | ") from input mesh: " +
203 | meshName;
204 | progressMessage +=
205 | " (" + (meshPayload.progress.numericalValue * 100).toFixed(2) + "%)";
206 | } else {
207 | progressMessage += ": Not adding mesh: " + meshName;
208 | progressMessage +=
209 | " (" + (meshPayload.progress.numericalValue * 100).toFixed(2) + "%)";
210 | }
211 | if (this.callbacks.onProgress) {
212 | this.callbacks.onProgress(
213 | "progress",
214 | progressMessage,
215 | meshPayload.progress.numericalValue
216 | );
217 | }
218 |
219 | return meshes;
220 | },
221 | };
222 |
223 | /**
224 | * Object to return by callback onMeshAlter. Used to disregard a certain mesh or to return one to many meshes.
225 | * @class
226 | *
227 | * @param {boolean} disregardMesh=false Tell implementation to completely disregard this mesh
228 | * @param {boolean} disregardMesh=false Tell implementation that mesh(es) have been altered or added
229 | */
230 | const LoadedMeshUserOverride = function(disregardMesh, alteredMesh) {
231 | this.disregardMesh = disregardMesh === true;
232 | this.alteredMesh = alteredMesh === true;
233 | this.meshes = [];
234 | };
235 |
236 | LoadedMeshUserOverride.prototype = {
237 | constructor: LoadedMeshUserOverride,
238 |
239 | /**
240 | * Add a mesh created within callback.
241 | *
242 | * @param {Mesh} mesh
243 | */
244 | addMesh: function(mesh) {
245 | this.meshes.push(mesh);
246 | this.alteredMesh = true;
247 | },
248 |
249 | /**
250 | * Answers if mesh shall be disregarded completely.
251 | *
252 | * @returns {boolean}
253 | */
254 | isDisregardMesh: function() {
255 | return this.disregardMesh;
256 | },
257 |
258 | /**
259 | * Answers if new mesh(es) were created.
260 | *
261 | * @returns {boolean}
262 | */
263 | providesAlteredMeshes: function() {
264 | return this.alteredMesh;
265 | },
266 | };
267 |
268 | export { MeshReceiver, LoadedMeshUserOverride };
269 |
--------------------------------------------------------------------------------
/src/js/vendor/obj2/worker/parallel/OBJLoader2Parser.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kai Salmen / https://kaisalmen.de
3 | * Development repository: https://github.com/kaisalmen/WWOBJLoader
4 | */
5 |
6 | /**
7 | * Parse OBJ data either from ArrayBuffer or string
8 | */
9 | const OBJLoader2Parser = function() {
10 | this.logging = {
11 | enabled: false,
12 | debug: false,
13 | };
14 |
15 | let scope = this;
16 | this.callbacks = {
17 | onProgress: function(text) {
18 | scope._onProgress(text);
19 | },
20 | onAssetAvailable: function(payload) {
21 | scope._onAssetAvailable(payload);
22 | },
23 | onError: function(errorMessage) {
24 | scope._onError(errorMessage);
25 | },
26 | onLoad: function(object3d, message) {
27 | scope._onLoad(object3d, message);
28 | },
29 | };
30 | this.contentRef = null;
31 | this.legacyMode = false;
32 |
33 | this.materials = {};
34 | this.materialPerSmoothingGroup = false;
35 | this.useOAsMesh = false;
36 | this.useIndices = false;
37 | this.disregardNormals = false;
38 |
39 | this.vertices = [];
40 | this.colors = [];
41 | this.normals = [];
42 | this.uvs = [];
43 |
44 | this.rawMesh = {
45 | objectName: "",
46 | groupName: "",
47 | activeMtlName: "",
48 | mtllibName: "",
49 |
50 | // reset with new mesh
51 | faceType: -1,
52 | subGroups: [],
53 | subGroupInUse: null,
54 | smoothingGroup: {
55 | splitMaterials: false,
56 | normalized: -1,
57 | real: -1,
58 | },
59 | counts: {
60 | doubleIndicesCount: 0,
61 | faceCount: 0,
62 | mtlCount: 0,
63 | smoothingGroupCount: 0,
64 | },
65 | };
66 |
67 | this.inputObjectCount = 1;
68 | this.outputObjectCount = 1;
69 | this.globalCounts = {
70 | vertices: 0,
71 | faces: 0,
72 | doubleIndicesCount: 0,
73 | lineByte: 0,
74 | currentByte: 0,
75 | totalBytes: 0,
76 | };
77 | };
78 |
79 | OBJLoader2Parser.prototype = {
80 | constructor: OBJLoader2Parser,
81 |
82 | _resetRawMesh: function() {
83 | // faces are stored according combined index of group, material and smoothingGroup (0 or not)
84 | this.rawMesh.subGroups = [];
85 | this.rawMesh.subGroupInUse = null;
86 | this.rawMesh.smoothingGroup.normalized = -1;
87 | this.rawMesh.smoothingGroup.real = -1;
88 |
89 | // this default index is required as it is possible to define faces without 'g' or 'usemtl'
90 | this._pushSmoothingGroup(1);
91 |
92 | this.rawMesh.counts.doubleIndicesCount = 0;
93 | this.rawMesh.counts.faceCount = 0;
94 | this.rawMesh.counts.mtlCount = 0;
95 | this.rawMesh.counts.smoothingGroupCount = 0;
96 | },
97 |
98 | /**
99 | * Tells whether a material shall be created per smoothing group.
100 | *
101 | * @param {boolean} materialPerSmoothingGroup=false
102 | * @return {OBJLoader2Parser}
103 | */
104 | setMaterialPerSmoothingGroup: function(materialPerSmoothingGroup) {
105 | this.materialPerSmoothingGroup = materialPerSmoothingGroup === true;
106 | return this;
107 | },
108 |
109 | /**
110 | * Usually 'o' is meta-information and does not result in creation of new meshes, but mesh creation on occurrence of "o" can be enforced.
111 | *
112 | * @param {boolean} useOAsMesh=false
113 | * @return {OBJLoader2Parser}
114 | */
115 | setUseOAsMesh: function(useOAsMesh) {
116 | this.useOAsMesh = useOAsMesh === true;
117 | return this;
118 | },
119 |
120 | /**
121 | * Instructs loaders to create indexed {@link BufferGeometry}.
122 | *
123 | * @param {boolean} useIndices=false
124 | * @return {OBJLoader2Parser}
125 | */
126 | setUseIndices: function(useIndices) {
127 | this.useIndices = useIndices === true;
128 | return this;
129 | },
130 |
131 | /**
132 | * Tells whether normals should be completely disregarded and regenerated.
133 | *
134 | * @param {boolean} disregardNormals=false
135 | * @return {OBJLoader2Parser}
136 | */
137 | setDisregardNormals: function(disregardNormals) {
138 | this.disregardNormals = disregardNormals === true;
139 | return this;
140 | },
141 |
142 | /**
143 | * Clears materials object and sets the new ones.
144 | *
145 | * @param {Object} materials Object with named materials
146 | */
147 | setMaterials: function(materials) {
148 | this.materials = Object.assign({}, materials);
149 | },
150 |
151 | /**
152 | * Register a function that is called once an asset (mesh/material) becomes available.
153 | *
154 | * @param onAssetAvailable
155 | * @return {OBJLoader2Parser}
156 | */
157 | setCallbackOnAssetAvailable: function(onAssetAvailable) {
158 | if (
159 | onAssetAvailable !== null &&
160 | onAssetAvailable !== undefined &&
161 | onAssetAvailable instanceof Function
162 | ) {
163 | this.callbacks.onAssetAvailable = onAssetAvailable;
164 | }
165 | return this;
166 | },
167 |
168 | /**
169 | * Register a function that is used to report overall processing progress.
170 | *
171 | * @param {Function} onProgress
172 | * @return {OBJLoader2Parser}
173 | */
174 | setCallbackOnProgress: function(onProgress) {
175 | if (
176 | onProgress !== null &&
177 | onProgress !== undefined &&
178 | onProgress instanceof Function
179 | ) {
180 | this.callbacks.onProgress = onProgress;
181 | }
182 | return this;
183 | },
184 |
185 | /**
186 | * Register an error handler function that is called if errors occur. It can decide to just log or to throw an exception.
187 | *
188 | * @param {Function} onError
189 | * @return {OBJLoader2Parser}
190 | */
191 | setCallbackOnError: function(onError) {
192 | if (
193 | onError !== null &&
194 | onError !== undefined &&
195 | onError instanceof Function
196 | ) {
197 | this.callbacks.onError = onError;
198 | }
199 | return this;
200 | },
201 |
202 | /**
203 | * Register a function that is called when parsing was completed.
204 | *
205 | * @param {Function} onLoad
206 | * @return {OBJLoader2Parser}
207 | */
208 | setCallbackOnLoad: function(onLoad) {
209 | if (onLoad !== null && onLoad !== undefined && onLoad instanceof Function) {
210 | this.callbacks.onLoad = onLoad;
211 | }
212 | return this;
213 | },
214 |
215 | /**
216 | * Announce parse progress feedback which is logged to the console.
217 | * @private
218 | *
219 | * @param {string} text Textual description of the event
220 | */
221 | _onProgress: function(text) {
222 | let message = text ? text : "";
223 | if (this.logging.enabled && this.logging.debug) {
224 | console.log(message);
225 | }
226 | },
227 |
228 | /**
229 | * Announce error feedback which is logged as error message.
230 | * @private
231 | *
232 | * @param {String} errorMessage The event containing the error
233 | */
234 | _onError: function(errorMessage) {
235 | if (this.logging.enabled && this.logging.debug) {
236 | console.error(errorMessage);
237 | }
238 | },
239 |
240 | _onAssetAvailable: function(payload) {
241 | let errorMessage =
242 | "OBJLoader2Parser does not provide implementation for onAssetAvailable. Aborting...";
243 | this.callbacks.onError(errorMessage);
244 | throw errorMessage;
245 | },
246 |
247 | _onLoad: function(object3d, message) {
248 | console.log("You reached parser default onLoad callback: " + message);
249 | },
250 |
251 | /**
252 | * Enable or disable logging in general (except warn and error), plus enable or disable debug logging.
253 | *
254 | * @param {boolean} enabled True or false.
255 | * @param {boolean} debug True or false.
256 | *
257 | * @return {OBJLoader2Parser}
258 | */
259 | setLogging: function(enabled, debug) {
260 | this.logging.enabled = enabled === true;
261 | this.logging.debug = debug === true;
262 | return this;
263 | },
264 |
265 | _configure: function() {
266 | this._pushSmoothingGroup(1);
267 | if (this.logging.enabled) {
268 | let matKeys = Object.keys(this.materials);
269 | let matNames =
270 | matKeys.length > 0
271 | ? "\n\tmaterialNames:\n\t\t- " + matKeys.join("\n\t\t- ")
272 | : "\n\tmaterialNames: None";
273 | let printedConfig =
274 | "OBJLoader.Parser configuration:" +
275 | matNames +
276 | "\n\tmaterialPerSmoothingGroup: " +
277 | this.materialPerSmoothingGroup +
278 | "\n\tuseOAsMesh: " +
279 | this.useOAsMesh +
280 | "\n\tuseIndices: " +
281 | this.useIndices +
282 | "\n\tdisregardNormals: " +
283 | this.disregardNormals;
284 | printedConfig +=
285 | "\n\tcallbacks.onProgress: " + this.callbacks.onProgress.name;
286 | printedConfig +=
287 | "\n\tcallbacks.onAssetAvailable: " +
288 | this.callbacks.onAssetAvailable.name;
289 | printedConfig += "\n\tcallbacks.onError: " + this.callbacks.onError.name;
290 | console.info(printedConfig);
291 | }
292 | },
293 |
294 | /**
295 | * Parse the provided arraybuffer
296 | *
297 | * @param {Uint8Array} arrayBuffer OBJ data as Uint8Array
298 | */
299 | execute: function(arrayBuffer) {
300 | if (this.logging.enabled) console.time("OBJLoader2Parser.execute");
301 | this._configure();
302 |
303 | let arrayBufferView = new Uint8Array(arrayBuffer);
304 | this.contentRef = arrayBufferView;
305 | let length = arrayBufferView.byteLength;
306 | this.globalCounts.totalBytes = length;
307 | let buffer = new Array(128);
308 |
309 | for (
310 | let code, word = "", bufferPointer = 0, slashesCount = 0, i = 0;
311 | i < length;
312 | i++
313 | ) {
314 | code = arrayBufferView[i];
315 | switch (code) {
316 | // space
317 | case 32:
318 | if (word.length > 0) buffer[bufferPointer++] = word;
319 | word = "";
320 | break;
321 | // slash
322 | case 47:
323 | if (word.length > 0) buffer[bufferPointer++] = word;
324 | slashesCount++;
325 | word = "";
326 | break;
327 |
328 | // LF
329 | case 10:
330 | if (word.length > 0) buffer[bufferPointer++] = word;
331 | word = "";
332 | this.globalCounts.lineByte = this.globalCounts.currentByte;
333 | this.globalCounts.currentByte = i;
334 | this._processLine(buffer, bufferPointer, slashesCount);
335 | bufferPointer = 0;
336 | slashesCount = 0;
337 | break;
338 |
339 | // CR
340 | case 13:
341 | break;
342 |
343 | default:
344 | word += String.fromCharCode(code);
345 | break;
346 | }
347 | }
348 | this._finalizeParsing();
349 | if (this.logging.enabled) console.timeEnd("OBJLoader2Parser.execute");
350 | },
351 |
352 | /**
353 | * Parse the provided text
354 | *
355 | * @param {string} text OBJ data as string
356 | */
357 | executeLegacy: function(text) {
358 | if (this.logging.enabled) console.time("OBJLoader2Parser.executeLegacy");
359 | this._configure();
360 | this.legacyMode = true;
361 | this.contentRef = text;
362 | let length = text.length;
363 | this.globalCounts.totalBytes = length;
364 | let buffer = new Array(128);
365 |
366 | for (
367 | let char, word = "", bufferPointer = 0, slashesCount = 0, i = 0;
368 | i < length;
369 | i++
370 | ) {
371 | char = text[i];
372 | switch (char) {
373 | case " ":
374 | if (word.length > 0) buffer[bufferPointer++] = word;
375 | word = "";
376 | break;
377 |
378 | case "/":
379 | if (word.length > 0) buffer[bufferPointer++] = word;
380 | slashesCount++;
381 | word = "";
382 | break;
383 |
384 | case "\n":
385 | if (word.length > 0) buffer[bufferPointer++] = word;
386 | word = "";
387 | this.globalCounts.lineByte = this.globalCounts.currentByte;
388 | this.globalCounts.currentByte = i;
389 | this._processLine(buffer, bufferPointer, slashesCount);
390 | bufferPointer = 0;
391 | slashesCount = 0;
392 | break;
393 |
394 | case "\r":
395 | break;
396 |
397 | default:
398 | word += char;
399 | }
400 | }
401 | this._finalizeParsing();
402 | if (this.logging.enabled) console.timeEnd("OBJLoader2Parser.executeLegacy");
403 | },
404 |
405 | _processLine: function(buffer, bufferPointer, slashesCount) {
406 | if (bufferPointer < 1) return;
407 |
408 | let reconstructString = function(content, legacyMode, start, stop) {
409 | let line = "";
410 | if (stop > start) {
411 | let i;
412 | if (legacyMode) {
413 | for (i = start; i < stop; i++) line += content[i];
414 | } else {
415 | for (i = start; i < stop; i++)
416 | line += String.fromCharCode(content[i]);
417 | }
418 | line = line.trim();
419 | }
420 | return line;
421 | };
422 |
423 | let bufferLength, length, i, lineDesignation;
424 | lineDesignation = buffer[0];
425 | switch (lineDesignation) {
426 | case "v":
427 | this.vertices.push(parseFloat(buffer[1]));
428 | this.vertices.push(parseFloat(buffer[2]));
429 | this.vertices.push(parseFloat(buffer[3]));
430 | if (bufferPointer > 4) {
431 | this.colors.push(parseFloat(buffer[4]));
432 | this.colors.push(parseFloat(buffer[5]));
433 | this.colors.push(parseFloat(buffer[6]));
434 | }
435 | break;
436 |
437 | case "vt":
438 | this.uvs.push(parseFloat(buffer[1]));
439 | this.uvs.push(parseFloat(buffer[2]));
440 | break;
441 |
442 | case "vn":
443 | this.normals.push(parseFloat(buffer[1]));
444 | this.normals.push(parseFloat(buffer[2]));
445 | this.normals.push(parseFloat(buffer[3]));
446 | break;
447 |
448 | case "f":
449 | bufferLength = bufferPointer - 1;
450 |
451 | // "f vertex ..."
452 | if (slashesCount === 0) {
453 | this._checkFaceType(0);
454 | for (i = 2, length = bufferLength; i < length; i++) {
455 | this._buildFace(buffer[1]);
456 | this._buildFace(buffer[i]);
457 | this._buildFace(buffer[i + 1]);
458 | }
459 |
460 | // "f vertex/uv ..."
461 | } else if (bufferLength === slashesCount * 2) {
462 | this._checkFaceType(1);
463 | for (i = 3, length = bufferLength - 2; i < length; i += 2) {
464 | this._buildFace(buffer[1], buffer[2]);
465 | this._buildFace(buffer[i], buffer[i + 1]);
466 | this._buildFace(buffer[i + 2], buffer[i + 3]);
467 | }
468 |
469 | // "f vertex/uv/normal ..."
470 | } else if (bufferLength * 2 === slashesCount * 3) {
471 | this._checkFaceType(2);
472 | for (i = 4, length = bufferLength - 3; i < length; i += 3) {
473 | this._buildFace(buffer[1], buffer[2], buffer[3]);
474 | this._buildFace(buffer[i], buffer[i + 1], buffer[i + 2]);
475 | this._buildFace(buffer[i + 3], buffer[i + 4], buffer[i + 5]);
476 | }
477 |
478 | // "f vertex//normal ..."
479 | } else {
480 | this._checkFaceType(3);
481 | for (i = 3, length = bufferLength - 2; i < length; i += 2) {
482 | this._buildFace(buffer[1], undefined, buffer[2]);
483 | this._buildFace(buffer[i], undefined, buffer[i + 1]);
484 | this._buildFace(buffer[i + 2], undefined, buffer[i + 3]);
485 | }
486 | }
487 | break;
488 |
489 | case "l":
490 | case "p":
491 | bufferLength = bufferPointer - 1;
492 | if (bufferLength === slashesCount * 2) {
493 | this._checkFaceType(4);
494 | for (i = 1, length = bufferLength + 1; i < length; i += 2)
495 | this._buildFace(buffer[i], buffer[i + 1]);
496 | } else {
497 | this._checkFaceType(lineDesignation === "l" ? 5 : 6);
498 | for (i = 1, length = bufferLength + 1; i < length; i++)
499 | this._buildFace(buffer[i]);
500 | }
501 | break;
502 |
503 | case "s":
504 | this._pushSmoothingGroup(buffer[1]);
505 | break;
506 |
507 | case "g":
508 | // 'g' leads to creation of mesh if valid data (faces declaration was done before), otherwise only groupName gets set
509 | this._processCompletedMesh();
510 | this.rawMesh.groupName = reconstructString(
511 | this.contentRef,
512 | this.legacyMode,
513 | this.globalCounts.lineByte + 2,
514 | this.globalCounts.currentByte
515 | );
516 | break;
517 |
518 | case "o":
519 | // 'o' is meta-information and usually does not result in creation of new meshes, but can be enforced with "useOAsMesh"
520 | if (this.useOAsMesh) this._processCompletedMesh();
521 | this.rawMesh.objectName = reconstructString(
522 | this.contentRef,
523 | this.legacyMode,
524 | this.globalCounts.lineByte + 2,
525 | this.globalCounts.currentByte
526 | );
527 | break;
528 |
529 | case "mtllib":
530 | this.rawMesh.mtllibName = reconstructString(
531 | this.contentRef,
532 | this.legacyMode,
533 | this.globalCounts.lineByte + 7,
534 | this.globalCounts.currentByte
535 | );
536 | break;
537 |
538 | case "usemtl":
539 | let mtlName = reconstructString(
540 | this.contentRef,
541 | this.legacyMode,
542 | this.globalCounts.lineByte + 7,
543 | this.globalCounts.currentByte
544 | );
545 | if (mtlName !== "" && this.rawMesh.activeMtlName !== mtlName) {
546 | this.rawMesh.activeMtlName = mtlName;
547 | this.rawMesh.counts.mtlCount++;
548 | this._checkSubGroup();
549 | }
550 | break;
551 |
552 | default:
553 | break;
554 | }
555 | },
556 |
557 | _pushSmoothingGroup: function(smoothingGroup) {
558 | let smoothingGroupInt = parseInt(smoothingGroup);
559 | if (isNaN(smoothingGroupInt)) {
560 | smoothingGroupInt = smoothingGroup === "off" ? 0 : 1;
561 | }
562 |
563 | let smoothCheck = this.rawMesh.smoothingGroup.normalized;
564 | this.rawMesh.smoothingGroup.normalized = this.rawMesh.smoothingGroup
565 | .splitMaterials
566 | ? smoothingGroupInt
567 | : smoothingGroupInt === 0
568 | ? 0
569 | : 1;
570 | this.rawMesh.smoothingGroup.real = smoothingGroupInt;
571 |
572 | if (smoothCheck !== smoothingGroupInt) {
573 | this.rawMesh.counts.smoothingGroupCount++;
574 | this._checkSubGroup();
575 | }
576 | },
577 |
578 | /**
579 | * Expanded faceTypes include all four face types, both line types and the point type
580 | * faceType = 0: "f vertex ..."
581 | * faceType = 1: "f vertex/uv ..."
582 | * faceType = 2: "f vertex/uv/normal ..."
583 | * faceType = 3: "f vertex//normal ..."
584 | * faceType = 4: "l vertex/uv ..." or "l vertex ..."
585 | * faceType = 5: "l vertex ..."
586 | * faceType = 6: "p vertex ..."
587 | */
588 | _checkFaceType: function(faceType) {
589 | if (this.rawMesh.faceType !== faceType) {
590 | this._processCompletedMesh();
591 | this.rawMesh.faceType = faceType;
592 | this._checkSubGroup();
593 | }
594 | },
595 |
596 | _checkSubGroup: function() {
597 | let index =
598 | this.rawMesh.activeMtlName + "|" + this.rawMesh.smoothingGroup.normalized;
599 | this.rawMesh.subGroupInUse = this.rawMesh.subGroups[index];
600 |
601 | if (
602 | this.rawMesh.subGroupInUse === undefined ||
603 | this.rawMesh.subGroupInUse === null
604 | ) {
605 | this.rawMesh.subGroupInUse = {
606 | index: index,
607 | objectName: this.rawMesh.objectName,
608 | groupName: this.rawMesh.groupName,
609 | materialName: this.rawMesh.activeMtlName,
610 | smoothingGroup: this.rawMesh.smoothingGroup.normalized,
611 | vertices: [],
612 | indexMappingsCount: 0,
613 | indexMappings: [],
614 | indices: [],
615 | colors: [],
616 | uvs: [],
617 | normals: [],
618 | };
619 | this.rawMesh.subGroups[index] = this.rawMesh.subGroupInUse;
620 | }
621 | },
622 |
623 | _buildFace: function(faceIndexV, faceIndexU, faceIndexN) {
624 | let subGroupInUse = this.rawMesh.subGroupInUse;
625 | let scope = this;
626 | let updateSubGroupInUse = function() {
627 | let faceIndexVi = parseInt(faceIndexV);
628 | let indexPointerV =
629 | 3 *
630 | (faceIndexVi > 0
631 | ? faceIndexVi - 1
632 | : faceIndexVi + scope.vertices.length / 3);
633 | let indexPointerC = scope.colors.length > 0 ? indexPointerV : null;
634 |
635 | let vertices = subGroupInUse.vertices;
636 | vertices.push(scope.vertices[indexPointerV++]);
637 | vertices.push(scope.vertices[indexPointerV++]);
638 | vertices.push(scope.vertices[indexPointerV]);
639 |
640 | if (indexPointerC !== null) {
641 | let colors = subGroupInUse.colors;
642 | colors.push(scope.colors[indexPointerC++]);
643 | colors.push(scope.colors[indexPointerC++]);
644 | colors.push(scope.colors[indexPointerC]);
645 | }
646 | if (faceIndexU) {
647 | let faceIndexUi = parseInt(faceIndexU);
648 | let indexPointerU =
649 | 2 *
650 | (faceIndexUi > 0
651 | ? faceIndexUi - 1
652 | : faceIndexUi + scope.uvs.length / 2);
653 | let uvs = subGroupInUse.uvs;
654 | uvs.push(scope.uvs[indexPointerU++]);
655 | uvs.push(scope.uvs[indexPointerU]);
656 | }
657 | if (faceIndexN && !scope.disregardNormals) {
658 | let faceIndexNi = parseInt(faceIndexN);
659 | let indexPointerN =
660 | 3 *
661 | (faceIndexNi > 0
662 | ? faceIndexNi - 1
663 | : faceIndexNi + scope.normals.length / 3);
664 | let normals = subGroupInUse.normals;
665 | normals.push(scope.normals[indexPointerN++]);
666 | normals.push(scope.normals[indexPointerN++]);
667 | normals.push(scope.normals[indexPointerN]);
668 | }
669 | };
670 |
671 | if (this.useIndices) {
672 | if (this.disregardNormals) faceIndexN = undefined;
673 | let mappingName =
674 | faceIndexV +
675 | (faceIndexU ? "_" + faceIndexU : "_n") +
676 | (faceIndexN ? "_" + faceIndexN : "_n");
677 | let indicesPointer = subGroupInUse.indexMappings[mappingName];
678 | if (indicesPointer === undefined || indicesPointer === null) {
679 | indicesPointer = this.rawMesh.subGroupInUse.vertices.length / 3;
680 | updateSubGroupInUse();
681 | subGroupInUse.indexMappings[mappingName] = indicesPointer;
682 | subGroupInUse.indexMappingsCount++;
683 | } else {
684 | this.rawMesh.counts.doubleIndicesCount++;
685 | }
686 | subGroupInUse.indices.push(indicesPointer);
687 | } else {
688 | updateSubGroupInUse();
689 | }
690 | this.rawMesh.counts.faceCount++;
691 | },
692 |
693 | _createRawMeshReport: function(inputObjectCount) {
694 | return (
695 | "Input Object number: " +
696 | inputObjectCount +
697 | "\n\tObject name: " +
698 | this.rawMesh.objectName +
699 | "\n\tGroup name: " +
700 | this.rawMesh.groupName +
701 | "\n\tMtllib name: " +
702 | this.rawMesh.mtllibName +
703 | "\n\tVertex count: " +
704 | this.vertices.length / 3 +
705 | "\n\tNormal count: " +
706 | this.normals.length / 3 +
707 | "\n\tUV count: " +
708 | this.uvs.length / 2 +
709 | "\n\tSmoothingGroup count: " +
710 | this.rawMesh.counts.smoothingGroupCount +
711 | "\n\tMaterial count: " +
712 | this.rawMesh.counts.mtlCount +
713 | "\n\tReal MeshOutputGroup count: " +
714 | this.rawMesh.subGroups.length
715 | );
716 | },
717 |
718 | /**
719 | * Clear any empty subGroup and calculate absolute vertex, normal and uv counts
720 | */
721 | _finalizeRawMesh: function() {
722 | let meshOutputGroupTemp = [];
723 | let meshOutputGroup;
724 | let absoluteVertexCount = 0;
725 | let absoluteIndexMappingsCount = 0;
726 | let absoluteIndexCount = 0;
727 | let absoluteColorCount = 0;
728 | let absoluteNormalCount = 0;
729 | let absoluteUvCount = 0;
730 | let indices;
731 | for (let name in this.rawMesh.subGroups) {
732 | meshOutputGroup = this.rawMesh.subGroups[name];
733 | if (meshOutputGroup.vertices.length > 0) {
734 | indices = meshOutputGroup.indices;
735 | if (indices.length > 0 && absoluteIndexMappingsCount > 0) {
736 | for (let i = 0; i < indices.length; i++) {
737 | indices[i] = indices[i] + absoluteIndexMappingsCount;
738 | }
739 | }
740 | meshOutputGroupTemp.push(meshOutputGroup);
741 | absoluteVertexCount += meshOutputGroup.vertices.length;
742 | absoluteIndexMappingsCount += meshOutputGroup.indexMappingsCount;
743 | absoluteIndexCount += meshOutputGroup.indices.length;
744 | absoluteColorCount += meshOutputGroup.colors.length;
745 | absoluteUvCount += meshOutputGroup.uvs.length;
746 | absoluteNormalCount += meshOutputGroup.normals.length;
747 | }
748 | }
749 |
750 | // do not continue if no result
751 | let result = null;
752 | if (meshOutputGroupTemp.length > 0) {
753 | result = {
754 | name:
755 | this.rawMesh.groupName !== ""
756 | ? this.rawMesh.groupName
757 | : this.rawMesh.objectName,
758 | subGroups: meshOutputGroupTemp,
759 | absoluteVertexCount: absoluteVertexCount,
760 | absoluteIndexCount: absoluteIndexCount,
761 | absoluteColorCount: absoluteColorCount,
762 | absoluteNormalCount: absoluteNormalCount,
763 | absoluteUvCount: absoluteUvCount,
764 | faceCount: this.rawMesh.counts.faceCount,
765 | doubleIndicesCount: this.rawMesh.counts.doubleIndicesCount,
766 | };
767 | }
768 | return result;
769 | },
770 |
771 | _processCompletedMesh: function() {
772 | let result = this._finalizeRawMesh();
773 | let haveMesh = result !== null;
774 | if (haveMesh) {
775 | if (
776 | this.colors.length > 0 &&
777 | this.colors.length !== this.vertices.length
778 | ) {
779 | this.callbacks.onError(
780 | "Vertex Colors were detected, but vertex count and color count do not match!"
781 | );
782 | }
783 | if (this.logging.enabled && this.logging.debug)
784 | console.debug(this._createRawMeshReport(this.inputObjectCount));
785 | this.inputObjectCount++;
786 |
787 | this._buildMesh(result);
788 | let progressBytesPercent =
789 | this.globalCounts.currentByte / this.globalCounts.totalBytes;
790 | this._onProgress(
791 | "Completed [o: " +
792 | this.rawMesh.objectName +
793 | " g:" +
794 | this.rawMesh.groupName +
795 | "" +
796 | "] Total progress: " +
797 | (progressBytesPercent * 100).toFixed(2) +
798 | "%"
799 | );
800 | this._resetRawMesh();
801 | }
802 | return haveMesh;
803 | },
804 |
805 | /**
806 | * SubGroups are transformed to too intermediate format that is forwarded to the MeshReceiver.
807 | * It is ensured that SubGroups only contain objects with vertices (no need to check).
808 | *
809 | * @param result
810 | */
811 | _buildMesh: function(result) {
812 | let meshOutputGroups = result.subGroups;
813 |
814 | let vertexFA = new Float32Array(result.absoluteVertexCount);
815 | this.globalCounts.vertices += result.absoluteVertexCount / 3;
816 | this.globalCounts.faces += result.faceCount;
817 | this.globalCounts.doubleIndicesCount += result.doubleIndicesCount;
818 | let indexUA =
819 | result.absoluteIndexCount > 0
820 | ? new Uint32Array(result.absoluteIndexCount)
821 | : null;
822 | let colorFA =
823 | result.absoluteColorCount > 0
824 | ? new Float32Array(result.absoluteColorCount)
825 | : null;
826 | let normalFA =
827 | result.absoluteNormalCount > 0
828 | ? new Float32Array(result.absoluteNormalCount)
829 | : null;
830 | let uvFA =
831 | result.absoluteUvCount > 0
832 | ? new Float32Array(result.absoluteUvCount)
833 | : null;
834 | let haveVertexColors = colorFA !== null;
835 |
836 | let meshOutputGroup;
837 | let materialNames = [];
838 |
839 | let createMultiMaterial = meshOutputGroups.length > 1;
840 | let materialIndex = 0;
841 | let materialIndexMapping = [];
842 | let selectedMaterialIndex;
843 | let materialGroup;
844 | let materialGroups = [];
845 |
846 | let vertexFAOffset = 0;
847 | let indexUAOffset = 0;
848 | let colorFAOffset = 0;
849 | let normalFAOffset = 0;
850 | let uvFAOffset = 0;
851 | let materialGroupOffset = 0;
852 | let materialGroupLength = 0;
853 |
854 | let materialOrg, material, materialName, materialNameOrg;
855 | // only one specific face type
856 | for (let oodIndex in meshOutputGroups) {
857 | if (!meshOutputGroups.hasOwnProperty(oodIndex)) continue;
858 | meshOutputGroup = meshOutputGroups[oodIndex];
859 |
860 | materialNameOrg = meshOutputGroup.materialName;
861 | if (this.rawMesh.faceType < 4) {
862 | materialName =
863 | materialNameOrg +
864 | (haveVertexColors ? "_vertexColor" : "") +
865 | (meshOutputGroup.smoothingGroup === 0 ? "_flat" : "");
866 | } else {
867 | materialName =
868 | this.rawMesh.faceType === 6
869 | ? "defaultPointMaterial"
870 | : "defaultLineMaterial";
871 | }
872 | materialOrg = this.materials[materialNameOrg];
873 | material = this.materials[materialName];
874 |
875 | // both original and derived names do not lead to an existing material => need to use a default material
876 | if (
877 | (materialOrg === undefined || materialOrg === null) &&
878 | (material === undefined || material === null)
879 | ) {
880 | materialName = haveVertexColors
881 | ? "defaultVertexColorMaterial"
882 | : "defaultMaterial";
883 | material = this.materials[materialName];
884 | if (this.logging.enabled) {
885 | console.info(
886 | 'object_group "' +
887 | meshOutputGroup.objectName +
888 | "_" +
889 | meshOutputGroup.groupName +
890 | '" was defined with unresolvable material "' +
891 | materialNameOrg +
892 | '"! Assigning "' +
893 | materialName +
894 | '".'
895 | );
896 | }
897 | }
898 | if (material === undefined || material === null) {
899 | let materialCloneInstructions = {
900 | materialNameOrg: materialNameOrg,
901 | materialName: materialName,
902 | materialProperties: {
903 | vertexColors: haveVertexColors ? 2 : 0,
904 | flatShading: meshOutputGroup.smoothingGroup === 0,
905 | },
906 | };
907 | let payload = {
908 | cmd: "assetAvailable",
909 | type: "material",
910 | materials: {
911 | materialCloneInstructions: materialCloneInstructions,
912 | },
913 | };
914 | this.callbacks.onAssetAvailable(payload);
915 |
916 | // only set materials if they don't exist, yet
917 | let matCheck = this.materials[materialName];
918 | if (matCheck === undefined || matCheck === null) {
919 | this.materials[materialName] = materialCloneInstructions;
920 | }
921 | }
922 |
923 | if (createMultiMaterial) {
924 | // re-use material if already used before. Reduces materials array size and eliminates duplicates
925 | selectedMaterialIndex = materialIndexMapping[materialName];
926 | if (!selectedMaterialIndex) {
927 | selectedMaterialIndex = materialIndex;
928 | materialIndexMapping[materialName] = materialIndex;
929 | materialNames.push(materialName);
930 | materialIndex++;
931 | }
932 | materialGroupLength = this.useIndices
933 | ? meshOutputGroup.indices.length
934 | : meshOutputGroup.vertices.length / 3;
935 | materialGroup = {
936 | start: materialGroupOffset,
937 | count: materialGroupLength,
938 | index: selectedMaterialIndex,
939 | };
940 | materialGroups.push(materialGroup);
941 | materialGroupOffset += materialGroupLength;
942 | } else {
943 | materialNames.push(materialName);
944 | }
945 |
946 | vertexFA.set(meshOutputGroup.vertices, vertexFAOffset);
947 | vertexFAOffset += meshOutputGroup.vertices.length;
948 |
949 | if (indexUA) {
950 | indexUA.set(meshOutputGroup.indices, indexUAOffset);
951 | indexUAOffset += meshOutputGroup.indices.length;
952 | }
953 |
954 | if (colorFA) {
955 | colorFA.set(meshOutputGroup.colors, colorFAOffset);
956 | colorFAOffset += meshOutputGroup.colors.length;
957 | }
958 |
959 | if (normalFA) {
960 | normalFA.set(meshOutputGroup.normals, normalFAOffset);
961 | normalFAOffset += meshOutputGroup.normals.length;
962 | }
963 | if (uvFA) {
964 | uvFA.set(meshOutputGroup.uvs, uvFAOffset);
965 | uvFAOffset += meshOutputGroup.uvs.length;
966 | }
967 |
968 | if (this.logging.enabled && this.logging.debug) {
969 | let materialIndexLine =
970 | selectedMaterialIndex === undefined || selectedMaterialIndex === null
971 | ? ""
972 | : "\n\t\tmaterialIndex: " + selectedMaterialIndex;
973 | let createdReport =
974 | "\tOutput Object no.: " +
975 | this.outputObjectCount +
976 | "\n\t\tgroupName: " +
977 | meshOutputGroup.groupName +
978 | "\n\t\tIndex: " +
979 | meshOutputGroup.index +
980 | "\n\t\tfaceType: " +
981 | this.rawMesh.faceType +
982 | "\n\t\tmaterialName: " +
983 | meshOutputGroup.materialName +
984 | "\n\t\tsmoothingGroup: " +
985 | meshOutputGroup.smoothingGroup +
986 | materialIndexLine +
987 | "\n\t\tobjectName: " +
988 | meshOutputGroup.objectName +
989 | "\n\t\t#vertices: " +
990 | meshOutputGroup.vertices.length / 3 +
991 | "\n\t\t#indices: " +
992 | meshOutputGroup.indices.length +
993 | "\n\t\t#colors: " +
994 | meshOutputGroup.colors.length / 3 +
995 | "\n\t\t#uvs: " +
996 | meshOutputGroup.uvs.length / 2 +
997 | "\n\t\t#normals: " +
998 | meshOutputGroup.normals.length / 3;
999 | console.debug(createdReport);
1000 | }
1001 | }
1002 | this.outputObjectCount++;
1003 | this.callbacks.onAssetAvailable(
1004 | {
1005 | cmd: "assetAvailable",
1006 | type: "mesh",
1007 | progress: {
1008 | numericalValue:
1009 | this.globalCounts.currentByte / this.globalCounts.totalBytes,
1010 | },
1011 | params: {
1012 | meshName: result.name,
1013 | },
1014 | materials: {
1015 | multiMaterial: createMultiMaterial,
1016 | materialNames: materialNames,
1017 | materialGroups: materialGroups,
1018 | },
1019 | buffers: {
1020 | vertices: vertexFA,
1021 | indices: indexUA,
1022 | colors: colorFA,
1023 | normals: normalFA,
1024 | uvs: uvFA,
1025 | },
1026 | // 0: mesh, 1: line, 2: point
1027 | geometryType:
1028 | this.rawMesh.faceType < 4 ? 0 : this.rawMesh.faceType === 6 ? 2 : 1,
1029 | },
1030 | [vertexFA.buffer],
1031 | indexUA !== null ? [indexUA.buffer] : null,
1032 | colorFA !== null ? [colorFA.buffer] : null,
1033 | normalFA !== null ? [normalFA.buffer] : null,
1034 | uvFA !== null ? [uvFA.buffer] : null
1035 | );
1036 | },
1037 |
1038 | _finalizeParsing: function() {
1039 | if (this.logging.enabled)
1040 | console.info("Global output object count: " + this.outputObjectCount);
1041 | if (this._processCompletedMesh() && this.logging.enabled) {
1042 | let parserFinalReport =
1043 | "Overall counts: " +
1044 | "\n\tVertices: " +
1045 | this.globalCounts.vertices +
1046 | "\n\tFaces: " +
1047 | this.globalCounts.faces +
1048 | "\n\tMultiple definitions: " +
1049 | this.globalCounts.doubleIndicesCount;
1050 | console.info(parserFinalReport);
1051 | }
1052 | },
1053 | };
1054 |
1055 | export { OBJLoader2Parser };
1056 |
--------------------------------------------------------------------------------
/src/js/worker-actions.js:
--------------------------------------------------------------------------------
1 | // Actions sent by main thread to the web worker
2 | export const MainThreadAction = Object.freeze({
3 | INIT_WORKER_STATE: "initialize-worker-state",
4 | REQUEST_BITMAPS: "request-bitmaps",
5 | START_RENDER_LOOP: "start-render-loop",
6 | STOP_RENDER_LOOP: "stop-render-loop",
7 | });
8 |
9 | // Actions sent by the web worker to the main thread
10 | export const WorkerAction = Object.freeze({
11 | BITMAPS: "bitmaps",
12 | NOTIFY: "notify",
13 | TERMINATE_ME: "terminate-me",
14 | });
15 |
--------------------------------------------------------------------------------
/src/js/workers/bitmap-worker.js:
--------------------------------------------------------------------------------
1 | import {
2 | AmbientLight,
3 | AxesHelper,
4 | Color,
5 | CubeGeometry,
6 | DirectionalLight,
7 | GridHelper,
8 | Mesh,
9 | MeshLambertMaterial,
10 | PerspectiveCamera,
11 | Scene,
12 | WebGLRenderer,
13 | } from "three";
14 |
15 | import { MainThreadAction, WorkerAction } from "../worker-actions";
16 |
17 | const NAME = "bitmap-worker";
18 |
19 | // stop the demo after x renderers (just to test worker.terminate())
20 | const NUM_RENDER_FOR_DEMO = 300;
21 |
22 | // internal state of this web worker
23 | const state = {};
24 |
25 | /**
26 | * Initialize a Three.js scene in this web worker's state.
27 | */
28 | const initScene = ({ name = "Default Scene name", addAxesHelpers = true }) => {
29 | const scene = new Scene();
30 | scene.autoUpdate = true;
31 | const color = new Color(0x222222); // it's a dark gray
32 | scene.background = color;
33 | scene.fog = null;
34 | scene.name = name;
35 |
36 | const side = 30;
37 | const geometry = new CubeGeometry(side, side, side);
38 | const material = new MeshLambertMaterial({ color: 0xfbbc05 });
39 | const cube = new Mesh(geometry, material);
40 | cube.name = "Cube";
41 | cube.position.set(0, side / 2, 0);
42 | scene.add(cube);
43 |
44 | const gridHelper = new GridHelper(200, 16);
45 | gridHelper.name = "Floor GridHelper";
46 | scene.add(gridHelper);
47 |
48 | if (addAxesHelpers) {
49 | // XYZ axes helper (XYZ axes are RGB colors, respectively)
50 | const axesHelper = new AxesHelper(75);
51 | axesHelper.name = "XYZ AzesHelper";
52 | scene.add(axesHelper);
53 | }
54 |
55 | const dirLight = new DirectionalLight(0x4682b4, 1); // steelblue
56 | dirLight.position.set(120, 30, -200);
57 | dirLight.castShadow = true;
58 | dirLight.shadow.camera.near = 10;
59 | scene.add(dirLight);
60 |
61 | const ambientLight = new AmbientLight(0xffffff, 0.2);
62 | scene.add(ambientLight);
63 |
64 | state.scene = scene;
65 | postMessage({
66 | action: WorkerAction.NOTIFY,
67 | payload: { info: `scene '${name}' initialized` },
68 | source: NAME,
69 | });
70 | };
71 |
72 | const initRenderer = () => {
73 | if (!state.canvas) {
74 | throw new Error("Cannot initialize a renderer without a canvas");
75 | }
76 | const gl = state.canvas.getContext("webgl");
77 | state.renderer = new WebGLRenderer({
78 | antialias: true,
79 | canvas: state.canvas,
80 | });
81 |
82 | state.renderer.setClearColor(0x222222); // it's a dark gray
83 |
84 | // We are not in the DOM, so we don't have access to window.devicePixelRatio.
85 | // Maybe I could compute the aspect in the onscreen canvas and pass it here
86 | // within the message payload.
87 | state.renderer.setPixelRatio(1);
88 |
89 | // We are rendering offscreen, so there is no DOM and we cannot set the inline
90 | // style of the canvas.
91 | state.renderer.setSize(gl.drawingBufferWidth, gl.drawingBufferHeight, false);
92 |
93 | state.renderer.shadowMap.enabled = true;
94 |
95 | postMessage({
96 | action: WorkerAction.NOTIFY,
97 | payload: { info: "renderer initialized" },
98 | source: NAME,
99 | });
100 | };
101 |
102 | const initCamera = ({
103 | aspect = 1,
104 | far = 10000,
105 | fov,
106 | name = "Default camera name",
107 | near = 0.1,
108 | }) => {
109 | if (!state.scene) {
110 | throw new Error("Cannot initialize camera without a scene");
111 | }
112 | state.camera = new PerspectiveCamera(fov, aspect, near, far);
113 | state.camera.name = name;
114 | state.camera.position.set(100, 100, 100);
115 | state.camera.lookAt(state.scene.position);
116 |
117 | postMessage({
118 | action: WorkerAction.NOTIFY,
119 | payload: { info: `camera '${state.camera.name}' initialized` },
120 | source: NAME,
121 | });
122 | };
123 |
124 | const initState = payload => {
125 | const { height, sceneName, width } = payload;
126 | state.canvas = new OffscreenCanvas(width, height);
127 |
128 | initScene({ addAxesHelpers: false, sceneName });
129 |
130 | const cameraConfig = {
131 | aspect: state.canvas.width / state.canvas.height,
132 | fov: 75,
133 | name: "My Perspective Camera",
134 | };
135 | initCamera(cameraConfig);
136 |
137 | initRenderer();
138 |
139 | state.counter = 0;
140 | };
141 |
142 | const set2DRenderingContexts = resolutions => {
143 | const contexts = resolutions.map(r => {
144 | const canvas = new OffscreenCanvas(r.width, r.height);
145 | return canvas.getContext("2d");
146 | });
147 | state.contexts = contexts;
148 | };
149 |
150 | /**
151 | * Render the scene to the offscreen canvas, then create a bitmap and send it to
152 | * the main thread with a zero-copy operation.
153 | *
154 | * We call the transferToImageBitmap method on the offscreen canvas and send the
155 | * bitmap to the main thread synchronously.
156 | *
157 | * Note: requestAnimationFrame and cancelAnimationFrame are available for web
158 | * workers. But we can also use requestAnimationFrame in the main thread and
159 | * send a message to the web worker requesting a new bitmap when the main thread
160 | * needs it.
161 | */
162 | const render = resolutions => {
163 | // This web worker has some internal state. If these conditions are not
164 | // satisfied we crash and burn, so the main thread knows there is a problem.
165 | if (!state.renderer) {
166 | throw new Error(
167 | `Cannot call "render" without a renderer in ${NAME}'s state`
168 | );
169 | }
170 | if (!state.canvas) {
171 | throw new Error(`Cannot call "render" without a canvas in ${NAME}'s state`);
172 | }
173 | if (!state.camera) {
174 | throw new Error(`Cannot call "render" without a camera in ${NAME}'s state`);
175 | }
176 | if (!state.scene) {
177 | throw new Error(`Cannot call "render" without a scene in ${NAME}'s state`);
178 | }
179 |
180 | // signal to the main thread that this web worker is done, so this worker's
181 | // heap can be freed. You can see that this web worker's heap disappears in
182 | // the Chrome Dev Tools Memory tab.
183 | if (state.counter > NUM_RENDER_FOR_DEMO) {
184 | state.scene.dispose();
185 | postMessage({ action: WorkerAction.TERMINATE_ME, source: NAME });
186 | }
187 |
188 | // render the scene in a "source" OffscreenCanvas only once
189 | state.renderer.render(state.scene, state.camera);
190 |
191 | // copy the "source" canvas to N "destination" canvases, one for each
192 | // requested bitmap
193 | const bitmaps = state.contexts.map((ctx, i) => {
194 | ctx.globalCompositeOperation = "copy";
195 | // ctx.fillStyle = "rgb(200,0,0)";
196 | // ctx.fillRect(10, 10, 55, 50);
197 | const { width, height } = resolutions[i];
198 | // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage
199 | ctx.drawImage(state.canvas, 0, 0, width, height);
200 | return ctx.canvas.transferToImageBitmap();
201 | });
202 |
203 | // in other use-cases the web worker could send back a `Blob` with
204 | // `canvas.convertToBlob().then(blob => postMessage({ blob }, [blob]))`
205 |
206 | const message = {
207 | action: WorkerAction.BITMAPS,
208 | payload: { bitmaps },
209 | source: NAME,
210 | };
211 |
212 | // ImageBitmap implements the Transerable interface, so we can send it to the
213 | // main thread WITHOUT using the structured clone algorithm. In other words,
214 | // this `postMessage` is a ZERO-COPY operation: we are passing each bitmap BY
215 | // REFERENCE, NOT BY VALUE.
216 | // https://developer.mozilla.org/en-US/docs/Web/API/Transferable
217 | // https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
218 | postMessage(message, bitmaps);
219 | };
220 |
221 | const style = "color: red; font-weight: normal";
222 |
223 | const onMessage = event => {
224 | const label = `[${event.data.source} --> ${NAME}] - ${event.data.action}`;
225 | // console.groupCollapsed(`%c${label}`, style);
226 | // if (event.data.payload) {
227 | // console.log(`payload: ${JSON.stringify(event.data.payload)}`);
228 | // // console.table(event.data);
229 | // }
230 | // console.groupEnd();
231 | console.log(`%c${label}`, style);
232 |
233 | switch (event.data.action) {
234 | case MainThreadAction.INIT_WORKER_STATE:
235 | console.log("SELF NAME (DedicatedWorkerGlobalScope name)", self.name);
236 | initState(event.data.payload);
237 | break;
238 | case MainThreadAction.REQUEST_BITMAPS: {
239 | state.counter++;
240 | state.camera.rotateZ(0.2);
241 | set2DRenderingContexts(event.data.payload.resolutions);
242 | render(event.data.payload.resolutions);
243 | break;
244 | }
245 | default: {
246 | console.warn(`${NAME} received a message that does not handle`, event);
247 | }
248 | }
249 | };
250 | onmessage = onMessage;
251 |
--------------------------------------------------------------------------------
/src/js/workers/transfer-worker.js:
--------------------------------------------------------------------------------
1 | import {
2 | AmbientLight,
3 | AxesHelper,
4 | CanvasTexture,
5 | Color,
6 | CubeGeometry,
7 | DirectionalLight,
8 | GridHelper,
9 | ImageBitmapLoader,
10 | LoadingManager,
11 | Mesh,
12 | MeshBasicMaterial,
13 | MeshLambertMaterial,
14 | ObjectLoader,
15 | PerspectiveCamera,
16 | Scene,
17 | WebGLRenderer,
18 | } from "three";
19 |
20 | import { MainThreadAction, WorkerAction } from "../worker-actions";
21 | import { OBJLoader2 } from "../vendor/OBJLoader2";
22 | import * as relativeURL from "../../models/male02.obj";
23 |
24 | // https://bwasty.github.io/gltf-loader-ts/index.html
25 | // https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/
26 | // https://github.com/mrdoob/three.js/blob/master/examples/webgl_loader_gltf.html
27 | import { GltfLoader } from "gltf-loader-ts";
28 |
29 | const NAME = "transfer-worker";
30 |
31 | const gltfLoader = new GltfLoader();
32 |
33 | const makeScene = name => {
34 | const scene = new Scene();
35 | scene.autoUpdate = true;
36 | const color = new Color(0x222222); // it's a dark gray
37 | scene.background = color;
38 | scene.fog = null;
39 | // Any Three.js object in the scene (and the scene itself) can have a name.
40 | scene.name = name;
41 |
42 | const side = 10;
43 | const geometry = new CubeGeometry(side, side, side);
44 | const material = new MeshLambertMaterial({ color: 0xfbbc05 });
45 | const cube = new Mesh(geometry, material);
46 | cube.name = "Cube";
47 | cube.position.set(0, side / 2, 0);
48 | scene.add(cube);
49 |
50 | const gridHelper = new GridHelper(200, 16);
51 | gridHelper.name = "Floor GridHelper";
52 | scene.add(gridHelper);
53 |
54 | // XYZ axes helper (XYZ axes are RGB colors, respectively)
55 | const axesHelper = new AxesHelper(75);
56 | axesHelper.name = "XYZ AzesHelper";
57 | scene.add(axesHelper);
58 |
59 | const dirLight = new DirectionalLight(0x4682b4, 1); // steelblue
60 | dirLight.position.set(120, 30, -200);
61 | dirLight.castShadow = true;
62 | dirLight.shadow.camera.near = 10;
63 | scene.add(dirLight);
64 |
65 | const ambientLight = new AmbientLight(0xffffff, 0.2);
66 | scene.add(ambientLight);
67 |
68 | return scene;
69 | };
70 |
71 | const makeRenderer = canvas => {
72 | const gl = canvas.getContext("webgl");
73 | const renderer = new WebGLRenderer({
74 | antialias: true,
75 | canvas,
76 | });
77 |
78 | renderer.setClearColor(0x222222); // it's a dark gray
79 |
80 | // We are not in the DOM, so we don't have access to window.devicePixelRatio.
81 | // Maybe I could compute the aspect in the onscreen canvas and pass it here
82 | // within the message payload.
83 | renderer.setPixelRatio(1);
84 |
85 | // We are rendering offscreen, so there is no DOM and we cannot set the inline
86 | // style of the canvas.
87 | const updateStyle = false;
88 | renderer.setSize(gl.drawingBufferWidth, gl.drawingBufferHeight, updateStyle);
89 | renderer.shadowMap.enabled = true;
90 | return renderer;
91 | };
92 |
93 | const makeCamera = (canvas, scene) => {
94 | const fov = 75;
95 | // There are no clientWith and clientHeight in an OffscreenCanvas
96 | // const { clientWidth, clientHeight } = canvas;
97 | // const aspect = clientWidth / clientHeight;
98 | const aspect = canvas.width / canvas.height;
99 | const near = 0.1;
100 | const far = 10000;
101 | const camera = new PerspectiveCamera(fov, aspect, near, far);
102 | camera.name = "my-camera";
103 | // camera.position.set(100, 100, 100);
104 | // Axes Helpers: RED (X), GREEN (Y), BLUE (Z)
105 | camera.position.set(100, 50, 200);
106 | camera.lookAt(scene.position);
107 |
108 | return camera;
109 | };
110 |
111 | const tryGltf = uri => {
112 | return new Promise((resolve, reject) => {
113 | return gltfLoader
114 | .load(uri)
115 | .then(asset => {
116 | // console.log("tryGltf -> asset", asset);
117 | const gltf = asset.gltf;
118 | // console.log("GLTF", gltf);
119 | resolve(gltf);
120 | })
121 | .catch(err => {
122 | reject(err);
123 | });
124 | });
125 | };
126 |
127 | // async function tryGltf(uri) {
128 | // const asset = await gltfLoader.load(uri);
129 | // console.log("tryGltf -> asset", asset);
130 | // let gltf = asset.gltf;
131 | // console.log("GLTF", gltf);
132 | // // let data = await asset.accessorData(0); // fetches BoxTextured0.bin
133 | // // let image: Image = await asset.imageData.get(0); // fetches CesiumLogoFlat.png
134 | // }
135 |
136 | const init = payload => {
137 | const { canvas, sceneName } = payload;
138 |
139 | postMessage({
140 | action: WorkerAction.NOTIFY,
141 | payload: { info: `[${NAME}] - scene initialized` },
142 | source: NAME,
143 | });
144 | const scene = makeScene(sceneName);
145 |
146 | postMessage({
147 | action: WorkerAction.NOTIFY,
148 | payload: { info: `[${NAME}] - renderer inizialized` },
149 | source: NAME,
150 | });
151 | const renderer = makeRenderer(canvas);
152 |
153 | postMessage({
154 | action: WorkerAction.NOTIFY,
155 | payload: { info: `[${NAME}] - camera inizialized` },
156 | source: NAME,
157 | });
158 | const camera = makeCamera(canvas, scene);
159 |
160 | const onManagerLoad = () => {
161 | postMessage({
162 | action: WorkerAction.NOTIFY,
163 | payload: {
164 | info: `[${NAME}] - Loaded all items`,
165 | },
166 | source: NAME,
167 | });
168 | };
169 |
170 | const onManagerProgress = (item, loaded, total) => {
171 | // console.log("LoadingManager progress:", item, loaded, total);
172 | postMessage({
173 | action: WorkerAction.NOTIFY,
174 | payload: {
175 | info: `[${NAME}] - Loaded ${loaded} of ${total} items`,
176 | },
177 | source: NAME,
178 | });
179 | };
180 |
181 | const onManagerStart = (url, itemsLoaded, itemsTotal) => {
182 | postMessage({
183 | action: WorkerAction.NOTIFY,
184 | payload: {
185 | info: `[${NAME}] - started loading file ${url} (Loaded ${itemsLoaded} of ${itemsTotal} items)`,
186 | },
187 | source: NAME,
188 | });
189 | };
190 |
191 | const onManagerError = error => {
192 | console.error("ERROR IN LOADING MANAGER", error);
193 | };
194 |
195 | const manager = new LoadingManager(
196 | onManagerLoad,
197 | onManagerProgress,
198 | onManagerError
199 | );
200 | manager.onStart = onManagerStart;
201 |
202 | const objectLoader = new ObjectLoader(manager);
203 | const bitmapLoader = new ImageBitmapLoader(manager);
204 | bitmapLoader.setOptions({ imageOrientation: "flipY" });
205 |
206 | const objLoader = new OBJLoader2(manager);
207 |
208 | const objURL = PUBLIC_URL.includes("github.io")
209 | ? `${PUBLIC_URL}/${relativeURL}`
210 | : `http://localhost:8080/${relativeURL}`;
211 |
212 | console.log(
213 | "=== PUBLIC_URL ===",
214 | PUBLIC_URL,
215 | "relativeURL",
216 | relativeURL,
217 | "objURL (3D model)",
218 | objURL
219 | );
220 |
221 | objLoader.load(objURL, object3D => {
222 | object3D.name = "male02";
223 | scene.add(object3D);
224 | });
225 |
226 | const onObjectLoad = object3D => {
227 | // console.log("=== object3D ===", object3D);
228 | const name = object3D.name || "unnamed-object";
229 | const info = `[${NAME}] - Loaded ${name} (geometry: ${object3D.geometry.type}, material: ${object3D.material.type})`;
230 | postMessage({
231 | action: WorkerAction.NOTIFY,
232 | payload: {
233 | info,
234 | },
235 | source: NAME,
236 | });
237 | // object3D.traverse(function(child) {
238 | // console.log("child", child);
239 | // if (child.isMesh) {
240 | // child.material.map = texture;
241 | // }
242 | // });
243 | object3D.position.set(50, 0, 50);
244 | object3D.scale.set(15, 15, 15);
245 | state.scene.add(object3D);
246 | };
247 |
248 | const onProgress = xhr => {
249 | if (xhr.lengthComputable) {
250 | const percentComplete = Math.round((xhr.loaded / xhr.total) * 100);
251 | postMessage({
252 | action: WorkerAction.NOTIFY,
253 | payload: {
254 | info: `[${NAME}] - downloading model (${percentComplete}%)`,
255 | },
256 | source: NAME,
257 | });
258 | }
259 | };
260 |
261 | const onError = error => {
262 | console.error("ERROR IN LOADER", error);
263 | };
264 |
265 | const url =
266 | "https://raw.githubusercontent.com/mrdoob/three.js/master/examples/models/json/teapot-claraio.json";
267 | objectLoader.load(url, onObjectLoad, onProgress, onError);
268 |
269 | // This one fails because it tries to access `window`, which of course is not
270 | // available in a web worker.
271 | // const url2 =
272 | // "https://raw.githubusercontent.com/mrdoob/three.js/master/examples/models/json/multimaterial.json";
273 | // objectLoader.load(url2, onLoad, onProgress, onError);
274 |
275 | const onBitmapLoad = bitmap => {
276 | const info = `[${NAME}] - Loaded bitmap (${bitmap.width}x${bitmap.height})`;
277 | postMessage({
278 | action: WorkerAction.NOTIFY,
279 | payload: {
280 | info,
281 | },
282 | source: NAME,
283 | });
284 | const texture = new CanvasTexture(bitmap);
285 | const materialWithTexture = new MeshBasicMaterial({ map: texture });
286 |
287 | const geom = new CubeGeometry(30, 30, 30);
288 | const cube = new Mesh(geom, materialWithTexture);
289 | cube.name = "Cube with textured material";
290 | cube.position.set(-50, 50, 50);
291 | scene.add(cube);
292 | };
293 |
294 | bitmapLoader.load(
295 | "https://raw.githubusercontent.com/mrdoob/three.js/master/examples/textures/planets/earth_atmos_2048.jpg",
296 | onBitmapLoad,
297 | // onProgress callback currently not supported
298 | undefined,
299 | onError
300 | );
301 |
302 | tryGltf(
303 | "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/BoxTextured/glTF/BoxTextured.gltf"
304 | )
305 | .then(glft => {
306 | console.log("glft", glft);
307 | // scene.add( gltf.scene );
308 |
309 | // gltf.animations; // Array
310 | // gltf.scene; // THREE.Group
311 | // gltf.scenes; // Array
312 | // gltf.cameras; // Array
313 | // gltf.asset; // Object
314 | })
315 | .catch(err => {
316 | console.log("err with gltf", err);
317 | });
318 |
319 | state.camera = camera;
320 | state.canvas = canvas;
321 | state.error = undefined;
322 | state.renderer = renderer;
323 | state.reqId = performance.now();
324 | state.scene = scene;
325 | };
326 |
327 | /**
328 | * Render the scene to the offscreen canvas.
329 | *
330 | * The scene rendered in the offscreen canvas appears automatically on the
331 | * onscreen canvas because the main thread transferred the ownership of the
332 | * rendering context to the offscreen canvas managed by this web worker.
333 | * The onscreen canvas is updated automatically and asynchronously.
334 | */
335 | const render = tick => {
336 | // We could use the tick to control the animation.
337 | if (!state.renderer) {
338 | state.error = new Error(
339 | `Cannot call "render" without a renderer in ${NAME}'s state`
340 | );
341 | return;
342 | }
343 | if (!state.canvas) {
344 | state.error = new Error(
345 | `Cannot call "render" without a canvas in ${NAME}'s state`
346 | );
347 | return;
348 | }
349 | if (!state.camera) {
350 | state.error = new Error(
351 | `Cannot call "render" without a camera in ${NAME}'s state`
352 | );
353 | return;
354 | }
355 | if (!state.scene) {
356 | state.error = new Error(
357 | `Cannot call "render" without a scene in ${NAME}'s state`
358 | );
359 | return;
360 | }
361 |
362 | // If we made it here, the web worker can safely render the scene.
363 | const male02 = state.scene.getObjectByName("male02");
364 | // console.log("Math.sin(tick)", Math.sin(tick));
365 | male02.rotateY(0.03);
366 | // male02.rotateZ(0.05);
367 | state.renderer.render(state.scene, state.camera);
368 |
369 | // Maybe the main thread is interested in knowing what this web worker is
370 | // doing, so we notify it about what has been done. Please note that the main
371 | // thread doesn't have to do anything, and CANNOT do anything on the canvas on
372 | // the screen, because it transferred the ownership of that canvas to the web
373 | // worker. So basically the visible, onscreen canvas is just a proxy for the
374 | // offscreen canvas.
375 | postMessage({
376 | action: WorkerAction.NOTIFY,
377 | payload: { info: `[${NAME}] - render loop` },
378 | source: NAME,
379 | });
380 | };
381 |
382 | let state = {
383 | camera: undefined,
384 | canvas: undefined,
385 | error: undefined,
386 | renderer: undefined,
387 | reqId: undefined,
388 | scene: undefined,
389 | };
390 |
391 | const style = "color: red; font-weight: normal";
392 |
393 | const onMessage = event => {
394 | // console.log(`%c${event.data.action}`, "color: red");
395 | const text = `[${event.data.source} --> ${NAME}] - ${event.data.action}`;
396 | console.log(`%c${text}`, style);
397 |
398 | switch (event.data.action) {
399 | case MainThreadAction.INIT_WORKER_STATE: {
400 | console.log("SELF NAME (DedicatedWorkerGlobalScope name)", self.name);
401 | init(event.data.payload);
402 | break;
403 | }
404 | case MainThreadAction.START_RENDER_LOOP: {
405 | renderLoop();
406 | break;
407 | }
408 | case MainThreadAction.STOP_RENDER_LOOP: {
409 | cancelAnimationFrame(state.reqId);
410 | break;
411 | }
412 | default: {
413 | console.warn(`${NAME} received a message that does not handle`, event);
414 | }
415 | }
416 | };
417 | onmessage = onMessage;
418 |
419 | const renderLoop = tick => {
420 | // state.camera.rotateZ(0.05);
421 | render(tick);
422 | if (state.error) {
423 | postMessage({
424 | action: WorkerAction.NOTIFY,
425 | payload: {
426 | info: `[${NAME}] - error: ${state.error.message}. Please terminate me.`,
427 | },
428 | source: NAME,
429 | });
430 | cancelAnimationFrame(state.reqId);
431 | postMessage({ action: WorkerAction.TERMINATE_ME, source: NAME });
432 | } else {
433 | state.reqId = requestAnimationFrame(renderLoop);
434 | }
435 | };
436 |
--------------------------------------------------------------------------------
/src/models/README.md:
--------------------------------------------------------------------------------
1 | # Models
2 |
3 | This directory contains 3D models in various formats.
4 |
5 | Each 3D model is emitted as a file in the output director, and can be required in JS, thanks to Webpack [file-loader](https://webpack.js.org/loaders/file-loader/).
6 |
7 | Here is the list of 3D models used in the application:
8 |
9 | - [male02](https://github.com/mrdoob/three.js/tree/master/examples/models/obj/male02/male02.obj)
10 |
--------------------------------------------------------------------------------
/src/textures/checkerboard.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackdbd/threejs-es6-webpack-starter/e4b00bbbfdb93016fbd486ec4a4b3456587b61fd/src/textures/checkerboard.jpg
--------------------------------------------------------------------------------
/src/textures/star.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jackdbd/threejs-es6-webpack-starter/e4b00bbbfdb93016fbd486ec4a4b3456587b61fd/src/textures/star.png
--------------------------------------------------------------------------------