Add a skybox effect (#7843)

This commit is contained in:
D8H
2025-09-23 21:14:44 +02:00
committed by GitHub
parent d6d7c5c1fb
commit 7c4617da99
4 changed files with 302 additions and 22 deletions

View File

@@ -2063,6 +2063,48 @@ module.exports = {
.setType('number')
.setGroup(_('Orientation'));
}
{
const effect = extension
.addEffect('Skybox')
.setFullName(_('Skybox'))
.setDescription(
_('Display a background on a cube surrounding the scene.')
)
.markAsNotWorkingForObjects()
.markAsOnlyWorkingFor3D()
.addIncludeFile('Extensions/3D/Skybox.js');
const properties = effect.getProperties();
properties
.getOrCreate('rightFaceResourceName')
.setType('resource')
.addExtraInfo('image')
.setLabel(_('Right face (X+)'));
properties
.getOrCreate('leftFaceResourceName')
.setType('resource')
.addExtraInfo('image')
.setLabel(_('Left face (X-)'));
properties
.getOrCreate('bottomFaceResourceName')
.setType('resource')
.addExtraInfo('image')
.setLabel(_('Bottom face (Y+)'));
properties
.getOrCreate('topFaceResourceName')
.setType('resource')
.addExtraInfo('image')
.setLabel(_('Top face (Y-)'));
properties
.getOrCreate('frontFaceResourceName')
.setType('resource')
.addExtraInfo('image')
.setLabel(_('Front face (Z+)'));
properties
.getOrCreate('backFaceResourceName')
.setType('resource')
.addExtraInfo('image')
.setLabel(_('Back face (Z-)'));
}
{
const effect = extension
.addEffect('HueAndSaturation')

102
Extensions/3D/Skybox.ts Normal file
View File

@@ -0,0 +1,102 @@
namespace gdjs {
interface SkyboxFilterNetworkSyncData {}
gdjs.PixiFiltersTools.registerFilterCreator(
'Scene3D::Skybox',
new (class implements gdjs.PixiFiltersTools.FilterCreator {
makeFilter(
target: EffectsTarget,
effectData: EffectData
): gdjs.PixiFiltersTools.Filter {
if (typeof THREE === 'undefined') {
return new gdjs.PixiFiltersTools.EmptyFilter();
}
return new (class implements gdjs.PixiFiltersTools.Filter {
_cubeTexture: THREE.CubeTexture;
_oldBackground:
| THREE.CubeTexture
| THREE.Texture
| THREE.Color
| null = null;
_isEnabled: boolean = false;
constructor() {
this._cubeTexture = target
.getRuntimeScene()
.getGame()
.getImageManager()
.getThreeCubeTexture(
effectData.stringParameters.rightFaceResourceName,
effectData.stringParameters.leftFaceResourceName,
effectData.stringParameters.topFaceResourceName,
effectData.stringParameters.bottomFaceResourceName,
effectData.stringParameters.frontFaceResourceName,
effectData.stringParameters.backFaceResourceName
);
}
isEnabled(target: EffectsTarget): boolean {
return this._isEnabled;
}
setEnabled(target: EffectsTarget, enabled: boolean): boolean {
if (this._isEnabled === enabled) {
return true;
}
if (enabled) {
return this.applyEffect(target);
} else {
return this.removeEffect(target);
}
}
applyEffect(target: EffectsTarget): boolean {
const scene = target.get3DRendererObject() as
| THREE.Scene
| null
| undefined;
if (!scene) {
return false;
}
// TODO Add a background stack in LayerPixiRenderer to allow
// filters to stack them.
this._oldBackground = scene.background;
scene.background = this._cubeTexture;
if (!scene.environment) {
scene.environment = this._cubeTexture;
}
this._isEnabled = true;
return true;
}
removeEffect(target: EffectsTarget): boolean {
const scene = target.get3DRendererObject() as
| THREE.Scene
| null
| undefined;
if (!scene) {
return false;
}
scene.background = this._oldBackground;
scene.environment = null;
this._isEnabled = false;
return true;
}
updatePreRender(target: gdjs.EffectsTarget): any {}
updateDoubleParameter(parameterName: string, value: number): void {}
getDoubleParameter(parameterName: string): number {
return 0;
}
updateStringParameter(parameterName: string, value: string): void {}
updateColorParameter(parameterName: string, value: number): void {}
getColorParameter(parameterName: string): number {
return 0;
}
updateBooleanParameter(parameterName: string, value: boolean): void {}
getNetworkSyncData(): SkyboxFilterNetworkSyncData {
return {};
}
updateFromNetworkSyncData(
syncData: SkyboxFilterNetworkSyncData
): void {}
})();
}
})()
);
}

View File

@@ -56,6 +56,11 @@ namespace gdjs {
*/
private _loadedThreeTextures: Hashtable<THREE.Texture>;
private _loadedThreeMaterials = new ThreeMaterialCache();
private _loadedThreeCubeTextures = new Map<string, THREE.CubeTexture>();
private _loadedThreeCubeTextureKeysByResourceName = new ArrayMap<
string,
string
>();
private _diskTextures = new Map<float, PIXI.Texture>();
private _rectangleTextures = new Map<string, PIXI.Texture>();
@@ -181,7 +186,25 @@ namespace gdjs {
if (loadedThreeTexture) {
return loadedThreeTexture;
}
const image = this._getImageSource(resourceName);
const threeTexture = new THREE.Texture(image);
threeTexture.magFilter = THREE.LinearFilter;
threeTexture.minFilter = THREE.LinearFilter;
threeTexture.wrapS = THREE.RepeatWrapping;
threeTexture.wrapT = THREE.RepeatWrapping;
threeTexture.colorSpace = THREE.SRGBColorSpace;
threeTexture.needsUpdate = true;
const resource = this._getImageResource(resourceName);
applyThreeTextureSettings(threeTexture, resource);
this._loadedThreeTextures.put(resourceName, threeTexture);
return threeTexture;
}
private _getImageSource(resourceName: string): HTMLImageElement {
// Texture is not loaded, load it now from the PixiJS texture.
// TODO (3D) - optimization: don't load the PixiJS Texture if not used by PixiJS.
// TODO (3D) - optimization: Ideally we could even share the same WebGL texture.
@@ -198,21 +221,86 @@ namespace gdjs {
`Can't load texture for resource "${resourceName}" as it's not an image.`
);
}
return image;
}
const threeTexture = new THREE.Texture(image);
threeTexture.magFilter = THREE.LinearFilter;
threeTexture.minFilter = THREE.LinearFilter;
threeTexture.wrapS = THREE.RepeatWrapping;
threeTexture.wrapT = THREE.RepeatWrapping;
threeTexture.colorSpace = THREE.SRGBColorSpace;
threeTexture.needsUpdate = true;
/**
* Return the three.js texture associated to the specified resource name.
* Returns a placeholder texture if not found.
* @param xPositiveResourceName The name of the resource
* @returns The requested cube texture, or a placeholder if not found.
*/
getThreeCubeTexture(
xPositiveResourceName: string,
xNegativeResourceName: string,
yPositiveResourceName: string,
yNegativeResourceName: string,
zPositiveResourceName: string,
zNegativeResourceName: string
): THREE.CubeTexture {
const key =
xPositiveResourceName +
'|' +
xNegativeResourceName +
'|' +
yPositiveResourceName +
'|' +
yNegativeResourceName +
'|' +
zPositiveResourceName +
'|' +
zNegativeResourceName;
const loadedThreeTexture = this._loadedThreeCubeTextures.get(key);
if (loadedThreeTexture) {
return loadedThreeTexture;
}
const resource = this._getImageResource(resourceName);
const cubeTexture = new THREE.CubeTexture();
// Faces on X axis need to be swapped.
cubeTexture.images[0] = this._getImageSource(xNegativeResourceName);
cubeTexture.images[1] = this._getImageSource(xPositiveResourceName);
// Faces on Y keep the same order.
cubeTexture.images[2] = this._getImageSource(yPositiveResourceName);
cubeTexture.images[3] = this._getImageSource(yNegativeResourceName);
// Faces on Z keep the same order.
cubeTexture.images[4] = this._getImageSource(zPositiveResourceName);
cubeTexture.images[5] = this._getImageSource(zNegativeResourceName);
// The images also need to be mirrored horizontally by users.
applyThreeTextureSettings(threeTexture, resource);
this._loadedThreeTextures.put(resourceName, threeTexture);
cubeTexture.magFilter = THREE.LinearFilter;
cubeTexture.minFilter = THREE.LinearFilter;
cubeTexture.colorSpace = THREE.SRGBColorSpace;
cubeTexture.needsUpdate = true;
return threeTexture;
const resource = this._getImageResource(xPositiveResourceName);
applyThreeTextureSettings(cubeTexture, resource);
this._loadedThreeCubeTextures.set(key, cubeTexture);
this._loadedThreeCubeTextureKeysByResourceName.add(
xPositiveResourceName,
key
);
this._loadedThreeCubeTextureKeysByResourceName.add(
xNegativeResourceName,
key
);
this._loadedThreeCubeTextureKeysByResourceName.add(
yPositiveResourceName,
key
);
this._loadedThreeCubeTextureKeysByResourceName.add(
yNegativeResourceName,
key
);
this._loadedThreeCubeTextureKeysByResourceName.add(
zPositiveResourceName,
key
);
this._loadedThreeCubeTextureKeysByResourceName.add(
zNegativeResourceName,
key
);
return cubeTexture;
}
/**
@@ -482,6 +570,11 @@ namespace gdjs {
for (const threeTexture of threeTextures) {
threeTexture.dispose();
}
for (const cubeTexture of this._loadedThreeCubeTextures.values()) {
cubeTexture.dispose();
}
this._loadedThreeCubeTextures.clear();
this._loadedThreeCubeTextureKeysByResourceName.clear();
this._loadedThreeMaterials.disposeAll();
@@ -528,12 +621,51 @@ namespace gdjs {
}
this._loadedThreeMaterials.dispose(resourceName);
const cubeTextureKeys =
this._loadedThreeCubeTextureKeysByResourceName.getValuesFor(
resourceName
);
if (cubeTextureKeys) {
for (const cubeTextureKey of cubeTextureKeys) {
const cubeTexture = this._loadedThreeCubeTextures.get(cubeTextureKey);
if (cubeTexture) {
cubeTexture.dispose();
this._loadedThreeCubeTextures.delete(cubeTextureKey);
}
}
}
}
}
class ArrayMap<K, V> {
map = new Map<K, Array<V>>();
getValuesFor(key: K): Array<V> | undefined {
return this.map.get(key);
}
add(key: K, value: V): void {
let values = this.map.get(key);
if (!values) {
values = [];
this.map.set(key, values);
}
values.push(value);
}
deleteValuesFor(key: K): void {
this.map.delete(key);
}
clear(): void {
this.map.clear();
}
}
class ThreeMaterialCache {
private _flaggedMaterials = new Map<string, THREE.Material>();
private _materialFlaggedKeys = new Map<string, Array<string>>();
private _materialFlaggedKeys = new ArrayMap<string, string>();
/**
* Return the three.js material associated to the specified resource name
@@ -584,12 +716,7 @@ namespace gdjs {
forceBasicMaterial ? 1 : 0
}|${vertexColors ? 1 : 0}`;
this._flaggedMaterials.set(cacheKey, material);
let flaggedKeys = this._materialFlaggedKeys.get(resourceName);
if (!flaggedKeys) {
flaggedKeys = [];
this._materialFlaggedKeys.set(resourceName, flaggedKeys);
}
flaggedKeys.push(cacheKey);
this._materialFlaggedKeys.add(resourceName, cacheKey);
}
/**
@@ -598,7 +725,7 @@ namespace gdjs {
* @param resourceName The name of the resource
*/
dispose(resourceName: string): void {
const flaggedKeys = this._materialFlaggedKeys.get(resourceName);
const flaggedKeys = this._materialFlaggedKeys.getValuesFor(resourceName);
if (flaggedKeys) {
for (const flaggedKey of flaggedKeys) {
const threeMaterial = this._flaggedMaterials.get(flaggedKey);
@@ -608,7 +735,7 @@ namespace gdjs {
this._flaggedMaterials.delete(flaggedKey);
}
}
this._materialFlaggedKeys.delete(resourceName);
this._materialFlaggedKeys.deleteValuesFor(resourceName);
}
/**

View File

@@ -18,6 +18,7 @@ namespace gdjs {
rendered2DLayersCount: 0,
rendered3DLayersCount: 0,
};
private _backgroundColor: THREE.Color | null = null;
constructor(
runtimeScene: gdjs.RuntimeScene,
@@ -209,15 +210,23 @@ namespace gdjs {
);
threeRenderer.resetState();
if (this._runtimeScene.getClearCanvas()) threeRenderer.clear();
threeScene.background = new THREE.Color(
if (!this._backgroundColor) {
this._backgroundColor = new THREE.Color();
}
this._backgroundColor.set(
this._runtimeScene.getBackgroundColor()
);
if (!threeScene.background) {
threeScene.background = this._backgroundColor;
}
isFirstRender = false;
} else {
// It's important to set the background to null, as maybe the first rendered
// layer has changed and so the Three.js scene background must be reset.
threeScene.background = null;
if (threeScene.background === this._backgroundColor) {
threeScene.background = null;
}
}
// Clear the depth as each layer is independent and display on top of the previous one,