Needle Engine

Changes between version 3.32.21-alpha and 3.32.22-alpha
Files changed (9) hide show
  1. plugins/vite/index.js +2 -0
  2. src/engine-components/ui/Graphic.ts +9 -2
  3. src/engine-components/ui/Image.ts +3 -2
  4. src/engine/extensions/NEEDLE_progressive.ts +59 -41
  5. plugins/types/needleConfig.d.ts +1 -0
  6. src/engine-components-experimental/networking/PlayerSync.ts +1 -1
  7. src/engine-components/SpriteRenderer.ts +28 -7
  8. plugins/types/userconfig.d.ts +3 -0
  9. plugins/vite/build-pipeline.js +97 -0
plugins/vite/index.js CHANGED
@@ -43,6 +43,7 @@
43
43
  import { vite_4_4_hack } from "./vite-4.4-hack.js";
44
44
  import { needleImportsLogger } from "./imports-logger.js";
45
45
  import { needleBuildInfo } from "./buildinfo.js";
46
+ import { needleBuildPipeline } from "./build-pipeline.js";
46
47
 
47
48
 
48
49
  export * from "./gzip.js";
@@ -79,6 +80,7 @@
79
80
  vite_4_4_hack(command, config, userSettings),
80
81
  needleFacebookInstantGames(command, config, userSettings),
81
82
  needleImportsLogger(command, config, userSettings),
83
+ needleBuildPipeline(command, config, userSettings),
82
84
  ];
83
85
  array.push(await editorConnection(command, config, userSettings, array));
84
86
  return array;
src/engine-components/ui/Graphic.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import { Color, LinearSRGBColorSpace, Object3D, SRGBColorSpace, Texture } from 'three';
2
2
  import * as ThreeMeshUI from 'three-mesh-ui'
3
+ import type { Options } from 'three-mesh-ui/build/types/core/elements/MeshUIBaseElement.js';
3
4
  import SimpleStateBehavior from "three-mesh-ui/examples/behaviors/states/SimpleStateBehavior.js"
4
5
 
5
6
  import { serializable } from '../../engine/engine_serialization_decorator.js';
7
+ import { NEEDLE_progressive } from '../../engine/extensions/NEEDLE_progressive.js';
6
8
  import { GameObject } from '../Component.js';
7
9
  import { RGBAColor } from "../js-extensions/RGBAColor.js"
8
10
  import { BaseUIComponent } from "./BaseUIComponent.js";
@@ -117,7 +119,7 @@
117
119
  }
118
120
  }
119
121
 
120
- setOptions(opts: object) {
122
+ setOptions(opts: Options) {
121
123
  this.makePanel();
122
124
  if (this.uiObject) {
123
125
  //@ts-ignore
@@ -218,9 +220,14 @@
218
220
  }
219
221
  // }
220
222
  this.setOptions({ backgroundImage: tex, borderRadius: 0, backgroundOpacity: this.color.alpha, backgroundSize: "stretch" });
223
+ NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId, tex, 0).then(res => {
224
+ if (res instanceof Texture) {
225
+ this.setOptions({ backgroundImage: res });
226
+ }
227
+ });
221
228
  }
222
229
  else {
223
- this.setOptions({ backgroundImage: null, borderRadius: 0, backgroundOpacity: this.color.alpha });
230
+ this.setOptions({ backgroundImage: undefined, borderRadius: 0, backgroundOpacity: this.color.alpha });
224
231
  }
225
232
  this.markDirty();
226
233
  }
src/engine-components/ui/Image.ts CHANGED
@@ -40,7 +40,8 @@
40
40
  private pixelsPerUnitMultiplier: number = 1;
41
41
 
