Skip to content

Commit 7c4617d

Browse files
authored
Add a skybox effect (#7843)
1 parent d6d7c5c commit 7c4617d

File tree

4 files changed

+303
-23
lines changed

4 files changed

+303
-23
lines changed

Extensions/3D/JsExtension.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2063,6 +2063,48 @@ module.exports = {
20632063
.setType('number')
20642064
.setGroup(_('Orientation'));
20652065
}
2066+
{
2067+
const effect = extension
2068+
.addEffect('Skybox')
2069+
.setFullName(_('Skybox'))
2070+
.setDescription(
2071+
_('Display a background on a cube surrounding the scene.')
2072+
)
2073+
.markAsNotWorkingForObjects()
2074+
.markAsOnlyWorkingFor3D()
2075+
.addIncludeFile('Extensions/3D/Skybox.js');
2076+
const properties = effect.getProperties();
2077+
properties
2078+
.getOrCreate('rightFaceResourceName')
2079+
.setType('resource')
2080+
.addExtraInfo('image')
2081+
.setLabel(_('Right face (X+)'));
2082+
properties
2083+
.getOrCreate('leftFaceResourceName')
2084+
.setType('resource')
2085+
.addExtraInfo('image')
2086+
.setLabel(_('Left face (X-)'));
2087+
properties
2088+
.getOrCreate('bottomFaceResourceName')
2089+
.setType('resource')
2090+
.addExtraInfo('image')
2091+
.setLabel(_('Bottom face (Y+)'));
2092+
properties
2093+
.getOrCreate('topFaceResourceName')
2094+
.setType('resource')
2095+
.addExtraInfo('image')
2096+
.setLabel(_('Top face (Y-)'));
2097+
properties
2098+
.getOrCreate('frontFaceResourceName')
2099+
.setType('resource')
2100+
.addExtraInfo('image')
2101+
.setLabel(_('Front face (Z+)'));
2102+
properties
2103+
.getOrCreate('backFaceResourceName')
2104+
.setType('resource')
2105+
.addExtraInfo('image')
2106+
.setLabel(_('Back face (Z-)'));
2107+
}
20662108
{
20672109
const effect = extension
20682110
.addEffect('HueAndSaturation')

Extensions/3D/Skybox.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
namespace gdjs {
2+
interface SkyboxFilterNetworkSyncData {}
3+
gdjs.PixiFiltersTools.registerFilterCreator(
4+
'Scene3D::Skybox',
5+
new (class implements gdjs.PixiFiltersTools.FilterCreator {
6+
makeFilter(
7+
target: EffectsTarget,
8+
effectData: EffectData
9+
): gdjs.PixiFiltersTools.Filter {
10+
if (typeof THREE === 'undefined') {
11+
return new gdjs.PixiFiltersTools.EmptyFilter();
12+
}
13+
return new (class implements gdjs.PixiFiltersTools.Filter {
14+
_cubeTexture: THREE.CubeTexture;
15+
_oldBackground:
16+
| THREE.CubeTexture
17+
| THREE.Texture
18+
| THREE.Color
19+
| null = null;
20+
_isEnabled: boolean = false;
21+
22+
constructor() {
23+
this._cubeTexture = target
24+
.getRuntimeScene()
25+
.getGame()
26+
.getImageManager()
27+
.getThreeCubeTexture(
28+
effectData.stringParameters.rightFaceResourceName,
29+
effectData.stringParameters.leftFaceResourceName,
30+
effectData.stringParameters.topFaceResourceName,
31+
effectData.stringParameters.bottomFaceResourceName,
32+
effectData.stringParameters.frontFaceResourceName,
33+
effectData.stringParameters.backFaceResourceName
34+
);
35+
}
36+
37+
isEnabled(target: EffectsTarget): boolean {
38+
return this._isEnabled;
39+
}
40+
setEnabled(target: EffectsTarget, enabled: boolean): boolean {
41+
if (this._isEnabled === enabled) {
42+
return true;
43+
}
44+
if (enabled) {
45+
return this.applyEffect(target);
46+
} else {
47+
return this.removeEffect(target);
48+
}
49+
}
50+
applyEffect(target: EffectsTarget): boolean {
51+
const scene = target.get3DRendererObject() as
52+
| THREE.Scene
53+
| null
54+
| undefined;
55+
if (!scene) {
56+
return false;
57+
}
58+
// TODO Add a background stack in LayerPixiRenderer to allow
59+
// filters to stack them.
60+
this._oldBackground = scene.background;
61+
scene.background = this._cubeTexture;
62+
if (!scene.environment) {
63+
scene.environment = this._cubeTexture;
64+
}
65+
this._isEnabled = true;
66+
return true;
67+
}
68+
removeEffect(target: EffectsTarget): boolean {
69+
const scene = target.get3DRendererObject() as
70+
| THREE.Scene
71+
| null
72+
| undefined;
73+
if (!scene) {
74+
return false;
75+
}
76+
scene.background = this._oldBackground;
77+
scene.environment = null;
78+
this._isEnabled = false;
79+
return true;
80+
}
81+
updatePreRender(target: gdjs.EffectsTarget): any {}
82+
updateDoubleParameter(parameterName: string, value: number): void {}
83+
getDoubleParameter(parameterName: string): number {
84+
return 0;
85+
}
86+
updateStringParameter(parameterName: string, value: string): void {}
87+
updateColorParameter(parameterName: string, value: number): void {}
88+
getColorParameter(parameterName: string): number {
89+
return 0;
90+
}
91+
updateBooleanParameter(parameterName: string, value: boolean): void {}
92+
getNetworkSyncData(): SkyboxFilterNetworkSyncData {
93+
return {};
94+
}
95+
updateFromNetworkSyncData(
96+
syncData: SkyboxFilterNetworkSyncData
97+
): void {}
98+
})();
99+
}
100+
})()
101+
);
102+
}

GDJS/Runtime/pixi-renderers/pixi-image-manager.ts

Lines changed: 148 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ namespace gdjs {
5656
*/
5757
private _loadedThreeTextures: Hashtable<THREE.Texture>;
5858
private _loadedThreeMaterials = new ThreeMaterialCache();
59+
private _loadedThreeCubeTextures = new Map<string, THREE.CubeTexture>();
60+
private _loadedThreeCubeTextureKeysByResourceName = new ArrayMap<
61+
string,
62+
string
63+
>();
5964

6065
private _diskTextures = new Map<float, PIXI.Texture>();
6166
private _rectangleTextures = new Map<string, PIXI.Texture>();
@@ -181,7 +186,25 @@ namespace gdjs {
181186
if (loadedThreeTexture) {
182187
return loadedThreeTexture;
183188
}
189+
const image = this._getImageSource(resourceName);
184190

191+
const threeTexture = new THREE.Texture(image);
192+
threeTexture.magFilter = THREE.LinearFilter;
193+
threeTexture.minFilter = THREE.LinearFilter;
194+
threeTexture.wrapS = THREE.RepeatWrapping;
195+
threeTexture.wrapT = THREE.RepeatWrapping;
196+
threeTexture.colorSpace = THREE.SRGBColorSpace;
197+
threeTexture.needsUpdate = true;
198+
199+
const resource = this._getImageResource(resourceName);
200+
201+
applyThreeTextureSettings(threeTexture, resource);
202+
this._loadedThreeTextures.put(resourceName, threeTexture);
203+
204+
return threeTexture;
205+
}
206+
207+
private _getImageSource(resourceName: string): HTMLImageElement {
185208
// Texture is not loaded, load it now from the PixiJS texture.
186209
// TODO (3D) - optimization: don't load the PixiJS Texture if not used by PixiJS.
187210
// TODO (3D) - optimization: Ideally we could even share the same WebGL texture.
@@ -198,21 +221,86 @@ namespace gdjs {
198221
`Can't load texture for resource "${resourceName}" as it's not an image.`
199222
);
200223
}
224+
return image;
225+
}
201226

202-
const threeTexture = new THREE.Texture(image);
203-
threeTexture.magFilter = THREE.LinearFilter;
204-
threeTexture.minFilter = THREE.LinearFilter;
205-
threeTexture.wrapS = THREE.RepeatWrapping;
206-
threeTexture.wrapT = THREE.RepeatWrapping;
207-
threeTexture.colorSpace = THREE.SRGBColorSpace;
208-
threeTexture.needsUpdate = true;
209-
210-
const resource = this._getImageResource(resourceName);
227+
/**
228+
* Return the three.js texture associated to the specified resource name.
229+
* Returns a placeholder texture if not found.
230+
* @param xPositiveResourceName The name of the resource
231+
* @returns The requested cube texture, or a placeholder if not found.
232+
*/
233+
getThreeCubeTexture(
234+
xPositiveResourceName: string,
235+
xNegativeResourceName: string,
236+
yPositiveResourceName: string,
237+
yNegativeResourceName: string,
238+
zPositiveResourceName: string,
239+
zNegativeResourceName: string
240+
): THREE.CubeTexture {
241+
const key =
242+
xPositiveResourceName +
243+
'|' +
244+
xNegativeResourceName +
245+
'|' +
246+
yPositiveResourceName +
247+
'|' +
248+
yNegativeResourceName +
249+
'|' +
250+
zPositiveResourceName +
251+
'|' +
252+
zNegativeResourceName;
253+
const loadedThreeTexture = this._loadedThreeCubeTextures.get(key);
254+
if (loadedThreeTexture) {
255+
return loadedThreeTexture;
256+
}
211257

212-
applyThreeTextureSettings(threeTexture, resource);
213-
this._loadedThreeTextures.put(resourceName, threeTexture);
258+
const cubeTexture = new THREE.CubeTexture();
259+
// Faces on X axis need to be swapped.
260+
cubeTexture.images[0] = this._getImageSource(xNegativeResourceName);
261+
cubeTexture.images[1] = this._getImageSource(xPositiveResourceName);
262+
// Faces on Y keep the same order.
263+
cubeTexture.images[2] = this._getImageSource(yPositiveResourceName);
264+
cubeTexture.images[3] = this._getImageSource(yNegativeResourceName);
265+
// Faces on Z keep the same order.
266+
cubeTexture.images[4] = this._getImageSource(zPositiveResourceName);
267+
cubeTexture.images[5] = this._getImageSource(zNegativeResourceName);
268+
// The images also need to be mirrored horizontally by users.
269+
270+
cubeTexture.magFilter = THREE.LinearFilter;
271+
cubeTexture.minFilter = THREE.LinearFilter;
272+
cubeTexture.colorSpace = THREE.SRGBColorSpace;
273+
cubeTexture.needsUpdate = true;
274+
275+
const resource = this._getImageResource(xPositiveResourceName);
276+
applyThreeTextureSettings(cubeTexture, resource);
277+
this._loadedThreeCubeTextures.set(key, cubeTexture);
278+
this._loadedThreeCubeTextureKeysByResourceName.add(
279+
xPositiveResourceName,
280+
key
281+
);
282+
this._loadedThreeCubeTextureKeysByResourceName.add(
283+
xNegativeResourceName,
284+
key
285+
);
286+
this._loadedThreeCubeTextureKeysByResourceName.add(
287+
yPositiveResourceName,
288+
key
289+
);
290+
this._loadedThreeCubeTextureKeysByResourceName.add(
291+
yNegativeResourceName,
292+
key
293+
);
294+
this._loadedThreeCubeTextureKeysByResourceName.add(
295+
zPositiveResourceName,
296+
key
297+
);
298+
this._loadedThreeCubeTextureKeysByResourceName.add(
299+
zNegativeResourceName,
300+
key
301+
);
214302

215-
return threeTexture;
303+
return cubeTexture;
216304
}
217305

218306
/**
@@ -482,6 +570,11 @@ namespace gdjs {
482570
for (const threeTexture of threeTextures) {
483571
threeTexture.dispose();
484572
}
573+
for (const cubeTexture of this._loadedThreeCubeTextures.values()) {
574+
cubeTexture.dispose();
575+
}
576+
this._loadedThreeCubeTextures.clear();
577+
this._loadedThreeCubeTextureKeysByResourceName.clear();
485578

486579
this._loadedThreeMaterials.disposeAll();
487580

@@ -528,12 +621,51 @@ namespace gdjs {
528621
}
529622

530623
this._loadedThreeMaterials.dispose(resourceName);
624+
625+
const cubeTextureKeys =
626+
this._loadedThreeCubeTextureKeysByResourceName.getValuesFor(
627+
resourceName
628+
);
629+
if (cubeTextureKeys) {
630+
for (const cubeTextureKey of cubeTextureKeys) {
631+
const cubeTexture = this._loadedThreeCubeTextures.get(cubeTextureKey);
632+
if (cubeTexture) {
633+
cubeTexture.dispose();
634+
this._loadedThreeCubeTextures.delete(cubeTextureKey);
635+
}
636+
}
637+
}
638+
}
639+
}
640+
641+
class ArrayMap<K, V> {
642+
map = new Map<K, Array<V>>();
643+
644+
getValuesFor(key: K): Array<V> | undefined {
645+
return this.map.get(key);
646+
}
647+
648+
add(key: K, value: V): void {
649+
let values = this.map.get(key);
650+
if (!values) {
651+
values = [];
652+
this.map.set(key, values);
653+
}
654+
values.push(value);
655+
}
656+
657+
deleteValuesFor(key: K): void {
658+
this.map.delete(key);
659+
}
660+
661+
clear(): void {
662+
this.map.clear();
531663
}
532664
}
533665

534666
class ThreeMaterialCache {
535667
private _flaggedMaterials = new Map<string, THREE.Material>();
536-
private _materialFlaggedKeys = new Map<string, Array<string>>();
668+
private _materialFlaggedKeys = new ArrayMap<string, string>();
537669

538670
/**
539671
* Return the three.js material associated to the specified resource name
@@ -584,12 +716,7 @@ namespace gdjs {
584716
forceBasicMaterial ? 1 : 0
585717
}|${vertexColors ? 1 : 0}`;
586718
this._flaggedMaterials.set(cacheKey, material);
587-
let flaggedKeys = this._materialFlaggedKeys.get(resourceName);
588-
if (!flaggedKeys) {
589-
flaggedKeys = [];
590-
this._materialFlaggedKeys.set(resourceName, flaggedKeys);
591-
}
592-
flaggedKeys.push(cacheKey);
719+
this._materialFlaggedKeys.add(resourceName, cacheKey);
593720
}
594721

595722
/**
@@ -598,7 +725,7 @@ namespace gdjs {
598725
* @param resourceName The name of the resource
599726
*/
600727
dispose(resourceName: string): void {
601-
const flaggedKeys = this._materialFlaggedKeys.get(resourceName);
728+
const flaggedKeys = this._materialFlaggedKeys.getValuesFor(resourceName);
602729
if (flaggedKeys) {
603730
for (const flaggedKey of flaggedKeys) {
604731
const threeMaterial = this._flaggedMaterials.get(flaggedKey);
@@ -608,7 +735,7 @@ namespace gdjs {
608735
this._flaggedMaterials.delete(flaggedKey);
609736
}
610737
}
611-
this._materialFlaggedKeys.delete(resourceName);
738+
this._materialFlaggedKeys.deleteValuesFor(resourceName);
612739
}
613740

614741
/**

0 commit comments

Comments
 (0)