Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

KHR_environment_map #1956

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open

KHR_environment_map #1956

wants to merge 29 commits into from

Conversation

rsahlin
Copy link

@rsahlin rsahlin commented Mar 17, 2021

This is a continuation of #1850 which seems to have gone stale and is based on a fork of glTF in the UX3D repo.

The purpose of this extension is to add support for defining an environment map (cubemap or spherical harmonics) to a glTF scene.
I have done my best to update according to comments in #1850

This extension is based on EXT_lights_image_based, with the major difference being that KTX2 is used as a container format for cubemap textures.
Cubemaps may choose to include prefiltered mip-levels , if not included this shall be done by implementations.
This extension specifies a set of texture formats that are allowed for the cubemaps.

@bhouston bhouston mentioned this pull request Apr 6, 2021
Copy link
Contributor

@bhouston bhouston left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @rsahlin for this. You have done the heavily lifting dealing with the image formats, especially the new extensions to support varying image formats.

@bhouston
Copy link
Contributor

bhouston commented Apr 6, 2021

With regards to names, I prefer not "environment", rather Sky Dome, Dome Light or Sky Light as I think that is more descriptive. Here is a survey of what other renderers and engines use:

  • V-Ray: Dome Light
  • Arnold: Sky Dome Light
  • UE4: Sky Light
  • Unity: Sky Box

Clarification of cubemap generation and use of spherical harmonics.
Remove duplicate section.
Added images and textures to json example.
Added implementation note for specular and irradiance sections.
Updated contributors.
rsahlin added 3 commits April 8, 2021 14:10
Elaborate on how KTX v2 is supported, add supported formats.
Specify that this extension requires KHR_texture_ktx.
Clarify that no pre-filtering shall be done for uncompressed formats.
Add open issues.
@rsahlin rsahlin mentioned this pull request Apr 29, 2021
rsahlin added 2 commits April 30, 2021 09:15
Add section for KTX v2 images, update json to include extensionsUsed
@netpro2k
Copy link

Starting to look into how best to represent environment map data for scenes in Mozilla Hubs. This definitely looks pretty close to our needs.

Its likely for our usecase we will want to be able to define multiple environment maps per scene, either on a per material level, or perhaps in more of a "light probe" way where environment maps will be effective in some spatial region, and possibly be mixed. The later certainly sounds out of scope for this extension, but allowing references on materials may make sense (the currently specified one on the scene being the default for materials not overriding it). In any case we can certainly at least share the actual lights definitions even if we are just pointing at it from an other extension or app specific data.