42
42
  private isBuiltinSprite() {
43
- switch (this.sprite?.texture?.name) {
43
+ const sprite = this.sprite;
44
+ switch (sprite?.texture?.name) {
44
45
  case "InputFieldBackground":
45
46
  case "UISprite":
46
47
  case "Background":
@@ -49,7 +50,7 @@
49
50
  }
50
51
  // this is a hack/workaround for production builds where the name of the sprite is missing
51
52
  // need to remove this!!!!
52
- if (!this.sprite?.texture?.name?.length && this.sprite?.texture?.image?.width === 32 && this.sprite?.texture?.image?.height === 32)
53
+ if (!sprite?.texture?.name?.length && sprite?.texture?.image?.width === 32 && sprite?.texture?.image?.height === 32)
53
54
  return true;
54
55
  return false;
55
56
  }
src/engine/extensions/NEEDLE_progressive.ts CHANGED
@@ -22,6 +22,7 @@
22
22
  if (debug) {
23
23
  window.addEventListener("keyup", evt => {
24
24
  if (evt.key === "p") {
25
+ console.log("Toggle progressive textures", debug_toggle_maps);
25
26
  debug_toggle_maps.forEach((map, material) => {
26
27
  Object.entries(map).forEach(([key, value]) => {
27
28
  if (show_lod0) {
@@ -43,51 +44,68 @@
43
44
  return EXTENSION_NAME;
44
45
  }
45
46
 
46
- static assignTextureLOD(context: Context, source: SourceIdentifier | undefined, material: Material, level: number = 0) : Promise<any> {
47
- if (!material) return Promise.resolve(null);
47
+ /** Load a different resolution of a texture (if available)
48
+ * @param context the context
49
+ * @param source the sourceid of the file from which the texture is loaded (this is usually the component's sourceId)
50
+ * @param materialOrTexture the material or texture to load the LOD for (if passing in a material all textures in the material will be loaded)
51
+ * @param level the level of detail to load (0 is the highest resolution) - currently only 0 is supported
52
+ */
53
+ static assignTextureLOD(context: Context, source: SourceIdentifier | undefined, materialOrTexture: Material | Texture, level: number = 0): Promise<any> {
54
+ if (!materialOrTexture) return Promise.resolve(null);
48
55
 
49
- const promises: Array<Promise<Texture | null>> = [];
56
+ if (materialOrTexture instanceof Material) {
57
+ const material = materialOrTexture;
58
+ const promises: Array<Promise<Texture | null>> = [];
50
59
 
51
-
52
- if (material instanceof RawShaderMaterial) {
53
- // iterate uniforms of custom shaders
54
- for (const slot of Object.keys(material.uniforms)) {
55
- const val = material.uniforms[slot].value;
56
- if (val instanceof Texture) {
57
- const task = this.assignTextureLODForSlot(context, source, material, level, slot, val);
58
- promises.push(task);
60
+ if (material instanceof RawShaderMaterial) {
61
+ // iterate uniforms of custom shaders
62
+ for (const slot of Object.keys(material.uniforms)) {
63
+ const val = material.uniforms[slot].value;
64
+ if (val instanceof Texture) {
65
+ const task = this.assignTextureLODForSlot(context, source, val, level, material, slot);
66
+ promises.push(task);
67
+ }
59
68
  }
60
69
  }
61
- }
62
- else {
63
- for (const slot of Object.keys(material)) {
64
- const val = material[slot];
65
- if (val instanceof Texture) {
66
- const task = this.assignTextureLODForSlot(context, source, material, level, slot, val);
67
- promises.push(task);
70
+ else {
71
+ for (const slot of Object.keys(material)) {
72
+ const val = material[slot];
73
+ if (val instanceof Texture) {
74
+ const task = this.assignTextureLODForSlot(context, source, val, level, material, slot);
75
+ promises.push(task);
76
+ }
68
77
  }
69
78
  }
79
+ return PromiseAllWithErrors(promises);
70
80
  }
71
81
 
72
- return PromiseAllWithErrors(promises);
82
+ if (materialOrTexture instanceof Texture) {
83
+ const texture = materialOrTexture;
84
+ return this.assignTextureLODForSlot(context, source, texture, level, null, null);
85
+ }
86
+
87
+ return Promise.resolve(null);
73
88
  }
74
89
 
75
- private static assignTextureLODForSlot(context: Context, source: SourceIdentifier | undefined, material: Material, level: number, slot: string, val: any): Promise<Texture | null> {
90
+ private static assignTextureLODForSlot(context: Context, source: SourceIdentifier | undefined, val: any, level: number, material: Material | null, slot: string | null): Promise<Texture | null> {
76
91
  if (val?.isTexture !== true) return Promise.resolve(null);
77
92
 
78
- if (debug) console.log("-----------\n", "FIND", material.name, slot, val?.name, val?.userData, val, material);
93
+ if (debug) console.log("-----------\n", "FIND", material?.name, slot, val?.name, val?.userData, val, material);
79
94
 
80
- return NEEDLE_progressive.getOrLoadTexture(context, source, material, slot, val, level).then(t => {
95
+ return NEEDLE_progressive.getOrLoadTexture(context, source, val, level, material, slot).then(t => {
81
96
 
82
97
  if (t?.isTexture === true) {
83
98
 
84
- if (debug) console.warn("Assign LOD", material.name, slot, t.name, t["guid"], material, "Prev:", val, "Now:", t, "\n--------------");
99
+ if (debug) console.warn("Assign LOD", material?.name, slot, t.name, t["guid"], material, "Prev:", val, "Now:", t, "\n--------------");
85
100
 
86
- material[slot] = t;
87
101
  t.needsUpdate = true;
88
- material.needsUpdate = true;
89
102
 
90
- if (debug) {
103
+ if (material && slot) {
104
+ material[slot] = t;
105
+ material.needsUpdate = true;
106
+ }
107
+
108
+ if (debug && material && slot) {
91
109
  let debug_map = debug_toggle_maps.get(material);
92
110
  if (!debug_map) {
93
111
  debug_map = {};
@@ -146,16 +164,16 @@
146
164
  private static resolved: { [key: string]: Texture } = {};
147
165
  private static currentlyLoading: { [key: string]: Promise<Texture | null> } = {};
148
166
 
149
- private static async getOrLoadTexture(context: Context, source: SourceIdentifier | undefined, material: Material, slot: string, current: Texture, _level: number): Promise<Texture | null> {
167
+ private static async getOrLoadTexture(context: Context, source: SourceIdentifier | undefined, current: Texture, _level: number, material: Material | null, slot: string | null): Promise<Texture | null> {
150
168
 
151
169
  const key = current.uuid;
152
170
 
153
171
  let progressiveInfo: ProgressiveTextureSchema | undefined;
154
172
 
155
173
  // See https://github.com/needle-tools/needle-engine-support/issues/133
156
- if(current.source && current.source[$progressiveTextureExtension])
174
+ if (current.source && current.source[$progressiveTextureExtension])
157
175
  progressiveInfo = current.source[$progressiveTextureExtension];
158
- if(!progressiveInfo) progressiveInfo = NEEDLE_progressive.cache.get(key);
176
+ if (!progressiveInfo) progressiveInfo = NEEDLE_progressive.cache.get(key);
159
177
 
160
178
  if (progressiveInfo) {
161
179
  if (debug)
@@ -171,12 +189,12 @@
171
189
  let res = this.resolved[resolveKey];
172
190
  // check if the texture has been disposed or not
173
191
  if (res.image?.data || res.source?.data) {
174
- if (debug) console.log("Texture has already been loaded: " + resolveKey, material.name, slot, current.name, res);
192
+ if (debug) console.log("Texture has already been loaded: " + resolveKey, material?.name, slot, current.name, res);
175
193
  res = this.copySettings(current, res);
176
194
  return res;
177
195
  }
178
196
  else if (res) {
179
- if(debug) console.warn("Texture has been disposed, will load again: " + resolveKey, material.name, slot, current.name, res.source.data);
197
+ if (debug) console.warn("Texture has been disposed, will load again: " + resolveKey, material?.name, slot, current.name, res.source.data);
180
198
  }
181
199
  }
182
200
 
@@ -184,7 +202,7 @@
184
202
  try {
185
203
  if (this.currentlyLoading[resolveKey] !== undefined) {
186
204
  if (debug)
187
- console.log("Already loading:", material.name + "." + slot, resolveKey);
205
+ console.log("Already loading:", material?.name + "." + slot, resolveKey);
188
206
  let res = await this.currentlyLoading[resolveKey];
189
207
  if (res)
190
208
  res = this.copySettings(current, res);
@@ -196,19 +214,19 @@
196
214
  addDracoAndKTX2Loaders(loader, context);
197
215
 
198
216
 
199
- if (debug) console.warn("Start loading " + uri, material.name, slot, ext.guid);
217
+ if (debug) console.warn("Start loading " + uri, material?.name, slot, ext.guid);
200
218
  if (debug) {
201
219
  await delay(Math.random() * 1000);
202
220
  }
203
221
 
204
222
  const gltf = await loader.loadAsync(uri);
205
223
  const parser = gltf.parser;
206
- if (debug) console.log("Loading finished " + uri, material.name, slot, ext.guid);
224
+ if (debug) console.log("Loading finished " + uri, material?.name, slot, ext.guid);
207
225
  let index = -1;
208
226
  let found = false;
209
-
227
+
210
228
  if (!gltf.parser.json?.textures) {
211
- if (debug) console.warn("No textures in glTF " + uri + " - may be a bug", material.name, slot, ext.guid);
229
+ if (debug) console.warn("No textures in glTF " + uri + " - may be a bug", material?.name, slot, ext.guid);
212
230
  return resolve(null);
213
231
  }
214
232
 
@@ -234,7 +252,7 @@
234
252
  }
235
253
  this.resolved[resolveKey] = tex as Texture;
236
254
  if (debug)
237
- console.log(material.name, slot, "change \"" + current.name + "\" → \"" + tex.name + "\"", uri, index, tex, material, resolveKey);
255
+ console.log(material?.name, slot, "change \"" + current.name + "\" → \"" + tex.name + "\"", uri, index, tex, material, resolveKey);
238
256
  resolve(tex);
239
257
  });
240
258
  this.currentlyLoading[resolveKey] = request;
@@ -343,7 +361,7 @@
343
361
  private static _currentProgressiveLoadingInfo: Map<Context, ProgressiveLoadingInfo[]> = new Map();
344
362
 
345
363
  // called whenever a progressive loading event starts
346
- private static onProgressiveLoadStart(context: Context, source: SourceIdentifier | undefined, uri: string, material: Material, slot: string): ProgressiveLoadingInfo {
364
+ private static onProgressiveLoadStart(context: Context, source: SourceIdentifier | undefined, uri: string, material: Material | null, slot: string | null): ProgressiveLoadingInfo {
347
365
  if (!this._currentProgressiveLoadingInfo.has(context)) {
348
366
  this._currentProgressiveLoadingInfo.set(context, []);
349
367
  }
@@ -379,11 +397,11 @@
379
397
  readonly context: Context;
380
398
  readonly source: SourceIdentifier | undefined;
381
399
  readonly uri: string;
382
- readonly material?: Material;
383
- readonly slot?: string;
400
+ readonly material?: Material | null;
401
+ readonly slot?: string | null;
384
402
  // TODO: can contain information if the event is a background process / preloading or if the object is currently visible
385
403
 
386
- constructor(context: Context, source: SourceIdentifier | undefined, uri: string, material?: Material, slot?: string) {
404
+ constructor(context: Context, source: SourceIdentifier | undefined, uri: string, material?: Material | null, slot?: string | null) {
387
405
  this.context = context;
388
406
  this.source = source;
389
407
  this.uri = uri;
plugins/types/needleConfig.d.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  allowHotReload: boolean,
10
10
  license: string | null,
11
11
  useRapier: boolean,
12
+ developmentBuild: boolean,
12
13
  }
13
14
 
14
15
 
src/engine-components-experimental/networking/PlayerSync.ts CHANGED
@@ -276,7 +276,7 @@
276
276
  }
277
277
  else if (debug) console.warn("PlayerState.start → owner is still undefined but dontDestroy is set to true", this.name);
278
278
  }
279
- else console.log("PlayerState.start → owner is assigned", this.owner);
279
+ else if(debug) console.log("PlayerState.start → owner is assigned", this.owner);
280
280
  }, 2000);
281
281
  }
282
282
  }
src/engine-components/SpriteRenderer.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import * as THREE from "three";
2
2
  import { Material, NearestFilter, Texture } from "three";
3
3
 
4
+ import { Context } from "../engine/engine_context.js";
4
5
  import { serializable, serializeable } from "../engine/engine_serialization_decorator.js";
5
6
  import { getParam } from "../engine/engine_utils.js";
7
+ import { NEEDLE_progressive } from "../engine/extensions/NEEDLE_progressive.js";
6
8
  import { Behaviour } from "./Component.js";
7
9
  import { RGBAColor } from "./js-extensions/RGBAColor.js";
8
10
 
@@ -70,7 +72,6 @@
70
72
  }
71
73
 
72
74
  export class Sprite {
73
-
74
75
  @serializable()
75
76
  guid?: string;
76
77
  @serializable(Texture)
@@ -83,6 +84,7 @@
83
84
  vertices!: Array<Vec2>;
84
85
 
85
86
  _geometry?: THREE.BufferGeometry;
87
+ _hasLoadedProgressive: boolean = false;
86
88
  }
87
89
 
88
90
  const $spriteTexOwner = Symbol("spriteOwner");
@@ -101,7 +103,7 @@
101
103
  @serializable()
102
104
  index: number = 0;
103
105
 
104
- update() {
106
+ update(context: Context, sourceId: string | undefined, material: Material | undefined) {
105
107
  if (!this.spriteSheet) return;
106
108
  const index = this.index;
107
109
  if (index < 0 || index >= this.spriteSheet.sprites.length)
@@ -113,6 +115,21 @@
113
115
  if (tex.minFilter == NearestFilter && tex.magFilter == NearestFilter)
114
116
  tex.anisotropy = 1;
115
117
  tex.needsUpdate = true;
118
+
119
+ if (!slice._hasLoadedProgressive) {
120
+ slice._hasLoadedProgressive = true;
121
+ const previousTexture = tex;
122
+ NEEDLE_progressive.assignTextureLOD(context, sourceId, tex, 0).then(res => {
123
+ if (res instanceof Texture) {
124
+ slice.texture = res;
125
+ const shouldUpdateInMaterial = material?.["map"] === previousTexture;
126
+ if (shouldUpdateInMaterial) {
127
+ material["map"] = res;
128
+ material.needsUpdate = true;
129
+ }
130
+ }
131
+ });
132
+ }
116
133
  }
117
134
  }
118
135
 
@@ -137,8 +154,11 @@
137
154
  if (value === this._spriteSheet) return;
138
155
  if (typeof value === "number") {
139
156
  const index = Math.floor(value);
140
- if (index === value)
141
- this.spriteIndex = index;
157
+ // if (value === index)
158
+ this.spriteIndex = index;
159
+ // else if (debug) {
160
+ // console.log("Spritesheet framedrop", index, value);
161
+ // }
142
162
  return;
143
163
  }
144
164
  else {
@@ -172,7 +192,7 @@
172
192
 
173
193
  awake(): void {
174
194
  this._currentSprite = undefined;
175
- if(debug) {
195
+ if (debug) {
176
196
  console.log("Awake", this.name, this, this.sprite);
177
197
  }
178
198
  }
@@ -218,6 +238,7 @@
218
238
  }
219
239
  this.sharedMaterial = mat;
220
240
  this._currentSprite = new THREE.Mesh(SpriteUtils.getOrCreateGeometry(sprite), mat);
241
+ NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId, mat, 0);
221
242
  }
222
243
  else {
223
244
  this._currentSprite.geometry = SpriteUtils.getOrCreateGeometry(sprite);
@@ -231,7 +252,7 @@
231
252
  this.gameObject.add(this._currentSprite);
232
253
  }
233
254
 
234
- if(this._currentSprite){
255
+ if (this._currentSprite) {
235
256
  this._currentSprite.layers.set(this.layer)
236
257
  }
237
258
 
@@ -240,6 +261,6 @@
240
261
  this.sharedMaterial.transparent = this.transparent;
241
262
  }
242
263
  this._currentSprite.castShadow = this.castShadows;
243
- this._spriteSheet?.update();
264
+ this._spriteSheet?.update(this.context, this.sourceId, this.sharedMaterial);
244
265
  }
245
266
  }
plugins/types/userconfig.d.ts CHANGED
@@ -28,6 +28,9 @@
28
28
  /** Set to true to disable generating the buildinfo.json file in your output directory */
29
29
  noBuildInfo: boolean;
30
30
 
31
+ /** Set to true to disable the needle build pipeline (running compression and optimization as a postprocessing step on the exported glTF files) */
32
+ noBuildPipeline: boolean;
33
+
31
34
  /** required for @serializable https://github.com/vitejs/vite/issues/13736 */
32
35
  vite44Hack: boolean;
33
36
 
plugins/vite/build-pipeline.js ADDED
@@ -0,0 +1,97 @@
1
+ import { exec, spawn } from 'child_process';
2
+ import { getOutputDirectory, loadConfig, tryLoadProjectConfig } from './config.js';
3
+ import { existsSync, readdirSync } from 'fs';
4
+
5
+ // see https://linear.app/needle/issue/NE-3798
6
+
7
+ /**
8
+ * Runs the needle build pipeline as part of the vite build process
9
+ * @param {import('../types').userSettings} userSettings
10
+ * @returns {import('vite').Plugin}
11
+ */
12
+ export const needleBuildPipeline = async (command, config, userSettings) => {
13
+ // we only want to run compression here if this is a distribution build
14
+ if (command !== "build") return;
15
+ if (userSettings.noBuildPipeline) return;
16
+
17
+ const meta = await loadConfig();
18
+ let shouldRun = false;
19
+ if (meta && meta.developmentBuild === false) {
20
+ shouldRun = true;
21
+ }
22
+ if (!shouldRun) {
23
+ log("Skipping build pipeline because this is a development build");
24
+ return;
25
+ }
26
+
27
+ /** @type {Promise<any>|null} */
28
+ let task = null;
29
+ let taskFinished = false;
30
+ let taskSucceeded = false;
31
+ return {
32
+ name: 'needle-buildpipeline',
33
+ enforce: "post",
34
+ apply: 'build',
35
+ buildEnd() {
36
+ // start the compression process once vite is done copying the files
37
+ task = invokeBuildPipeline().then((res) => {
38
+ taskFinished = true;
39
+ taskSucceeded = res;
40
+ });
41
+ },
42
+ closeBundle() {
43
+ // this is the last hook that is called, so we can wait for the task to finish here
44
+ if (taskFinished) return;
45
+ // delay the final log slightly to give other plugins a chance to log their stuff
46
+ wait(100).then(() => {
47
+ if (!taskFinished) log("Waiting for postprocessing to finish...")
48
+ });
49
+ return task.then(_ => {
50
+ log("finished", taskSucceeded ? "successfully" : "with errors");
51
+ });
52
+ },
53
+ }
54
+ }
55
+
56
+ function log(...args) {
57
+ console.log("[needle-buildpipeline]", ...args);
58
+ }
59
+
60
+ /**
61
+ * @param {*} opts
62
+ * @returns {Promise<boolean>}
63
+ */
64
+ async function invokeBuildPipeline(opts) {
65
+ await wait(1000);
66
+ const outputDirectory = getOutputDirectory() + "/assets";
67
+ if (!existsSync(outputDirectory)) {
68
+ log("No output directory found at ", outputDirectory);
69
+ return;
70
+ }
71
+ const files = readdirSync(outputDirectory).filter(f => f.endsWith(".glb") || f.endsWith(".gltf"));
72
+ log(files.length + " file(s) to process");
73
+
74
+ const cmd = "npm run transform --prefix node_modules/@needle-tools/gltf-build-pipeline";
75
+ log("Running command \"" + cmd + "\" at " + process.cwd() + "...");
76
+ const sub = exec(cmd);
77
+ sub.stdout.on('data', data => {
78
+ if (data.length <= 0) return;
79
+ // ensure that it doesnt end with a newline
80
+ if (data.endsWith("\n")) data = data.slice(0, -1);
81
+ console.log(data);
82
+ });
83
+ sub.stderr.on('data', console.error);
84
+ return new Promise((resolve, reject) => {
85
+ sub.on('exit', (code) => {
86
+ resolve(code === 0);
87
+ });
88
+ });
89
+ }
90
+
91
+ function wait(ms) {
92
+ return new Promise((resolve, reject) => {
93
+ setTimeout(() => {
94
+ resolve();
95
+ }, ms);
96
+ });
97
+ }