Skip to content

Commit b13d1b1

Browse files
authored
KTX2Exporter: Fix metadata, add example (#29541)
1 parent 2513543 commit b13d1b1

File tree

4 files changed

+239
-12
lines changed

4 files changed

+239
-12
lines changed

examples/files.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,7 @@
503503
"misc_exporter_stl",
504504
"misc_exporter_usdz",
505505
"misc_exporter_exr",
506+
"misc_exporter_ktx2",
506507
"misc_lookat"
507508
],
508509
"css2d": [

examples/jsm/exporters/KTX2Exporter.js

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
ColorManagement,
23
FloatType,
34
HalfFloatType,
45
UnsignedByteType,
@@ -10,6 +11,7 @@ import {
1011
NoColorSpace,
1112
LinearSRGBColorSpace,
1213
SRGBColorSpace,
14+
SRGBTransfer,
1315
DataTexture,
1416
REVISION,
1517
} from 'three';
@@ -43,6 +45,13 @@ import {
4345
VK_FORMAT_R8G8B8A8_UNORM,
4446
} from '../libs/ktx-parse.module.js';
4547

48+
/**
49+
* References:
50+
* - https://github.khronos.org/KTX-Specification/ktxspec.v2.html
51+
* - https://registry.khronos.org/DataFormat/specs/1.3/dataformat.1.3.html
52+
* - https://github.com/donmccurdy/KTX-Parse
53+
*/
54+
4655
const VK_FORMAT_MAP = {
4756

4857
[ RGBAFormat ]: {
@@ -95,14 +104,23 @@ const VK_FORMAT_MAP = {
95104

96105
};
97106

98-
const KHR_DF_CHANNEL_MAP = {
107+
const KHR_DF_CHANNEL_MAP = [
99108

100-
0: KHR_DF_CHANNEL_RGBSDA_RED,
101-
1: KHR_DF_CHANNEL_RGBSDA_GREEN,
102-
2: KHR_DF_CHANNEL_RGBSDA_BLUE,
103-
3: KHR_DF_CHANNEL_RGBSDA_ALPHA,
109+
KHR_DF_CHANNEL_RGBSDA_RED,
110+
KHR_DF_CHANNEL_RGBSDA_GREEN,
111+
KHR_DF_CHANNEL_RGBSDA_BLUE,
112+
KHR_DF_CHANNEL_RGBSDA_ALPHA,
104113

105-
};
114+
];
115+
116+
// TODO: sampleLower and sampleUpper may change based on color space.
117+
const KHR_DF_CHANNEL_SAMPLE_LOWER_UPPER = {
118+
119+
[ FloatType ]: [ 0xbf800000, 0x3f800000 ],
120+
[ HalfFloatType ]: [ 0xbf800000, 0x3f800000 ],
121+
[ UnsignedByteType ]: [ 0, 255 ],
122+
123+
}
106124

107125
const ERROR_INPUT = 'THREE.KTX2Exporter: Supported inputs are DataTexture, Data3DTexture, or WebGLRenderer and WebGLRenderTarget.';
108126
const ERROR_FORMAT = 'THREE.KTX2Exporter: Supported formats are RGBAFormat, RGFormat, or RedFormat.';
@@ -172,7 +190,7 @@ export class KTX2Exporter {
172190
basicDesc.colorPrimaries = texture.colorSpace === NoColorSpace
173191
? KHR_DF_PRIMARIES_UNSPECIFIED
174192
: KHR_DF_PRIMARIES_BT709;
175-
basicDesc.transferFunction = texture.colorSpace === SRGBColorSpace
193+
basicDesc.transferFunction = ColorManagement.getTransfer( texture.colorSpace ) === SRGBTransfer
176194
? KHR_DF_TRANSFER_SRGB
177195
: KHR_DF_TRANSFER_LINEAR;
178196

@@ -188,7 +206,8 @@ export class KTX2Exporter {
188206

189207
let channelType = KHR_DF_CHANNEL_MAP[ i ];
190208

191-
if ( texture.colorSpace === LinearSRGBColorSpace || texture.colorSpace === NoColorSpace ) {
209+
// Assign KHR_DF_SAMPLE_DATATYPE_LINEAR if the channel is linear _and_ differs from the transfer function.
210+
if ( channelType === KHR_DF_CHANNEL_RGBSDA_ALPHA && basicDesc.transferFunction !== KHR_DF_TRANSFER_LINEAR ) {
192211

193212
channelType |= KHR_DF_SAMPLE_DATATYPE_LINEAR;
194213

@@ -204,11 +223,11 @@ export class KTX2Exporter {
204223
basicDesc.samples.push( {
205224

206225
channelType: channelType,
207-
bitOffset: i * array.BYTES_PER_ELEMENT,
226+
bitOffset: i * array.BYTES_PER_ELEMENT * 8,
208227
bitLength: array.BYTES_PER_ELEMENT * 8 - 1,
209228
samplePosition: [ 0, 0, 0, 0 ],
210-
sampleLower: texture.type === UnsignedByteType ? 0 : - 1,
211-
sampleUpper: texture.type === UnsignedByteType ? 255 : 1,
229+
sampleLower: KHR_DF_CHANNEL_SAMPLE_LOWER_UPPER[ texture.type ][ 0 ],
230+
sampleUpper: KHR_DF_CHANNEL_SAMPLE_LOWER_UPPER[ texture.type ][ 1 ],
212231

213232
} );
214233

@@ -269,7 +288,11 @@ async function toDataTexture( renderer, rtt ) {
269288

270289
}
271290

272-
return new DataTexture( view, rtt.width, rtt.height, rtt.texture.format, rtt.texture.type );
291+
const texture = new DataTexture( view, rtt.width, rtt.height, rtt.texture.format, rtt.texture.type );
292+
293+
texture.colorSpace = rtt.texture.colorSpace;
294+
295+
return texture;
273296

274297
}
275298

examples/misc_exporter_ktx2.html

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<title>three.js webgl - exporter - ktx2</title>
5+
<meta charset="utf-8">
6+
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
7+
<link type="text/css" rel="stylesheet" href="main.css">
8+
</head>
9+
<body>
10+
<div id="info">
11+
<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgl - exporter - ktx2
12+
</div>
13+
14+
<script type="importmap">
15+
{
16+
"imports": {
17+
"three": "../build/three.module.js",
18+
"three/addons/": "./jsm/"
19+
}
20+
}
21+
</script>
22+
23+
<script type="module">
24+
25+
import * as THREE from 'three';
26+
27+
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
28+
import { KTX2Exporter } from 'three/addons/exporters/KTX2Exporter.js';
29+
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
30+
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
31+
32+
let scene, camera, renderer, exporter, mesh, controls, renderTarget, dataTexture;
33+
34+
const params = {
35+
target: 'pmrem',
36+
export: exportFile
37+
};
38+
39+
init();
40+
41+
function init() {
42+
43+
renderer = new THREE.WebGLRenderer( { antialias: true } );
44+
renderer.toneMapping = THREE.AgXToneMapping;
45+
renderer.setPixelRatio( window.devicePixelRatio );
46+
renderer.setSize( window.innerWidth, window.innerHeight );
47+
renderer.setAnimationLoop( animate );
48+
document.body.appendChild( renderer.domElement );
49+
50+
//
51+
52+
camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.1, 100 );
53+
camera.position.set( 10, 0, 0 );
54+
55+
scene = new THREE.Scene();
56+
57+
exporter = new KTX2Exporter();
58+
const rgbeloader = new RGBELoader();
59+
60+
//
61+
62+
const pmremGenerator = new THREE.PMREMGenerator( renderer );
63+
pmremGenerator.compileEquirectangularShader();
64+
65+
rgbeloader.load( 'textures/equirectangular/venice_sunset_1k.hdr', function ( texture ) {
66+
67+
texture.mapping = THREE.EquirectangularReflectionMapping;
68+
69+
renderTarget = pmremGenerator.fromEquirectangular( texture );
70+
scene.background = renderTarget.texture;
71+
72+
} );
73+
74+
createDataTexture();
75+
76+
//
77+
78+
controls = new OrbitControls( camera, renderer.domElement );
79+
controls.enableDamping = true;
80+
controls.rotateSpeed = - 0.25; // negative, to track mouse pointer
81+
82+
//
83+
84+
window.addEventListener( 'resize', onWindowResize );
85+
86+
const gui = new GUI();
87+
88+
gui.add( params, 'target' ).options( [ 'pmrem', 'data-texture' ] ).onChange( swapScene );
89+
gui.add( params, 'export' ).name( 'Export KTX2' );
90+
gui.open();
91+
92+
}
93+
94+
function onWindowResize() {
95+
96+
camera.aspect = window.innerWidth / window.innerHeight;
97+
camera.updateProjectionMatrix();
98+
99+
renderer.setSize( window.innerWidth, window.innerHeight );
100+
101+
}
102+
103+
function animate() {
104+
105+
controls.update();
106+
renderer.render( scene, camera );
107+
108+
}
109+
110+
function createDataTexture() {
111+
112+
const normal = new THREE.Vector3();
113+
const coord = new THREE.Vector2();
114+
const size = 800, radius = 320, factor = Math.PI * 0.5 / radius;
115+
const data = new Float32Array( 4 * size * size );
116+
117+
for ( let i = 0; i < size; i ++ ) {
118+
119+
for ( let j = 0; j < size; j ++ ) {
120+
121+
const idx = i * size * 4 + j * 4;
122+
coord.set( j, i ).subScalar( size / 2 );
123+
124+
if ( coord.length() < radius )
125+
normal.set(
126+
Math.sin( coord.x * factor ),
127+
Math.sin( coord.y * factor ),
128+
Math.cos( coord.x * factor )
129+
);
130+
else
131+
normal.set( 0, 0, 1 );
132+
133+
data[ idx + 0 ] = .5 + .5 * normal.x;
134+
data[ idx + 1 ] = .5 + .5 * normal.y;
135+
data[ idx + 2 ] = .5 + .5 * normal.z;
136+
data[ idx + 3 ] = 1.;
137+
138+
}
139+
140+
}
141+
142+
dataTexture = new THREE.DataTexture( data, size, size, THREE.RGBAFormat, THREE.FloatType );
143+
dataTexture.needsUpdate = true;
144+
145+
const material = new THREE.MeshBasicMaterial( { map: dataTexture } );
146+
const quad = new THREE.PlaneGeometry( 50, 50 );
147+
mesh = new THREE.Mesh( quad, material );
148+
mesh.visible = false;
149+
150+
scene.add( mesh );
151+
152+
}
153+
154+
function swapScene() {
155+
156+
if ( params.target == 'pmrem' ) {
157+
158+
camera.position.set( 10, 0, 0 );
159+
controls.enabled = true;
160+
scene.background = renderTarget.texture;
161+
mesh.visible = false;
162+
renderer.toneMapping = THREE.AgXToneMapping;
163+
164+
} else {
165+
166+
camera.position.set( 0, 0, 70 );
167+
controls.enabled = false;
168+
scene.background = new THREE.Color( 0, 0, 0 );
169+
mesh.visible = true;
170+
renderer.toneMapping = THREE.NoToneMapping;
171+
172+
}
173+
174+
}
175+
176+
async function exportFile() {
177+
178+
let result;
179+
180+
if ( params.target == 'pmrem' )
181+
result = await exporter.parse( renderer, renderTarget );
182+
else
183+
result = await exporter.parse( dataTexture );
184+
185+
saveArrayBuffer( result, params.target + '.ktx2' );
186+
187+
}
188+
189+
function saveArrayBuffer( buffer, filename ) {
190+
191+
const blob = new Blob( [ buffer ], { type: 'image/ktx2' } );
192+
const link = document.createElement( 'a' );
193+
194+
link.href = URL.createObjectURL( blob );
195+
link.download = filename;
196+
link.click();
197+
198+
}
199+
200+
</script>
201+
202+
</body>
203+
</html>
32.7 KB
Loading

0 commit comments

Comments
 (0)