Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 31 additions & 12 deletions Assets/Scripts/SparkGltfastAddon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,15 @@
using UnityEngine;

// glTFast import add-on that overrides the default JPEG/PNG image loading, so models loaded at
// runtime (ObjectMode's useRuntimeLoad path) get their textures created through our pipeline —
// later, Spark-encoded on device.
// runtime get their textures created through our pipeline
//
// We implement IDefaultImageFormatLoader (not plain ITextureImageLoader): it's glTFast's hook for
// *overriding the core PNG/JPEG decode*, selected per image via IsAbleToLoad(ImageFormat). Plain
// ITextureImageLoader is for extension formats (WebP/KTX) that redirect the image source — not us.
// ITextureImageLoader is for extension formats (WebP/KTX) that redirect the image source.
//
// Registration is global (RuntimeInitializeOnLoadMethod), so it applies to every runtime
// GltfImport.Load. The editor ScriptedImporter doesn't run RuntimeInitializeOnLoad callbacks, so
// edit-time glTF imports are unaffected.
//
// STEP 4 (this version): decode the JPEG/PNG, then Spark-encode it on device. Colorspace comes
// from glTFast's per-slot `linear` flag (srgb = !linear). The channel format is fixed RGB for now;
// once gltFast is extended to thread per-texture channel usage (step 3) the addon will pick
// RG/R/RGBA per slot. Fixed RGB is safe for the demo's models: their textures are opaque, and
// normal maps still render correctly because the shader reconstructs Z from X,Y regardless of B.
static class SparkGltfastAddon
{
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
Expand Down Expand Up @@ -76,22 +69,35 @@ static bool IsJpeg(System.ReadOnlySpan<byte> d) =>
static bool IsPng(System.ReadOnlySpan<byte> d) =>
d.Length >= 8 && d[0] == 0x89 && d[1] == 0x50 && d[2] == 0x4E && d[3] == 0x47;

// Required channel-unaware overload — forward to the channel-aware one with all channels.
public Task<ImageResult> LoadImage(
NativeArray<byte>.ReadOnly data,
bool linear,
bool readable,
bool generateMipMaps,
CancellationToken cancellationToken)
=> LoadImage(data, linear, readable, generateMipMaps, cancellationToken, 0xF);

// Channel-aware override — picks the Spark format from glTFast's per-image channel mask.
public Task<ImageResult> LoadImage(
NativeArray<byte>.ReadOnly data,
bool linear,
bool readable,
bool generateMipMaps,
CancellationToken cancellationToken,
int channelMask)
{
// Decode the compressed bytes into a temporary GPU texture for Spark to sample. `linear`
// (from the texture's slot usage) sets the colorspace; LoadImage uploads + mips it.
var decoded = new Texture2D(2, 2, TextureFormat.RGBA32, generateMipMaps, linear);
decoded.LoadImage(data.ToArray(), markNonReadable: true);

// Channel-minimal Spark format from glTFast's per-image channel mask.
var format = FormatForChannels(channelMask);
Debug.Log($"[SparkAddon] LoadImage: decoded {data.Length}B → {decoded.width}x{decoded.height} " +
$"{decoded.graphicsFormat}; Spark-encoding (linear={linear}, srgb={!linear}, mips={generateMipMaps})…");
$"{decoded.graphicsFormat}; Spark-encoding {format} (linear={linear}, mask=0x{channelMask:X}, mips={generateMipMaps})…");

// Spark-encode on device. Fixed RGB until gltFast threads per-texture channel usage.
var compressed = Spark.EncodeTexture(decoded, SparkFormat.RGB, srgb: !linear, mips: generateMipMaps);
var compressed = Spark.EncodeTexture(decoded, format, srgb: !linear, mips: generateMipMaps);

if (compressed != null)
{
Expand All @@ -104,4 +110,17 @@ public Task<ImageResult> LoadImage(
Debug.LogWarning($"[SparkAddon] ✗ Spark.EncodeTexture returned null; using uncompressed {decoded.graphicsFormat}");
return Task.FromResult(new ImageResult(decoded));
}

// Smallest Spark format whose contiguous channel set covers the mask's highest set channel.
// e.g. normal R|G → RG, occlusion R → R, metallic-roughness G|B → RGB. A mask of 0 (image
// not reached by glTFast's slot pass, e.g. an extension-only texture) falls back to RGBA
// so we never under-allocate channels.
static SparkFormat FormatForChannels(int mask)
{
if ((mask & 0x8) != 0) return SparkFormat.RGBA; // A
if ((mask & 0x4) != 0) return SparkFormat.RGB; // B
if ((mask & 0x2) != 0) return SparkFormat.RG; // G
if ((mask & 0x1) != 0) return SparkFormat.R; // R
return SparkFormat.RGBA; // unknown → safe superset
}
}
2 changes: 1 addition & 1 deletion Packages/manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"dependencies": {
"com.unity.cloud.gltfast": "6.19.0",
"com.atteneder.gltfast": "file:../../gltFast",
"com.unity.collab-proxy": "2.11.4",
"com.unity.feature.development": "1.0.2",
"com.unity.memoryprofiler": "1.1.12",
Expand Down
25 changes: 12 additions & 13 deletions Packages/packages-lock.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
{
"dependencies": {
"com.atteneder.gltfast": {
"version": "file:../../gltFast",
"depth": 0,
"source": "local",
"dependencies": {
"com.unity.modules.jsonserialize": "1.0.0",
"com.unity.modules.unitywebrequest": "1.0.0",
"com.unity.mathematics": "1.3.3",
"com.unity.burst": "1.8.29",
"com.unity.collections": "2.6.6"
}
},
"com.ludicon.spark": {
"version": "file:com.ludicon.spark",
"depth": 0,
Expand All @@ -16,19 +28,6 @@
},
"url": "https://packages.unity.com"
},
"com.unity.cloud.gltfast": {
"version": "6.19.0",
"depth": 0,
"source": "registry",
"dependencies": {
"com.unity.burst": "1.8.29",
"com.unity.collections": "2.6.6",
"com.unity.mathematics": "1.3.3",
"com.unity.modules.jsonserialize": "1.0.0",
"com.unity.modules.unitywebrequest": "1.0.0"
},
"url": "https://packages.unity.com"
},
"com.unity.collab-proxy": {
"version": "2.11.4",
"depth": 0,
Expand Down