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

feat(textures): Support compressed textures, add example #2333

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
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
272 changes: 272 additions & 0 deletions examples/tutorials/hello-texture-compressed/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
// luma.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import type {
Device,
NumberArray,
TextureFormat,
TypedArray,
VariableShaderType
} from '@luma.gl/core';
import {UniformStore} from '@luma.gl/core';
import type {AnimationProps} from '@luma.gl/engine';
import {AnimationLoopTemplate, Model, CubeGeometry} from '@luma.gl/engine';
import {Matrix4} from '@math.gl/core';
import {
read as readKTX2,
KHR_SUPERCOMPRESSION_NONE,
VKFormat,
VK_FORMAT_R8G8B8A8_SRGB,
VK_FORMAT_R8G8B8A8_UNORM,
VK_FORMAT_R16G16B16A16_SFLOAT,
VK_FORMAT_R32G32B32A32_SFLOAT,
VK_FORMAT_ASTC_4x4_SRGB_BLOCK,
VK_FORMAT_ETC2_R8G8B8_SRGB_BLOCK,
VK_FORMAT_ETC2_R8G8B8A8_SRGB_BLOCK,
VK_FORMAT_BC1_RGB_SRGB_BLOCK,
VK_FORMAT_BC3_SRGB_BLOCK,
VK_FORMAT_BC5_UNORM_BLOCK,
VK_FORMAT_BC7_SRGB_BLOCK,
KTX2Container
} from 'ktx-parse';

export const title = 'Texture Compressed';
export const description = 'Shows rendering a compressed texture.';

const WGSL_SHADER = /* WGSL */ `\
struct Uniforms {
modelViewProjectionMatrix : mat4x4<f32>,
};

@group(0) @binding(0) var<uniform> app : Uniforms;
@group(0) @binding(1) var uTexture : texture_2d<f32>;
@group(0) @binding(2) var uTextureSampler : sampler;

struct VertexInputs {
// CUBE GEOMETRY
@location(0) positions : vec4<f32>,
@location(1) texCoords : vec2<f32>,
@location(2) colors : vec3<f32>,
};

struct FragmentInputs {
@builtin(position) Position : vec4<f32>,
@location(0) fragUV : vec2<f32>,
@location(1) fragPosition: vec4<f32>,
}

@vertex
fn vertexMain(inputs: VertexInputs) -> FragmentInputs {
var outputs : FragmentInputs;
outputs.Position = app.modelViewProjectionMatrix * inputs.positions;
outputs.fragUV = inputs.texCoords;
outputs.fragPosition = 0.5 * (inputs.positions + vec4(1.0, 1.0, 1.0, 1.0));
return outputs;
}

@fragment
fn fragmentMain(inputs: FragmentInputs) -> @location(0) vec4<f32> {
// return inputs.fragPosition;
return textureSample(uTexture, uTextureSampler, inputs.fragUV);
// TODO: apply sRGB OETF
}
`;

// GLSL

export const VS_GLSL = /* glsl */ `\
#version 300 es
#define SHADER_NAME cube-vs

uniform appUniforms {
mat4 modelViewProjectionMatrix;
} app;

layout(location=0) in vec3 positions;
layout(location=1) in vec2 texCoords;

out vec2 fragUV;
out vec4 fragPosition;

void main() {
gl_Position = app.modelViewProjectionMatrix * vec4(positions, 1.0);
fragUV = texCoords;
fragPosition = 0.5 * (vec4(positions, 1.) + vec4(1., 1., 1., 1.));
}
`;

export const FS_GLSL = /* glsl */ `\
#version 300 es
#define SHADER_NAME cube-fs
precision highp float;

uniform sampler2D uTexture;

uniform appUniforms {
mat4 modelViewProjectionMatrix;
} app;

in vec2 fragUV;
in vec4 fragPosition;

layout (location=0) out vec4 fragColor;

vec4 sRGBTransferOETF( in vec4 value ) {
return vec4( mix( pow( value.rgb, vec3( 0.41666 ) ) * 1.055 - vec3( 0.055 ), value.rgb * 12.92, vec3( lessThanEqual( value.rgb, vec3( 0.0031308 ) ) ) ), value.a );
}

void main() {
fragColor = fragPosition;
fragColor = texture(uTexture, vec2(fragUV.x, 1.0 - fragUV.y));
fragColor = sRGBTransferOETF( fragColor );
}
`;

type AppUniforms = {
mvpMatrix: NumberArray;
};

const app: {uniformTypes: Record<keyof AppUniforms, VariableShaderType>} = {
uniformTypes: {
mvpMatrix: 'mat4x4<f32>'
}
};

const eyePosition = [0, 0, -4];