We also have a MOZ_lightmap extension which allows attaching baked light maps to materials (right now not very formally defined, but very simply adds a single texture to materials directly mapping to three.js's lightmap implementation) so we also have a desire to be able to use environment cubuemaps only for specular reflections and not diffuse lighting (as that is already baked into the lightmaps). Again, its possible here we can just use the specularCubemaps data as specified in this extension and ignore/omit the irradianceCoefficients for these cases, but just bringing it up as a point of discussion in case its generally applicable.

@rsahlin
Copy link
Author

rsahlin commented May 19, 2021

Hi netpro2k and thanks for your comments!

Its likely for our usecase we will want to be able to define multiple environment maps per scene, either on a per material level, or perhaps in more of a "light probe" way where environment maps will be effective in some spatial region, and possibly be mixed. The later certainly sounds out of scope for this extension, but allowing references on materials may make sense (the currently specified one on the scene being the default for materials not overriding it). In any case we can certainly at least share the actual lights definitions even if we are just pointing at it from an other extension or app specific data.

A good idea to be able to specify multiple environment maps!
Not sure it should be on material level though (since it's not really a property of the material)
Think it makes sense on a node (spatial) level - but not sure on how to actually achieve it.

Do you have a suggestion on how to spatially override the scene environment map?

@rsahlin
Copy link
Author

rsahlin commented May 19, 2021

We also have a MOZ_lightmap extension which allows attaching baked light maps to materials (right now not very formally defined, but very simply adds a single texture to materials directly mapping to three.js's lightmap implementation) so we also have a desire to be able to use environment cubuemaps only for specular reflections and not diffuse lighting (as that is already baked into the lightmaps). Again, its possible here we can just use the specularCubemaps data as specified in this extension and ignore/omit the irradianceCoefficients for these cases, but just bringing it up as a point of discussion in case its generally applicable.

Good point - maybe I could add a value indicating the contribution/use of the specularCubemaps
LIke a bitflag with values:
SPECULAR - cubemap contributes to direct (incoming) light.
DIFFUSE - cubemap contributes to diffuse light - if used together with irradianceCoefficients implementations may choose which one they prefer.
REFLECTION - cubemap used for environment reflections - this means that a filtered cubemap needs to be created if not already present.

Do you think this could work?

@bhouston
Copy link
Contributor

maybe I could add a value indicating the contribution/use of the specularCubemaps

This is non-physical, advanced and I would suggest we do not do that. This is similar to the concept to advanced light linking, where only certain objects are affected by certain lights. This is not supported in the punctual light extension either because it is really advanced.

@bhouston
Copy link
Contributor

I think that if we removed SH's from the current definition and had the mipmaps optional (I prefer removed, but it isn't that important), this could get approved by the committee. I think because we haven't done that this extension is not getting support from the standards committee and we are moving around laterally as a result.

I think there is a clear way forward to getting this approved. I would suggest we just go ahead and get it done?

@netpro2k
Copy link

I think that if we removed SH's from the current definition and had the mipmaps optional (I prefer removed, but it isn't that important), this could get approved by the committee.

While I definitely appreciate the tooling simplicity this would provide,I think my main concern with this extension essentially being just "a way to associate cubemaps with a gltf" would be that it is then a bit under specified what that model should look like at runtime.

I am not particularly tied to including SHs but I do think this extension needs to at least mostly specify how whatever data it does include is to be used.

Re mips, I am still unclear on what the suggested solution is for compressed textures if we do not require including the full mip chain other than decompressing it, generating the mip chain, and then uploading the entire thing uncompressed. I suppose this is not completely unacceptable but certainly seems undesirable from a "I know what the runtime costs of this asset I am creating are going to be" perspective.

This is non-physical, advanced and I would suggest we do not do that. This is similar to the concept to advanced light linking, where only certain objects are affected by certain lights.

I would definitely agree on the point of light linking being out of scope for this, not sure about including something to indicate how the cubemap should be used. Especially if dropping SHs (since previously it was discussed that you could omit the cubemap or SH if you only wanted specular or diffuse components) it does seem like it may be useful to be able to specify multiple ways to use the cubemap data. Don't feel particularly strong about this point either though, I can see a good argument for this being only a "purely physical" lighting representation to keep things simple.

Not sure it should be on material level though (since it's not really a property of the material)
Think it makes sense on a node (spatial) level - but not sure on how to actually achieve it.

Hmm yeah its a bit conceptually weird to associate it with a material, though this is what three.js does. Associating with nodes could also make sense, though I do think any sort of spatial mapping (like specifying a bounding area of effect, falloff, etc) is probably getting out of scope for this and should be done in another extension.

Specifying just 1 environment map for the entire gltf seems a bit restricted to the sort of "model viewer" usecase, where I am thinking more along the lines of a "level" asset which will likely need multiple.

I guess I am trying to come up with something in between single environment map per scene and a full "light probe" extension, but that might end up being too awkward.

@rsahlin
Copy link
Author

rsahlin commented Nov 17, 2021

Hi @johguenther

  1. Allow rotation or not? It's useful to tweak the look and illumination, and it would allow for a dynamic (animated, rotating)
    environment. The alternative to apply the inverse transform to the whole scene (and camera) seems an ugly workaround to me.

I don't really understand the usecase here - could you please elaborate?
Conceptually I see the environment as fixed and non rotatable, just like it is in the real world.

3.If the latter, is it a problem if there are multiple environments at the same time?

I would say yes, since we do want the implementation to be accessible.
I know that most game engines support multiple light probes / SH's - but this is extension is not meant to provide this functionallity.
The goal is to be able to specify one environment light (direct and diffuse light) - once this is in place we can look into an extension that does a similar thing but on a Node level (light probes)
I see the two as different in what they do - one environment vs multiple lights/probes.

Hope you understand what I mean?

Best regards
/Richard

@johguenther
Copy link

I don't really understand the usecase here - could you please elaborate? Conceptually I see the environment as fixed and non rotatable, just like it is in the real world.

It probably depends on the point of view: I see it mainly as a light source (after all it's a KHR_lights extension) and secondarily as environment, whereas you place the emphasis the other way around. A usecase example is a studio environment (also existing in the real world) where the light boxes / diffusors can be rotated around the subject.

The goal is to be able to specify one environment light (direct and diffuse light) - once this is in place we can look into an extension that does a similar thing but on a Node level (light probes) I see the two as different in what they do - one environment vs multiple lights/probes.

Fair enough. Is there already such a "light probes on nodes" extension in the making (a PR)?

@donmccurdy
Copy link
Contributor

donmccurdy commented Nov 18, 2021

Fair enough. Is there already such a "light probes on nodes" extension in the making (a PR)?

The number of authoring tools that can export light probe data in any format is — as best as I can tell — zero, despite efforts to do so:

That being the case, I'm unfortunately not sure where to start defining a standard light probe representation. Ideally we want standards to be informed by some kind of implementation, even if it's only proof-of-concept. It's a compelling feature, but I think it probably needs concrete workflows and interest from potential implementors first.

@johguenther
Copy link

I was thinking just of the HDRI environment texture, to be placed as a light at nodes. An implementation / renderer could derive the SH coefficients or other pre-filtering from that in a preprocessing step, if needed.

@rsahlin
Copy link
Author

rsahlin commented Nov 19, 2021

I was thinking just of the HDRI environment texture, to be placed as a light at nodes. An implementation / renderer could derive the SH coefficients or other pre-filtering from that in a preprocessing step, if needed.

This is what this extension aims to do for the scene - as opposed to node level.
That is, if no irradiance map (SH) is specified then the cubemap shall be used to calculate irradiance.

This extension shall be standalone without the dependency to some other extension to define cubemap texture support.
Fixes to schema, add schema for cubemap
"uri": "cubemap0.ktx2",
"mimeType": "image/ktx2"
}
],
Copy link
Contributor

@donmccurdy donmccurdy Jan 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious, why create a new images array under the extension rather than using the root images array? Similarly, it might be more natural to use the root textures array rather than adding cubemaps here. For example:

// root
{
  "textures": [{
    "source": 0, 
    "extensions": {"KHR_lights_environment":{"layer": 0}} // optional
  }],
  "images": [{
    "uri": "cubemap.ktx2",
    "mimeType": "image/ktx2"
  }],
  "extensions": [{
    "lights": [{
      "name": "SceneEnvironment",
      "cubemap": 0,
      "irradianceCoefficients": [...],
      ...
    }]
  }],
  ...
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @donmccurdy and thanks for your comment

I agree that it is not the obvious choice to put the cubemaps inside the light extension.
The reason for this is backwards compatibility and separation.

By backwards compatibility I mean that since we introduce a new fileformat (ktx) and new texture formats this may break current implementations if they go through the main images and/or textures arrays and treat them like they are jpg/png.
This is a behavior which I think must be allowed with the glTF 2.0 spec - there simply is no other formats for images/textures.

If the cubemaps are included in the root images/textures arrays - how are implementations that does not support this extension supposed to handle those resources?

By separation I mean that from a data packaging perspective it may make sense to put the cubemaps inside the extension to make it abundantly clear that these resources are only ever used if the extension is supported (and used)

I understand that the behavior to load the cubemaps will be slightly different than to normal resources - but I think it's worth it due to the above reasons.

Please let me know what you think!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, I don't think this would break any existing implementation in the same way that the KHR_texture_basisu extension didn't by adding image/ktx2 mime type in the images. Otherwise an implementation that didn't support KHR_texture_basisu should run into the same issue you described.

In practice I think most implementation would load what they support and either ignore the rest or load them generically.

For example, in my image loading implementation (WebGPU/WebGL based), for jpg/png or any web loadable image type I load it using an Image element and use createImageBitmap to send it to the GPU. For everything else I simply load it as an array buffer, working under the assumption that any spec compliant object that references that image will know what to do it with it from there. As is the case with KHR_texture_basisu, and now this KHR_lights_environment would as well. In my case with the current spec of this extension, I reused the same Image loading logic I just had to call it again from the extension specific logic rather during the root load logic.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aaah, now I see what you mean.

That would mean that the expected behavior at load time is to check what textures are used and then proceed to load image resources based on that usage.

I will move the images from being included in the extension to be at root level images array instead.

## Specular Radiance Cubemaps

The cubemap used for specular radiance is defined as a cubemap containing separate images for each cube face.
The data in the maps represents illuminance in candela per square meter.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how to interpret cd / m² — wouldn't we need to know the physical area represented by a pixel to use that? Perhaps cd / sr (sr = steradians)? I notice Unreal Engine uses cd / m², but it also requires a reference size (in meters) used for projection onto the scene. It would be helpful to know if other formats (.hdr or .exr) have chosen any existing precedent here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way I think about it is that the output uses the same units meaning that the area represented is only resolved in the final step, usually in the display.
Modern displays knows how many candelas they can output and the same units are used for instance in HDR content.

Does this answer your question?

On a side note, this is one reason why I think it is important to proceed with #2083

The data in the maps represents illuminance in candela per square meter.
Using this information it is possible to calculate the non-direct light contribution to the scene if irradiance coefficients are not included.

Cube faces are defined in the KTX2 format specification, implementations of this extension must follow the KTX2 cubemap specification.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably necessary to say something about the color space. For material textures, the embedded color space can (and typically should) be ignored. For environment cubemaps I'm not sure – given the specified units of cd / m² is this always Linear-sRGB? Or do we need to allow other options?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My assumption was that the colorspace is given by the vkFormat.
According to the KTX V2 spec the dataformat colorspace information is only needed when the format is VK_FORMAT_UNDEFINED

This would give:
VK_FORMAT_R16G16B16_SFLOAT being a linear format in the default colorspace
VK_FORMAT_R8G8B8_SRGB being nonlinear in the default colorspace
The default colorspace being define by the ITU BT 709 color primaries.

Do you think this makes sense?

@shannon
Copy link

shannon commented Jan 6, 2022

I just wanted to provide some feedback on this extension as I recently implemented the specular cubemap and irradiance coefficient portion (I haven't done the localized bit yet).

Schema layout

I do think this extension would be simpler if it reused the root image and texture collections for the cubemap. As it stands now I had to put in extra extension specific loading in place to load the cubemap, which really just includes fetching and parsing the KTX. Normally though, I can just loop through the images / textures and load from there since there is already logic in place for loading ktx2 image types. Now I have to go check if there are khr_lights_environment.lights to load as well. It's not the end of the world but it just seemed a bit unnecessary. I think @donmccurdy's suggestion in the review comments makes sense there.

Irradiance Coefficients

So let me preface by saying my mathematics is not the strongest but I have read through the referenced papers on spherical harmonics and looked through the various code samples included on deriving SH. What I found was that there are slight variations that led to some confusion about what the "correct" approach would be. For example, there was one example that used the texel solid angle for applying weight to the coefficients and another that simply divided by the number of samples rather than keeping a weighted sum. So because of this discrepancy I went looking for other papers and came across Stupid SH Tricks which did a good job of breaking it down but it also used another slight variation in that it used an approximation for the solid angle.

Another major thing to consider is that if the irradianceCoefficients are supplied in the gltf file, they must match what the runtime derivation would be if they weren't. I know this seems obvious but with the above I think this may vary more than expected. In my opinion, because the gltf file should be universal, it kind of forces us to pick one rather than leaving it up to the runtime implementation.

In addition, while looking for implementations I came across bablyonjs's implementation of spherical harmonics. Specifically here. You can see that their coefficients have integrated the reconstructions coefficients directly and have divided by pi to simulate Lambertian (more on this bit below). Should this spec take a similar approach? At least the reconstruction portion might make sense. If so the included irradianceCoefficients need to specify this or they definitely won't match runtime derivation or the shader code.

How irradiance coefficients fit into the GGX/Lambertian BRDF model

Perhaps you can just ignore this section. I think this is just my own misunderstanding of the math because after I read what I wrote here I realized that perhaps the texels ARE each channel which would explain the off by PI if the code examples assume a 4 channel format (i.e. 4 * pi / sum).

I spent an embarrassingly long time trying to figure out why my diffuse portion was so washed out. I am using the same GGX/Lambertian BRDF model from the glTF-Sample-Viewer. I had assumed that I could swap out the Lambertian portion with the reconstructed irradiance. It did not look good.

So I came across another implementation which did the weight a little bit differently here. And I thought, that can't be right. They are summing each color channel, rather than each texel. So I thought it must be off by PI somewhere as that is pretty close to 3 (the number of channels). So I divided the weight by PI and voila it looked perfect. But I could not understand why I needed to do this. Perhaps I missed it but I couldn't find it anywhere in the referenced papers. It wasn't until later that I came across the bablyonjs implementation and I realized that this is required in the ggx/lambertian model.

In any case, my point is that it would be helpful if this extension called this out somewhere or provided reference to a paper which described the relationship between SH and GGX/Lambertian.

Cubemap Texture Formats

I think it's important to note that WebGPU does not support 3 channel texture formats (with the exception of packed formats). It also does not provide a way to write 3 channel buffers to 4 channel formats. This means that you have to pad the buffer before offloading it to the GPU resulting in extra CPU side preprocessing. I don't know if that is a problem for this spec so much as a problem for WebGPU. However, the list of supported texture formats here seemed somewhat arbitrary. It seems like we should at least also support the 4 channel variations.

@rsahlin
Copy link
Author

rsahlin commented Jan 11, 2022

Hi @shannon and thank you so much for your great effort and comments - greatly appreciated!
Just back from christmas vacation hence the late reply :-)

Schema Layout

I totally see your point and have been thinking about where to include the cubemaps.
Yes, the current solution is somewhat deviates from the normal loading procedure.
Let's continue this discussion in the above comments.

Irradiance Coefficients

Yes, there are slight differences 'out there' as to how the coefficients are calculated and used.
One reason I have not (yet) detailed this process further is because glTF is a fileformat and I want to get a good balance in providing the means to include data and mandating exactly how this data is used.
That said, I do believe we should agree on one algorithm to use for calculating the coefficients (because this is the data stored in the file)
I have not seen Stupid SH Tricks at a quick glance it look brilliant!
Maybe there is something there we can point to - or do you have a prefered solution for calculating the coefficients?

I would assume the divide by PI depends on if a solid angle is used when calculating the coefficients or not.
Meaning that the need for division by PI in the shader is coupled to that (algorithm used to calculate the coefficients).

Cubemap Texture Formats

This texture format limitation is nothing strange to me at all.
Desktop discreet GPUs normally don't support 3 channel texture formats (that are not word aligned) - however I do not see the need to include RGBA texture formats - the 4'th channel will simply be unused or combined with some other one channel texture of choice :-)

Or do you see a need to actually provide the alpha channel in the cubemap?

Again - thank you so much for your comments and effort! :-)

