Skip to content

Commit 706e9cf

Browse files
committed
allow exporting compressed meshes and textures (e.g. draco + ktx2)
added coffemat.etc1s.glb and etc1s+draco, etc1s+meshopt to test compressed formats only use one temp render context, clean up renderer after writing file, make metalnessMap and roughnessMap readable refactor: move to TextureUtils class and use that in GLTFExporter, respect sRGB vs. Linear, cache some generated objects cleanup simplify modifiedMap access fix formatting for TextureUtils dispose of temporary renderer remove duplicate switch entries
1 parent c0f9764 commit 706e9cf

File tree

6 files changed

+170
-7
lines changed

6 files changed

+170
-7
lines changed

examples/jsm/exporters/GLTFExporter.js

+30-5
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ import {
2020
RepeatWrapping,
2121
Scene,
2222
Source,
23-
SRGBColorSpace,
23+
sRGBEncoding,
24+
CompressedTexture,
2425
Vector3
2526
} from 'three';
27+
import { decompress } from './../utils/TextureUtils.js';
2628

2729

2830
/**
@@ -827,8 +829,20 @@ class GLTFWriter {
827829

828830
console.warn( 'THREE.GLTFExporter: Merged metalnessMap and roughnessMap textures.' );
829831

830-
const metalness = metalnessMap ? metalnessMap.image : null;
831-
const roughness = roughnessMap ? roughnessMap.image : null;
832+
if ( metalnessMap instanceof CompressedTexture ) {
833+
834+
metalnessMap = decompress( metalnessMap );
835+
836+
}
837+
838+
if ( roughnessMap instanceof CompressedTexture ) {
839+
840+
roughnessMap = decompress( roughnessMap );
841+
842+
}
843+
844+
const metalness = metalnessMap?.image;
845+
const roughness = roughnessMap?.image;
832846

833847
const width = Math.max( metalness ? metalness.width : 0, roughness ? roughness.width : 0 );
834848
const height = Math.max( metalness ? metalness.height : 0, roughness ? roughness.height : 0 );
@@ -1146,7 +1160,7 @@ class GLTFWriter {
11461160

11471161
} else {
11481162

1149-
throw new Error( 'THREE.GLTFExporter: Unsupported bufferAttribute component type.' );
1163+
throw new Error( 'THREE.GLTFExporter: Unsupported bufferAttribute component type: ' + attribute.array.constructor );
11501164

11511165
}
11521166

@@ -1236,7 +1250,7 @@ class GLTFWriter {
12361250

12371251
if ( format !== RGBAFormat ) {
12381252

1239-
console.error( 'GLTFExporter: Only RGBAFormat is supported.' );
1253+
console.error( 'GLTFExporter: Only RGBAFormat is supported.', image );
12401254

12411255
}
12421256

@@ -1344,13 +1358,24 @@ class GLTFWriter {
13441358
*/
13451359
processTexture( map ) {
13461360

1361+
const writer = this;
1362+
const options = writer.options;
13471363
const cache = this.cache;
13481364
const json = this.json;
13491365

13501366
if ( cache.textures.has( map ) ) return cache.textures.get( map );
13511367

13521368
if ( ! json.textures ) json.textures = [];
13531369

1370+
// make non-readable textures (e.g. CompressedTexture) readable by blitting them into a new texture
1371+
if ( map instanceof CompressedTexture ) {
1372+
1373+
map = decompress( map, options.maxTextureSize );
1374+
// remove from cache, as the underlaying canvas is always the same between decompressed textures
1375+
cache.images.delete( map.image );
1376+
1377+
}
1378+
13541379
let mimeType = map.userData.mimeType;
13551380

13561381
if ( mimeType === 'image/webp' ) mimeType = 'image/png';

examples/jsm/utils/TextureUtils.js

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import {
2+
PlaneGeometry,
3+
ShaderMaterial,
4+
Uniform,
5+
Mesh,
6+
PerspectiveCamera,
7+
Scene,
8+
WebGLRenderer,
9+
Texture,
10+
sRGBEncoding
11+
} from 'three';
12+
13+
let temporaryRenderer;
14+
let fullscreenQuadGeometry;
15+
let fullscreenQuadMaterial;
16+
let fullscreenQuad;
17+
18+
export function decompress( texture, maxTextureSize, renderer = null ) {
19+
20+
if ( ! fullscreenQuadGeometry ) fullscreenQuadGeometry = new PlaneGeometry( 2, 2, 1, 1 );
21+
if ( ! fullscreenQuadMaterial ) fullscreenQuadMaterial = new ShaderMaterial( {
22+
uniforms: { blitTexture: new Uniform( texture ) },
23+
vertexShader: `
24+
varying vec2 vUv;
25+
void main(){
26+
vUv = uv;
27+
gl_Position = vec4(position.xy * 1.0,0.,.999999);
28+
}`,
29+
fragmentShader: `
30+
uniform sampler2D blitTexture;
31+
varying vec2 vUv;
32+
33+
// took from threejs 05fc79cd52b79e8c3e8dec1e7dca72c5c39983a4
34+
vec4 conv_LinearTosRGB( in vec4 value ) {
35+
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 );
36+
}
37+
38+
void main(){
39+
gl_FragColor = vec4(vUv.xy, 0, 1);
40+
41+
#ifdef IS_SRGB
42+
gl_FragColor = conv_LinearTosRGB( texture2D( blitTexture, vUv) );
43+
#else
44+
gl_FragColor = texture2D( blitTexture, vUv);
45+
#endif
46+
}`
47+
} );
48+
49+
fullscreenQuadMaterial.uniforms.blitTexture.value = texture;
50+
fullscreenQuadMaterial.defines.IS_SRGB = texture.encoding == sRGBEncoding;
51+
fullscreenQuadMaterial.needsUpdate = true;
52+
53+
if ( ! fullscreenQuad ) {
54+
55+
fullscreenQuad = new Mesh( fullscreenQuadGeometry, fullscreenQuadMaterial );
56+
fullscreenQuad.frustrumCulled = false;
57+
58+
}
59+
60+
const temporaryCam = new PerspectiveCamera();
61+
const temporaryScene = new Scene();
62+
temporaryScene.add( fullscreenQuad );
63+
64+
if ( ! renderer ) {
65+
66+
if ( ! temporaryRenderer )
67+
temporaryRenderer = new WebGLRenderer( { antialias: false } );
68+
69+
renderer = temporaryRenderer;
70+
71+
}
72+
73+
renderer.setSize( Math.min( texture.image.width, maxTextureSize ), Math.min( texture.image.height, maxTextureSize ) );
74+
renderer.clear();
75+
renderer.render( temporaryScene, temporaryCam );
76+
77+
const readableTexture = new Texture( renderer.domElement );
78+
79+
readableTexture.minFilter = texture.minFilter;
80+
readableTexture.magFilter = texture.magFilter;
81+
readableTexture.wrapS = texture.wrapS;
82+
readableTexture.wrapT = texture.wrapT;
83+
readableTexture.name = texture.name;
84+
85+
readableTexture.userData.mimeType = 'image/png';
86+
87+
if ( temporaryRenderer ) {
88+
89+
temporaryRenderer.dispose();
90+
91+
}
92+
93+
return readableTexture;
94+
95+
}

examples/misc_exporter_gltf.html

+45-2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030

3131
import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js';
3232
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
33+
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
34+
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
35+
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
3336
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
3437

3538
function exportGLTF( input ) {
@@ -99,7 +102,7 @@
99102
let container;
100103

101104
let camera, object, object2, material, geometry, scene1, scene2, renderer;
102-
let gridHelper, sphere, model;
105+
let gridHelper, sphere, model, coffeemat;
103106

104107
const params = {
105108
trs: false,
@@ -111,7 +114,8 @@
111114
exportSphere: exportSphere,
112115
exportModel: exportModel,
113116
exportObjects: exportObjects,
114-
exportSceneObject: exportSceneObject
117+
exportSceneObject: exportSceneObject,
118+
exportCompressedObject: exportCompressedObject,
115119
};
116120

117121
init();
@@ -451,6 +455,38 @@
451455

452456
window.addEventListener( 'resize', onWindowResize );
453457

458+
// ---------------------------------------------------------------------
459+
// Exporting compressed textures and meshes (KTX2 / Draco / Meshopt)
460+
// ---------------------------------------------------------------------
461+
const ktx2Loader = new KTX2Loader()
462+
.setTranscoderPath( 'jsm/libs/basis/' )
463+
.detectSupport( renderer );
464+
465+
const dracoLoader = new DRACOLoader()
466+
.setDecoderPath( 'jsm/libs/draco/' );
467+
468+
const gltfLoader = new GLTFLoader().setPath( 'models/gltf/' );
469+
gltfLoader.setKTX2Loader( ktx2Loader );
470+
gltfLoader.setDRACOLoader( dracoLoader );
471+
gltfLoader.setMeshoptDecoder( MeshoptDecoder );
472+
gltfLoader.load( 'coffeemat.etc1s+draco.glb', function ( gltf ) {
473+
474+
// coffeemat.etc1s+draco.glb was produced from the source scene using gltf-transform:
475+
// gltf-transform etc1s coffeemat/scene.gltf coffeemat.etc1s.glb
476+
// gltf-transform draco coffeemat.etc1s.glb coffeemat.etc1s+draco.glb
477+
// The resulting model uses KHR_texture_basisu (for texture compression using ETC1S) and DRACO mesh compression.
478+
479+
gltf.scene.position.x = 400;
480+
gltf.scene.position.z = - 200;
481+
482+
scene1.add( gltf.scene );
483+
484+
coffeemat = gltf.scene;
485+
486+
} );
487+
488+
//
489+
454490
const gui = new GUI();
455491

456492
let h = gui.addFolder( 'Settings' );
@@ -466,6 +502,7 @@
466502
h.add( params, 'exportModel' ).name( 'Export Model' );
467503
h.add( params, 'exportObjects' ).name( 'Export Sphere With Grid' );
468504
h.add( params, 'exportSceneObject' ).name( 'Export Scene 1 and Object' );
505+
h.add( params, 'exportCompressedObject' ).name( 'Export Coffeemat (compressed)' );
469506

470507
gui.open();
471508

@@ -507,6 +544,12 @@
507544

508545
}
509546

547+
function exportCompressedObject() {
548+
549+
exportGLTF( [ coffeemat ] );
550+
551+
}
552+
510553
function onWindowResize() {
511554

512555
camera.aspect = window.innerWidth / window.innerHeight;
3.14 MB
Binary file not shown.
3.23 MB
Binary file not shown.
2.88 KB
Loading

0 commit comments

Comments
 (0)