export default class AppAnimationLoopTemplate extends AnimationLoopTemplate {
static info = `\
<p>
Drawing a compressed texture
</p>

<p>
Rendered using the luma.gl <code>Model</code>, <code>CubeGeometry</code> and <code>AnimationLoop</code> classes.
</p>
`;

mvpMatrix = new Matrix4();
viewMatrix = new Matrix4().lookAt({eye: eyePosition}).rotateZ(Math.PI);
model: Model | null = null;
device: Device;

uniformStore = new UniformStore<{app: AppUniforms}>({app});

constructor({device}: AnimationProps) {
super();
this.device = device;
this.initialize();
}

async initialize() {
const device = this.device;

// failing: 2d_etc2, 2d_rgba16_linear, 2d_rgba32_linear
const buffer = await fetch('2d_astc4x4.ktx2').then(res => res.arrayBuffer());
const container = readKTX2(new Uint8Array(buffer));

if (container.supercompressionScheme !== KHR_SUPERCOMPRESSION_NONE) {
throw new Error(`Supercompression not implemented: ${container.supercompressionScheme}`);
}

const texture = device.createTexture({
data: getData(container),
format: getFormat(container.vkFormat),
mipLevels: 1,
width: container.pixelWidth,
height: container.pixelHeight,
sampler: device.createSampler({
minFilter: 'nearest',
magFilter: 'nearest',
mipmapFilter: 'none'
})
});

const geometry = new CubeGeometry({indices: false});

this.model = new Model(device, {
source: WGSL_SHADER,
vs: VS_GLSL,
fs: FS_GLSL,
geometry,
bindings: {
app: this.uniformStore.getManagedUniformBuffer(device, 'app'),
uTexture: texture
},
parameters: {
depthWriteEnabled: true,
depthCompare: 'less-equal'
}
});
}

onFinalize() {
this.model?.destroy();
this.uniformStore.destroy();
}

onRender({device, aspect, tick}: AnimationProps) {
this.mvpMatrix.perspective({fovy: Math.PI / 3, aspect}).multiplyRight(this.viewMatrix);
this.uniformStore.setUniforms({app: {mvpMatrix: this.mvpMatrix}});

const renderPass = device.beginRenderPass({clearColor: [0, 0, 0, 1], clearDepth: 1});
this.model?.draw(renderPass);
renderPass.end();
}
}

function getData(container: KTX2Container): TypedArray {
const data: TypedArray = container.levels[0].levelData;

switch (container.vkFormat) {
case VK_FORMAT_R32G32B32A32_SFLOAT:
return new Float32Array(data.buffer);

case VK_FORMAT_R16G16B16A16_SFLOAT:
return new Uint16Array(data.buffer);

default:
return data;
}
}

function getFormat(vkFormat: VKFormat): TextureFormat {
switch (vkFormat) {
case VK_FORMAT_R8G8B8A8_UNORM:
return 'rgba8unorm';

case VK_FORMAT_R8G8B8A8_SRGB:
return 'rgba8unorm-srgb';

case VK_FORMAT_R16G16B16A16_SFLOAT:
return 'rgba16float';

case VK_FORMAT_R32G32B32A32_SFLOAT:
return 'rgba32float';

case VK_FORMAT_ASTC_4x4_SRGB_BLOCK:
return 'astc-4x4-unorm-srgb';

case VK_FORMAT_ETC2_R8G8B8_SRGB_BLOCK:
return 'etc2-rgb8unorm-srgb';

case VK_FORMAT_ETC2_R8G8B8A8_SRGB_BLOCK:
return 'etc2-rgba8unorm-srgb';

case VK_FORMAT_BC1_RGB_SRGB_BLOCK:
return 'bc1-rgb-unorm-srgb-webgl';

case VK_FORMAT_BC3_SRGB_BLOCK:
return 'bc3-rgba-unorm-srgb';

case VK_FORMAT_BC5_UNORM_BLOCK:
return 'bc5-rg-unorm';

case VK_FORMAT_BC7_SRGB_BLOCK:
return 'bc7-rgba-unorm-srgb';

default:
throw new Error(`Unknown vkFormat, ${vkFormat}`);
}
}
11 changes: 11 additions & 0 deletions examples/tutorials/hello-texture-compressed/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!doctype html>
<script type="module">
import {makeAnimationLoop} from '@luma.gl/engine';
import {webgpuAdapter} from '@luma.gl/webgpu';
import {webgl2Adapter} from '@luma.gl/webgl';
import AnimationLoopTemplate from './app.ts';

const animationLoop = makeAnimationLoop(AnimationLoopTemplate, {adapters: [webgl2Adapter]});
animationLoop.start();
</script>
<body style="margin: 0"></body>
23 changes: 23 additions & 0 deletions examples/tutorials/hello-texture-compressed/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "luma.gl-examples-texture-compressed",
"version": "9.1.0-alpha.6",
"private": true,
"scripts": {
"start": "vite",
"build": "tsc && vite build",
"serve": "vite preview"
},
"dependencies": {
"@luma.gl/core": "9.2.0-alpha.1",
"@luma.gl/engine": "9.2.0-alpha.1",
"@luma.gl/shadertools": "9.2.0-alpha.1",
"@luma.gl/webgl": "9.2.0-alpha.1",
"@luma.gl/webgpu": "9.2.0-alpha.1",
"@math.gl/core": "^4.1.0",
"ktx-parse": "^0.7.1"
},
"devDependencies": {
"typescript": "^5.5.0",
"vite": "^5.0.0"
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
18 changes: 18 additions & 0 deletions examples/tutorials/hello-texture-compressed/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import fs from 'fs/promises';
import {defineConfig} from 'vite';

/** @see https://vitejs.dev/config/ */
export default defineConfig(async () => ({
resolve: {alias: await getAliases('@luma.gl', `${__dirname}/../../..`)},
server: {open: true}
}));

/** Run against local source */
const getAliases = async (frameworkName, frameworkRootDir) => {
const modules = await fs.readdir(`${frameworkRootDir}/modules`);
const aliases = {};
for (const module of modules) {
aliases[`${frameworkName}/${module}`] = `${frameworkRootDir}/modules/${module}/src`;
}
return aliases;
};
8 changes: 5 additions & 3 deletions modules/core/src/adapter/resources/texture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {Sampler, SamplerProps} from './sampler';
import {ExternalImage} from '../../image-utils/image-types';
import {log} from '../../utils/log';

type RequiredExcept<T, K extends keyof T> = Pick<T, K> & Required<Omit<T, K>>;

/** Options for Texture.copyExternalImage */
export type CopyExternalImageOptions = {
/** Image */
Expand Down Expand Up @@ -262,16 +264,16 @@ export abstract class Texture extends Resource<TextureProps> {
}
}

_normalizeCopyImageDataOptions(options_: CopyImageDataOptions): Required<CopyImageDataOptions> {
_normalizeCopyImageDataOptions(
options_: CopyImageDataOptions
): RequiredExcept<CopyImageDataOptions, 'bytesPerRow' | 'rowsPerImage'> {
const {width, height, depth} = this;
const options = {...Texture.defaultCopyDataOptions, width, height, depth, ...options_};

const info = this.device.getTextureFormatInfo(this.format);
if (!options_.bytesPerRow && !info.bytesPerPixel) {
throw new Error(`bytesPerRow must be provided for texture format ${this.format}`);
}
options.bytesPerRow = options_.bytesPerRow || width * (info.bytesPerPixel || 4);
options.rowsPerImage = options_.rowsPerImage || height;

// WebGL will error if we try to copy outside the bounds of the texture
// options.width = Math.min(options.width, this.width - options.x);
Expand Down
12 changes: 9 additions & 3 deletions modules/webgl/src/adapter/converters/webgl-texture-table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export const WEBGL_TEXTURE_FORMATS: Record<TextureFormat, WebGLFormatInfo> = {
'rgb8unorm-webgl': {gl: GL.RGB8},
'rgb8snorm-webgl': {gl: GL.RGB8_SNORM},

// 32-bit formats
// 32-bit formats
'rgba8unorm': {gl: GL.RGBA8},
'rgba8unorm-srgb': {gl: GL.SRGB8_ALPHA8},
'rgba8snorm': {gl: GL.RGBA8_SNORM},
Expand All @@ -149,7 +149,7 @@ export const WEBGL_TEXTURE_FORMATS: Record<TextureFormat, WebGLFormatInfo> = {
'rg11b10ufloat': {gl: GL.R11F_G11F_B10F, rb: true},
'rgb10a2unorm': {gl: GL.RGB10_A2, rb: true},
'rgb10a2uint': {gl: GL.RGB10_A2UI, rb: true},

// 48-bit formats
'rgb16unorm-webgl': {gl: GL.RGB16_EXT}, // rgb not renderable
'rgb16snorm-webgl': {gl: GL.RGB16_SNORM_EXT}, // rgb not renderable
Expand All @@ -166,7 +166,7 @@ export const WEBGL_TEXTURE_FORMATS: Record<TextureFormat, WebGLFormatInfo> = {

// 96-bit formats (deprecated!)
'rgb32float-webgl': {gl: GL.RGB32F, x: EXT_color_buffer_float, dataFormat: GL.RGB, types: [GL.FLOAT]},

// 128-bit formats
'rgba32uint': {gl: GL.RGBA32UI, rb: true},
'rgba32sint': {gl: GL.RGBA32I, rb: true},
Expand Down Expand Up @@ -333,6 +333,12 @@ export function getTextureFormatWebGL(format: TextureFormat): {
const formatData = WEBGL_TEXTURE_FORMATS[format];
const webglFormat = convertTextureFormatToGL(format);
const decoded = getTextureFormatInfo(format);

if (decoded.compressed) {
// TODO: Unclear whether this is always valid, this may be why ETC2 RGBA8 fails.
formatData.dataFormat = webglFormat as GLTexelDataFormat;
}

return {
internalFormat: webglFormat,
format:
Expand Down
Loading
Loading