@shannon
Copy link

shannon commented Jan 11, 2022

@rsahlin more than happy to contribute where I can.

I don't really have a preference for which algorithm to use. I ended up using the Stupid SH Tricks algorithm because it was the simplest to implement and since it used an approximation I assumed it must be slightly faster.

float f[],s[];
float fWtSum=0;
Foreach(cube map face)
    Foreach(texel)
        float fTmp = 1 + u^2+v^2;
        float fWt = 4/(sqrt(fTmp)*fTmp);
        EvalSHBasis(texel,s);
        f += t(texel)*fWt*s; // vector
        fWtSum += fWt;
f *= 4*Pi/fWtSum; // area of sphere

My understanding is that this is just the same thing but with an approximation of the solid angle. I did replace it with an actual solid angle calculation and it made no visible difference. In both cases I needed to keep the 1/PI or it was very blown out. After more reading on it, I do think it really just needs to be divided by pi to fit into the Lambertian BRDF formula.
See:
https://google.github.io/filament/Filament.md.html#toc5.3.4.6
https://google.github.io/filament/Filament.md.html#toc4.5

In either case, if this spec defined that the full solid angle should be calculated I would just drop the approximation. It only affects the preprocess time and not render time anyways.

I've included the two relevant JS and WGSL shader snippets here for reference:

async deriveIrradianceCoefficients(commandEncoder, cubemap) {
        const start = performance.now();
        //Downsample to SPHERICAL_HARMONICS_SAMPLE_SIZE (128x128 seems to be sufficient)
        //Downsampling also ensures that the data is in rgba32float format regardless of the input ktx format.
        const data  = await this.downsampleCubemap(commandEncoder, cubemap);

        const size = SPHERICAL_HARMONICS_SAMPLE_SIZE;

        const coefficients = [...new Array(9)].map(() => new Float32Array(3));

        const texel = (x, y, f) => {
            const i = ((f * size * size) + ((size * y) + x)) * 4;
            return new Float32Array(data.buffer, data.byteOffset + (i * 4), 3);
        }

        let weightSum = 0;
        for(let f = 0; f < 6; f++) {
            for (let y = 0; y < size; y++) {
                for (let x = 0; x < size; x++) {
                    const u = ((x + 0.5) / size) * 2.0 - 1.0;
                    const v = ((y + 0.5) / size) * 2.0 - 1.0;

                    const temp   = 1.0 + u * u + v * v;
                    const weight = 4.0 / (Math.sqrt(temp) * temp);
                    // const weight = texelSolidAngle(x, y, size, size);
    
                    const [dx, dy, dz] = cubeCoord(u, v, f);
                    const color = texel(x, y, f);

                    for(let c = 0; c < 3; c++) { //this is faster than vec3 methods
                        const value = color[c] * weight;

                        //band 0
                        coefficients[0][c] += value * 0.282095;

                        //band 1
                        coefficients[1][c] += value * 0.488603 * dy;
                        coefficients[2][c] += value * 0.488603 * dz;
                        coefficients[3][c] += value * 0.488603 * dx;

                        //band 2
                        coefficients[4][c] += value * 1.092548 * dx * dy;
                        coefficients[5][c] += value * 1.092548 * dy * dz;
                        coefficients[6][c] += value * 0.315392 * (3.0 * dz * dz - 1.0);
                        coefficients[7][c] += value * 1.092548 * dx * dz;
                        coefficients[8][c] += value * 0.546274 * (dx * dx - dy * dy);  
                    }
                    weightSum += weight;
                }
            }
        }

        for(let c = 0; c < 9; c++) {
            vec3.scale(coefficients[c], coefficients[c], 4 * Math.PI / weightSum);
        }
        
        console.log(`Derived in ${performance.now() - start}ms`, coefficients);
        return coefficients;
}
fn getIrradiance(n: vec3<f32>) -> vec3<f32>{
            let c1 = 0.429043;
            let c2 = 0.511664;
            let c3 = 0.743125;
            let c4 = 0.886227;
            let c5 = 0.247708;

            var L00  = lighting.irradianceCoefficients[0];
            var L1_1 = lighting.irradianceCoefficients[1];
            var L10  = lighting.irradianceCoefficients[2];
            var L11  = lighting.irradianceCoefficients[3];
            var L2_2 = lighting.irradianceCoefficients[4];
            var L2_1 = lighting.irradianceCoefficients[5];
            var L20  = lighting.irradianceCoefficients[6];
            var L21  = lighting.irradianceCoefficients[7];
            var L22  = lighting.irradianceCoefficients[8];

            return (
                c1 * L22 * (n.x * n.x - n.y * n.y) +
                c3 * L20 * n.z * n.z +
                c4 * L00 -
                c5 * L20 +
                2.0 * c1 * (L2_2 * n.x * n.y + L21 * n.x * n.z + L2_1 * n.y * n.z) +
                2.0 * c2 * (L11 * n.x + L1_1 * n.y + L10 * n.z)
            ) / vec3<f32>(${M_PI});
}

For the cubemap texture formats. I don't really feel strongly about this, I just noticed that while trying to get an early estimation of the time it would take to prefilter and derive the coefficients, I found that padding the buffer from 3 channels to 4 channels before sending it to the GPU was a large portion of the time spent. About 200ms of blocking CPU time on a decent desktop. This leads to a lot of jank on startup. I'm still optimizing this so it's possible I can get rid of this in some other way.

@rsahlin
Copy link
Author

rsahlin commented Jan 14, 2022

Great work @shannon - thanks :-)
I will look into this as soon as I have bandwidth - probably within 2-3 weeks.

@rsahlin
Copy link
Author

rsahlin commented Feb 4, 2022

I am still spending my time completing the KHR_displaymapping_pq extension and will hopefully get around to updating this extension within a couple of weeks.

@rsahlin
Copy link
Author

rsahlin commented Feb 17, 2022

I am finally gearing up to get time to spend on this extension - hope to have time in the coming weeks

@shannon
Copy link

shannon commented Nov 16, 2022

I just wanted to share a running implementation of this extension in case anyone is interested.

demo | source code

This is not a finished engine so a couple of things to note:

  • It will use WebGPU on Chrome Desktop and fall back to WebGL2 if not supported. WebGPU is still very much in development so it may break on a browser update. You can force WebGL2 mode if that's the case.
  • You can change the environment by clicking the Lightbulb Icon ->Environment Lighting.
  • You can also change the model by clicking the Cube Icon.
  • One very useful thing for this particular extension is to click the Gear Icon -> Debug -> Light Info Irradiance. This was crucial for my debugging as mentioned in the conversation above.

A good portion of the shader code was ported or referenced from the glTF-Sample-Viewer project.

I wrote a deno script to generate the sample environments from polyhaven HDRs. (Yay for WebGPU in deno!).

I did implement the localized portion of this extension but I don't have any good sample models to really test it fully. I will try to make a better one at some point as I was really just adding the bounding boxes to existing environments. Which is not ideal since the scene doesn't really line up with anything like the scene from the reference paper.

@donmccurdy donmccurdy mentioned this pull request May 21, 2023
@aidinabedi
Copy link
Contributor

Thank you @rsahlin and everyone that's working on this. I would love to see this in glTF and am curious what the status is.

I'm asking because we're looking at proposing something that would fit very nicely with this. It's based on an Unreal feature called HDRI Backdrop, which is a more generalized version of a skybox that is more suited for product visualization.

Regarding KHR_lights_environment, I also have some detailed questions:

  • What is the motivation behind the layer property? Is it common to have multiple cubemaps in the same KTX V2 file? Is it in the case of mipmaps?
  • Have you considered also supporting equirectangular/panorama images as an alternative to KTX cubemaps? This would make the extension less reliant on special tooling and keep the door open for a future HDR format extension (that we're also looking into).

@rsahlin
Copy link
Author

rsahlin commented May 26, 2023

Hi @shannon, @aidinabedi and all - thanks for the interest and support for this extension!
Apologies for not giving it attention - I have been 'tied up' elsewhere for the past year.
However - now is a great time to continue the work :-)
I'm working on an internal project where there is a direct need for this extension, I also know that it is wanted by 3D Commerce.
Starting next week I will dedicate part of my time to KHR_lights_environment

In answer to your questions @aidinabedi:

  • I do not know if it is common to have multiple cubemaps in a KTX V2 file - but the option is there so I feel we must adress it.
    The property defaults to 0 , meaning that you can just ignore it if not used :-)

  • Since glTF is a transmission (delivery) format we usually go for solutions that minimizes the need for target processing.
    Meaning that, processing that can be done prior to deployment (of the glTF) should not be done on the target

  • with the exception if processing is (timewise) costly or complicates the implementation.

Unless there are some very specific (current) needs that cannot be fulfilled by using an equirectangular -> ktx cubemap tool I would prefer to keep it simple (and only support KTX V2 cubemap format) - is this ok with you?

Can you think of any reason why this would not work in the future?

Best regards
/Richard

@aidinabedi
Copy link
Contributor

That's excellent to hear. Will you be at Khronos AWE event next week? Would be a great opportunity to discuss some of these details there as well.

  • I do not know if it is common to have multiple cubemaps in a KTX V2 file - but the option is there so I feel we must adress it.
    The property defaults to 0 , meaning that you can just ignore it if not used :-)

In my limited experience, I haven't encountered multiple cubemaps in a single KTX file (and can't really imagine much use case for it). Additionally, since KHR_texture_basisu itself doesn't provide any layer property I think it would be more consistent to go without it. And this ties into one of the reasons I'm bringing it up. I see a lot of benefit from re-using the existing textures and images arrays defined in the core glTF spec (rather than defining a new distinct set of images and cubemaps that are bound to a specific extension). This would make the extension more compatible with other extensions that also need cubemaps while also avoiding the precedent of each extension defining their own set of cubemaps and images. KHR_texture_basisu allows defining non-2D textures as long as they are not used by any material maps.

  • Since glTF is a transmission (delivery) format we usually go for solutions that minimizes the need for target processing.
    Meaning that, processing that can be done prior to deployment (of the glTF) should not be done on the target

I would like to argue that panoramic images are the de facto transmission/delivery format for environment maps. You'll find endless panoramic HDRIs available online, but few (if any) cubemaps. Additionally, I believe one of the goals of glTF (at least in my view) is to standardize features already implemented - and I have yet to encounter a 3D engine (whether web or native) that doesn't support panoramic images out-of-the-box. For some it's also the only way to input an environment (like Unreal, Sketchfab, and even the glTF sample viewer). On top of that it would additionally removes the extension's reliance on a single image format, which makes it significantly more attractive for engines and platforms that don't prioritize supporting KTX.

Sincerely,
Aidin

@lexaknyazev
Copy link
Member

In my limited experience, I haven't encountered multiple cubemaps in a single KTX file (and can't really imagine much use case for it).

The use case could probably be related to switching environments similar to KHR_materials_variants. The lack of examples is mostly caused by the lack of tools, which is being addressed at the moment.

Additionally, since KHR_texture_basisu itself doesn't provide any layer property I think it would be more consistent to go without it.

The BasisU extension provides a drop-in alternative to the core PNG and JPEG formats so it deliberately does not expose any extra KTX features.

KHR_texture_basisu allows defining non-2D textures as long as they are not used by any material maps.

glTF texture objects are defined as image/sampler tuples, which would have little value over referring to images directly. Nevertheless, this extension should put its images in the global images array as was discussed (but not yet updated) above.

I believe one of the goals of glTF (at least in my view) is to standardize features already implemented

glTF's goal is finding efficient (and sometimes new) ways of standardizing existing features, not necessarily copying popular solutions as-is. Since this extension is in early draft, it's expected that glTF-Sample-Viewer has no support for its design. Besides, using KTX to store equirectangular panoramic images could be evaluated as well.

On top of that it would additionally removes the extension's reliance on a single image format, which makes it significantly more attractive for engines and platforms that don't prioritize supporting KTX.

Relying and standardizing on KTX for image transmission is one of the main 3D Formats WG priorities.

@shannon
Copy link

shannon commented May 29, 2023

I have been trying to optimize my implementation of this a bit to get rid of the jank on startup. Something I am noticing is that the prefilter shader is not cheap and it needs to run faces x roughnessLevel x (ggx+Charlie), which leads to a total of 120 draw calls. On some environments this is pretty fast but on others it can be pretty slow and in webgl2 this leads to blocking the main UI thread (compositor).

Does it make sense to store all of this in the same KTX file ahead of time? With roughness stored as mip levels along with a separate LUT. Before I implemented this extension, I stored the ggx and Charlie as two separate cubemaps already prefiltered and loaded them directly. Perhaps this could be a use case for the layers in KTX.

I'm really just thinking out loud as I am not an expert in this area. My understanding is this would require defining a specific BRDF for this extension. For now I am just going to break up the prefiltering to something like running one roughness level per frame. Instead of all in one frame.

@rsahlin
Copy link
Author

rsahlin commented May 31, 2023

Hi
I will not be at AWE - but I'm happy to take a zoom/teams/hangout call when suitable @aidinabedi

I agree with @lexaknyazev - the reason we do not see any current usage of cubemaps in KTX is due to lack of tools.
Personally I think it makes perfect sense to use KTX V2 as container for cubemaps.

  • yes, sure you can find an abundance of equirectangular images on the web, likely the result of no standardized way of distributing cubemaps.

I think the normal way of releasing extensions is to first expose the functionality then decide to break down into multiple extensions if there is need for it.
At the moment there is really no other feature in glTF that would use the cubemap format as far as I know.
Or do you have something specific in mind @aidinabedi ?

@shannon Thanks for the info - it would be great if we could provide 'implementation notes' for how to efficiently and/or accurately do the prefiltering.
I am also wondering what the visual result is from different methods.
It seems to me that for many usecases you would be hard pressed to see any difference, as the roughness disperses and darkens the reflection.

@rsahlin
Copy link
Author

rsahlin commented Jun 14, 2023

I have created a couple of test models - one with the intention of checking roughness values and how the reflection changes with roughness going from 0 to 1

The spheres are 1m in diameter, there are two rows - one close to the viewer (at Z=0) and one row at a distance (Z = 15m)
This is also to check that mip-level selection is consistent across depth.
The spheres are pure white metal (ie 100% reflective) - just to make it easier to spot problems.
I am using a custom studio cubemap
image

@rsahlin rsahlin changed the title KHR_lights_environment KHR_environment_map Jun 14, 2023
What is specified here is not lights in the sense that are otherwise defined in glTF - for instance KHR_llights_punctual.
One major difference is that the incoming light contribution from an environment lacks distance.
This extension shall not be seen as a replacement for spot or directional lights - it shall be seen as an extension that may provide reflection lookup and some non-directed light contribution.
But not as a replacement for 'real' light extensions - hence the decision to rename.
@rsahlin
Copy link
Author

rsahlin commented Jun 19, 2023

And here is a test model to show what the reflection will be with different IOR values - going from IOR = 1 to the left up to IOR = 3 on the right.
This is done using the KHR_materials_ior extension
I have removed all lights from the scene so that it's only the reflection that is visible.

image

Moved images array to root of glTF.
Added properties for cubemap intensity, irradiance factor and ior.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants