Avoid loading cubemap as DDS file?

I’m running into the issue where running my game outside of PlayCanvas has a missing skybox, due to the server not handling the mimetype of DDS files correctly, as discussed here: [SOLVED] Web Download builds missing Cubemaps - #2 by yaustar

My problem is I’m not self hosting this project, I’m submitting to websites like Newgrounds. I’m sure they’d be happy to look into handling this on their side, I’m wondering if there’s a workaround I can do in the meantime.

Is there a way to load the skybox in the editor just as JPEG’s? Or would it need be loaded programmatically?

Figured it out, this post by @Leonidas was the most helpful: How to create skybox programmatically? - #12 by Leonidas, pointing out the PlayCanvas glTF has an example of doing this.

Here’s a minimal script you can attach to your Root entity that’ll programmatically attach a skybox if anyone needs a quick solution. This assumes your skybox images are called ['px', 'nx', 'py', 'ny', 'pz', 'nz'] with .png suffix, and note it sets the intensity to 0.05 at the end.

var ProgrammaticSkybox = pc.createScript('programmaticSkybox');

// initialize code called once per entity
ProgrammaticSkybox.prototype.initialize = function() {
    // Required to make skybox work in Newgrounds
    // See: https://forum.playcanvas.com/t/avoid-loading-cubemap-as-dds-file/21214
    const app = this.app;
    const textures = ['px', 'nx', 'py', 'ny', 'pz', 'nz'].map(name => this.app.assets.find(name + '.png'));
    this.textures = textures;
    window.skyboxScript = this;
    
    const cubemapAsset = new pc.Asset('skybox_cubemap', 'cubemap', null, {
        textures: textures.map(function (faceAsset) {
            return faceAsset.id;
        })
    });
    cubemapAsset.loadFaces = true;
    cubemapAsset.on('load', function () {
        this.initSkyboxFromTexture(cubemapAsset.resource);
    }.bind(this));
    app.assets.add(cubemapAsset);
    app.assets.load(cubemapAsset);
};

ProgrammaticSkybox.prototype.initSkyboxFromTexture = function(skybox) {
    const app = this.app;
    const device = app.graphicsDevice;

    const cubemaps = [];

    const reprojectToCubemap = function (src, size) {
        // generate faces cubemap
        const faces = new pc.Texture(device, {
            name: 'skyboxFaces',
            cubemap: true,
            width: size,
            height: size,
            type: pc.TEXTURETYPE_RGBM.toString(),
            addressU: pc.ADDRESS_CLAMP_TO_EDGE,
            addressV: pc.ADDRESS_CLAMP_TO_EDGE,
            fixCubemapSeams: false,
            mipmaps: false
        });
        pc.reprojectTexture(src, faces);

        return faces;
    };

    if (skybox.cubemap) {
        if (skybox.type === pc.TEXTURETYPE_DEFAULT || skybox.type === pc.TEXTURETYPE_RGBM) {
            // cubemap format is acceptible, use it directly
            cubemaps.push(skybox);
        } else {
            // cubemap must be rgbm or default to be used on the skybox
            cubemaps.push(reprojectToCubemap(skybox, skybox.width));
        }
    } else {
        skybox.projection = pc.TEXTUREPROJECTION_EQUIRECT;
        // reproject equirect to cubemap for skybox
        cubemaps.push(reprojectToCubemap(skybox, skybox.width / 4));
    }

    // generate prefiltered lighting data
    const sizes = [128, 64, 32, 16, 8, 4];
    const specPower = [undefined, 512, 128, 32, 8, 2];
    for (let i = 0; i < sizes.length; ++i) {
        const prefilter = new pc.Texture(device, {
            cubemap: true,
            name: 'skyboxPrefilter' + i,
            width: sizes[i],
            height: sizes[i],
            type: pc.TEXTURETYPE_RGBM,
            addressU: pc.ADDRESS_CLAMP_TO_EDGE,
            addressV: pc.ADDRESS_CLAMP_TO_EDGE,
            fixCubemapSeams: true,
            mipmaps: false
        });
        // @ts-ignore
        pc.reprojectTexture(device, cubemaps[1] || skybox, prefilter, specPower[i], 4096);
        cubemaps.push(prefilter);
    }

    // assign the textures to the scene
    app.scene.gammaCorrection = pc.GAMMA_SRGB;
    app.scene.skyboxMip = this.skyboxMip;               // Set the skybox to the 128x128 cubemap mipmap level
    app.scene.setSkybox(cubemaps);
    app.renderNextFrame = true;                         // ensure we render again when the cubemap arrives
    app.scene.skyboxIntensity = 0.05;

};
2 Likes

Nice, thanks for sharing the full solution @OmarShehata!

1 Like

Surprised that portals like new grounds doesn’t allow this :frowning: