Parcourir la source

Examples: Add UltraHDRLoader (#28825)

* Add UltraHDRLoader

* Add UltraHDRLoader example
Adjust files.json
Add 2K hdr.jpg texture

* Change URL validation to equivalent hdrgm:Version param validation

* Add docs screenshot

* Adjust for DeepScan

* Add THREE.FloatType support

* Return pending DataTexture directly from .load

* Deprecate hdrjpg example

* Adjust code to precalculate values where feasible
Adjust for DeepScan
MJurczyk il y a 1 an
Parent
commit
0652198055

+ 1 - 1
examples/files.json

@@ -118,8 +118,8 @@
 		"webgl_loader_tilt",
 		"webgl_loader_texture_dds",
 		"webgl_loader_texture_exr",
+		"webgl_loader_texture_ultrahdr",
 		"webgl_loader_texture_hdr",
-		"webgl_loader_texture_hdrjpg",
 		"webgl_loader_texture_ktx",
 		"webgl_loader_texture_ktx2",
 		"webgl_loader_texture_logluv",

+ 1 - 0
examples/jsm/Addons.js

@@ -117,6 +117,7 @@ export * from './loaders/PDBLoader.js';
 export * from './loaders/PLYLoader.js';
 export * from './loaders/PVRLoader.js';
 export * from './loaders/RGBELoader.js';
+export * from './loaders/UltraHDRLoader.js';
 export * from './loaders/RGBMLoader.js';
 export * from './loaders/STLLoader.js';
 export * from './loaders/SVGLoader.js';

+ 592 - 0
examples/jsm/loaders/UltraHDRLoader.js

@@ -0,0 +1,592 @@
+import {
+	ClampToEdgeWrapping,
+	DataTexture,
+	DataUtils,
+	FileLoader,
+	HalfFloatType,
+	LinearFilter,
+	LinearMipMapLinearFilter,
+	LinearSRGBColorSpace,
+	Loader,
+	RGBAFormat,
+	UVMapping,
+} from 'three';
+
+// UltraHDR Image Format - https://developer.android.com/media/platform/hdr-image-format
+// Originally proposed as gainmap-js using a WASM dependency - https://github.com/MONOGRID/gainmap-js
+
+/**
+ *
+ * Short format brief:
+ *
+ *  [JPEG headers]
+ *  [XMP metadata describing the MPF container and *both* SDR and gainmap images]
+ *  [Optional metadata] [EXIF] [ICC Profile]
+ *  [SDR image]
+ *  [XMP metadata describing only the gainmap image]
+ *  [Gainmap image]
+ *
+ * Each section is separated by a 0xFFXX byte followed by a descriptor byte (0xFFE0, 0xFFE1, 0xFFE2.)
+ * Binary image storages are prefixed with a unique 0xFFD8 16-bit descriptor.
+ */
+
+/**
+ * Current feature set:
+ * - JPEG headers (required)
+ * - XMP metadata (required)
+ *  - XMP validation (not implemented)
+ * - EXIF profile (not implemented)
+ * - ICC profile (not implemented)
+ * - Binary storage for SDR & HDR images (required)
+ * - Gainmap metadata (required)
+ * - Non-JPEG image formats (not implemented)
+ * - Primary image as an HDR image (not implemented)
+ */
+
+/* Calculating this SRGB powers is extremely slow for 4K images and can be sufficiently precalculated for a 3-4x speed boost */
+const SRGB_TO_LINEAR = Array( 1024 )
+	.fill( 0 )
+	.map( ( _, value ) =>
+		Math.pow( ( value / 255 ) * 0.9478672986 + 0.0521327014, 2.4 )
+	);
+
+class UltraHDRLoader extends Loader {
+
+	constructor( manager ) {
+
+		super( manager );
+
+		this.type = HalfFloatType;
+
+	}
+
+	setDataType( value ) {
+
+		this.type = value;
+
+		return this;
+
+	}
+
+	parse( buffer, onLoad ) {
+
+		const xmpMetadata = {
+			version: null,
+			baseRenditionIsHDR: null,
+			gainMapMin: null,
+			gainMapMax: null,
+			gamma: null,
+			offsetSDR: null,
+			offsetHDR: null,
+			hdrCapacityMin: null,
+			hdrCapacityMax: null,
+		};
+		const textDecoder = new TextDecoder();
+
+		const data = new DataView( buffer );
+		const dataAsString = textDecoder.decode( data );
+
+		/* Minimal sufficient validation - https://developer.android.com/media/platform/hdr-image-format#signal_of_the_format */
+		if ( ! dataAsString.includes( 'hdrgm:Version="1.0"' ) ) {
+
+			throw new Error( 'THREE.UltraHDRLoader: Not a valid UltraHDR image' );
+
+		}
+
+		let byteOffset = 0;
+		const sections = [];
+
+		while ( byteOffset < data.byteLength ) {
+
+			const byte = data.getUint8( byteOffset );
+
+			if ( byte === 0xff ) {
+
+				const leadingByte = data.getUint8( byteOffset + 1 );
+
+				if (
+					[
+						/* Valid section headers */
+						0xd8, // SOI
+						0xe0, // APP0
+						0xe1, // APP1
+						0xe2, // APP2
+					].includes( leadingByte )
+				) {
+
+					sections.push( {
+						sectionType: leadingByte,
+						section: [ byte, leadingByte ],
+						sectionOffset: byteOffset + 2,
+					} );
+
+					byteOffset += 2;
+
+				} else {
+
+					sections[ sections.length - 1 ].section.push( byte, leadingByte );
+
+					byteOffset += 2;
+
+				}
+
+			} else {
+
+				sections[ sections.length - 1 ].section.push( byte );
+
+				byteOffset ++;
+
+			}
+
+		}
+
+		let primaryImage, gainmapImage;
+
+		for ( let i = 0; i < sections.length; i ++ ) {
+
+			const { sectionType, section, sectionOffset } = sections[ i ];
+
+			if ( sectionType === 0xe0 ) {
+
+				/* JPEG Header - no useful information */
+
+			} else if ( sectionType === 0xe1 ) {
+
+				/* XMP Metadata */
+
+				this._parseXMPMetadata(
+					textDecoder.decode( new Uint8Array( section ) ),
+					xmpMetadata
+				);
+
+			} else if ( sectionType === 0xe2 ) {
+
+				/* Data Sections - MPF / EXIF / ICC Profile */
+
+				const sectionData = new DataView(
+					new Uint8Array( section.slice( 2 ) ).buffer
+				);
+				const sectionHeader = sectionData.getUint32( 2, false );
+
+				if ( sectionHeader === 0x4d504600 ) {
+
+					/* MPF Section */
+
+					/* Section contains a list of static bytes and ends with offsets indicating location of SDR and gainmap images */
+					/* First bytes after header indicate little / big endian ordering (0x49492A00 - LE / 0x4D4D002A - BE) */
+					/*
+            ... 60 bytes indicating tags, versions, etc. ...
+
+            bytes | bits | description
+
+            4       32     primary image size
+            4       32     primary image offset
+            2       16     0x0000
+            2       16     0x0000
+
+            4       32     0x00000000
+            4       32     gainmap image size
+            4       32     gainmap image offset
+            2       16     0x0000
+            2       16     0x0000
+          */
+
+					const mpfLittleEndian = sectionData.getUint32( 6 ) === 0x49492a00;
+					const mpfBytesOffset = 60;
+
+					/* SDR size includes the metadata length, SDR offset is always 0 */
+
+					const primaryImageSize = sectionData.getUint32(
+						mpfBytesOffset,
+						mpfLittleEndian
+					);
+					const primaryImageOffset = sectionData.getUint32(
+						mpfBytesOffset + 4,
+						mpfLittleEndian
+					);
+
+					/* Gainmap size is an absolute value starting from its offset, gainmap offset needs 6 bytes padding to take into account 0x00 bytes at the end of XMP */
+					const gainmapImageSize = sectionData.getUint32(
+						mpfBytesOffset + 16,
+						mpfLittleEndian
+					);
+					const gainmapImageOffset =
+						sectionData.getUint32( mpfBytesOffset + 20, mpfLittleEndian ) +
+						sectionOffset +
+						6;
+
+					primaryImage = new Uint8Array(
+						data.buffer,
+						primaryImageOffset,
+						primaryImageSize
+					);
+
+					gainmapImage = new Uint8Array(
+						data.buffer,
+						gainmapImageOffset,
+						gainmapImageSize
+					);
+
+				}
+
+			}
+
+		}
+
+		if ( primaryImage && gainmapImage ) {
+
+			this._applyGainmapToSDR(
+				xmpMetadata,
+				primaryImage,
+				gainmapImage,
+				( hdrBuffer, width, height ) => {
+
+					onLoad( {
+						width,
+						height,
+						data: hdrBuffer,
+						format: RGBAFormat,
+						type: this.type,
+					} );
+
+				},
+				( error ) => {
+
+					throw new Error( error );
+
+				}
+			);
+
+		} else {
+
+			throw new Error( 'THREE.UltraHDRLoader: Could not parse UltraHDR images' );
+
+		}
+
+	}
+
+	load( url, onLoad, onError ) {
+
+		const texture = new DataTexture(
+			this.type === HalfFloatType ? new Uint16Array() : new Float32Array(),
+			0,
+			0,
+			RGBAFormat,
+			this.type,
+			UVMapping,
+			ClampToEdgeWrapping,
+			ClampToEdgeWrapping,
+			LinearFilter,
+			LinearMipMapLinearFilter,
+			1,
+			LinearSRGBColorSpace
+		);
+		texture.generateMipmaps = true;
+		texture.flipY = true;
+
+		const loader = new FileLoader( this.manager );
+		loader.setResponseType( 'arraybuffer' );
+		loader.setRequestHeader( this.requestHeader );
+		loader.setPath( this.path );
+		loader.setWithCredentials( this.withCredentials );
+		loader.load( url, ( buffer ) => {
+
+			try {
+
+				this.parse(
+					buffer,
+					( texData ) => {
+
+						texture.image = {
+							data: texData.data,
+							width: texData.width,
+							height: texData.height,
+						};
+						texture.needsUpdate = true;
+
+						if ( onLoad ) onLoad( texture, texData );
+
+					},
+					onError
+				);
+
+			} catch ( error ) {
+
+				if ( onError ) onError( error );
+
+				console.error( error );
+
+			}
+
+		} );
+
+		return texture;
+
+	}
+
+	_parseXMPMetadata( xmpDataString, xmpMetadata ) {
+
+		const domParser = new DOMParser();
+
+		const xmpXml = domParser.parseFromString(
+			xmpDataString.substring(
+				xmpDataString.indexOf( '<' ),
+				xmpDataString.lastIndexOf( '>' ) + 1
+			),
+			'text/xml'
+		);
+
+		/* Determine if given XMP metadata is the primary GContainer descriptor or a gainmap descriptor */
+		const [ hasHDRContainerDescriptor ] = xmpXml.getElementsByTagName(
+			'Container:Directory'
+		);
+
+		if ( hasHDRContainerDescriptor ) {
+
+			/* There's not much useful information in the container descriptor besides memory-validation */
+
+		} else {
+
+			/* Gainmap descriptor - defaults from https://developer.android.com/media/platform/hdr-image-format#HDR_gain_map_metadata */
+
+			const [ gainmapNode ] = xmpXml.getElementsByTagName( 'rdf:Description' );
+
+			xmpMetadata.version = gainmapNode.getAttribute( 'hdrgm:Version' ) || '1.0';
+			xmpMetadata.baseRenditionIsHDR =
+				gainmapNode.getAttribute( 'hdrgm:BaseRenditionIsHDR' ) === 'True';
+			xmpMetadata.gainMapMin = parseFloat(
+				gainmapNode.getAttribute( 'hdrgm:GainMapMin' ) || 0.0
+			);
+			xmpMetadata.gainMapMax = parseFloat(
+				gainmapNode.getAttribute( 'hdrgm:GainMapMax' ) || 1.0
+			);
+			xmpMetadata.gamma = parseFloat(
+				gainmapNode.getAttribute( 'hdrgm:Gamma' ) || 1.0
+			);
+			xmpMetadata.offsetSDR = parseFloat(
+				gainmapNode.getAttribute( 'hdrgm:OffsetSDR' ) / ( 1 / 64 )
+			);
+			xmpMetadata.offsetHDR = parseFloat(
+				gainmapNode.getAttribute( 'hdrgm:OffsetHDR' ) / ( 1 / 64 )
+			);
+			xmpMetadata.hdrCapacityMin = parseFloat(
+				gainmapNode.getAttribute( 'hdrgm:HDRCapacityMin' ) || 0.0
+			);
+			xmpMetadata.hdrCapacityMax = parseFloat(
+				gainmapNode.getAttribute( 'hdrgm:HDRCapacityMax' ) || 1.0
+			);
+
+		}
+
+	}
+
+	_srgbToLinear( value ) {
+
+		if ( value / 255 < 0.04045 ) {
+
+			return ( value / 255 ) * 0.0773993808;
+
+		}
+
+		if ( value < 1024 ) {
+
+			return SRGB_TO_LINEAR[ ~ ~ value ];
+
+		}
+
+		return Math.pow( ( value / 255 ) * 0.9478672986 + 0.0521327014, 2.4 );
+
+	}
+
+	_applyGainmapToSDR(
+		xmpMetadata,
+		sdrBuffer,
+		gainmapBuffer,
+		onSuccess,
+		onError
+	) {
+
+		const getImageDataFromBuffer = ( buffer ) =>
+			new Promise( ( resolve, reject ) => {
+
+				const imageLoader = document.createElement( 'img' );
+
+				imageLoader.onload = () => {
+
+					const image = {
+						width: imageLoader.naturalWidth,
+						height: imageLoader.naturalHeight,
+						source: imageLoader,
+					};
+
+					URL.revokeObjectURL( imageLoader.src );
+
+					resolve( image );
+
+				};
+
+				imageLoader.onerror = () => {
+
+					URL.revokeObjectURL( imageLoader.src );
+
+					reject();
+
+				};
+
+				imageLoader.src = URL.createObjectURL(
+					new Blob( [ buffer ], { type: 'image/jpeg' } )
+				);
+
+			} );
+
+		Promise.all( [
+			getImageDataFromBuffer( sdrBuffer ),
+			getImageDataFromBuffer( gainmapBuffer ),
+		] )
+			.then( ( [ sdrImage, gainmapImage ] ) => {
+
+				const sdrImageAspect = sdrImage.width / sdrImage.height;
+				const gainmapImageAspect = gainmapImage.width / gainmapImage.height;
+
+				if ( sdrImageAspect !== gainmapImageAspect ) {
+
+					onError(
+						'THREE.UltraHDRLoader Error: Aspect ratio mismatch between SDR and Gainmap images'
+					);
+
+					return;
+
+				}
+
+				const canvas = document.createElement( 'canvas' );
+				const ctx = canvas.getContext( '2d', {
+					willReadFrequently: true,
+					colorSpace: 'srgb',
+				} );
+
+				canvas.width = sdrImage.width;
+				canvas.height = sdrImage.height;
+
+				/* Use out-of-the-box interpolation of Canvas API to scale gainmap to fit the SDR resolution */
+				ctx.drawImage(
+					gainmapImage.source,
+					0,
+					0,
+					gainmapImage.width,
+					gainmapImage.height,
+					0,
+					0,
+					sdrImage.width,
+					sdrImage.height
+				);
+				const gainmapImageData = ctx.getImageData(
+					0,
+					0,
+					sdrImage.width,
+					sdrImage.height,
+					{ colorSpace: 'srgb' }
+				);
+
+				ctx.drawImage( sdrImage.source, 0, 0 );
+				const sdrImageData = ctx.getImageData(
+					0,
+					0,
+					sdrImage.width,
+					sdrImage.height,
+					{ colorSpace: 'srgb' }
+				);
+
+				/* HDR Recovery formula - https://developer.android.com/media/platform/hdr-image-format#use_the_gain_map_to_create_adapted_HDR_rendition */
+				let hdrBuffer;
+
+				if ( this.type === HalfFloatType ) {
+
+					hdrBuffer = new Uint16Array( sdrImageData.data.length ).fill( 23544 );
+
+				} else {
+
+					hdrBuffer = new Float32Array( sdrImageData.data.length ).fill( 255 );
+
+				}
+
+				const maxDisplayBoost = Math.sqrt(
+					Math.pow(
+						/* 1.8 instead of 2 near-perfectly rectifies approximations introduced by precalculated SRGB_TO_LINEAR values */
+						1.8,
+						xmpMetadata.hdrCapacityMax
+					)
+				);
+				const unclampedWeightFactor =
+					( Math.log2( maxDisplayBoost ) - xmpMetadata.hdrCapacityMin ) /
+					( xmpMetadata.hdrCapacityMax - xmpMetadata.hdrCapacityMin );
+				const weightFactor = Math.min(
+					Math.max( unclampedWeightFactor, 0.0 ),
+					1.0
+				);
+				const useGammaOne = xmpMetadata.gamma === 1.0;
+
+				for (
+					let pixelIndex = 0;
+					pixelIndex < sdrImageData.data.length;
+					pixelIndex += 4
+				) {
+
+					const x = ( pixelIndex / 4 ) % sdrImage.width;
+					const y = Math.floor( pixelIndex / 4 / sdrImage.width );
+
+					for ( let index = pixelIndex; index < pixelIndex + 3; index ++ ) {
+
+						const sdrValue = sdrImageData.data[ index ];
+
+						const gainmapIndex = ( y * sdrImage.width + x ) * 4;
+						const gainmapValue = gainmapImageData.data[ gainmapIndex ] / 255.0;
+
+						/* Gamma is 1.0 by default */
+						const logRecovery = useGammaOne
+							? gainmapValue
+							: Math.pow( gainmapValue, 1.0 / xmpMetadata.gamma );
+
+						const logBoost =
+							xmpMetadata.gainMapMin * ( 1.0 - logRecovery ) +
+							xmpMetadata.gainMapMax * logRecovery;
+
+						const hdrValue =
+						( sdrValue + xmpMetadata.offsetSDR ) *
+						( logBoost * weightFactor === 0.0
+							? 1.0
+							: Math.pow( 2, logBoost * weightFactor ) ) -
+					xmpMetadata.offsetHDR;
+
+						const linearHDRValue = Math.min(
+							Math.max(
+								this._srgbToLinear( hdrValue ),
+								0
+							),
+							65504
+						);
+
+						hdrBuffer[ index ] =
+							this.type === HalfFloatType
+								? DataUtils.toHalfFloat( linearHDRValue )
+								: linearHDRValue;
+
+					}
+
+				}
+
+				onSuccess( hdrBuffer, sdrImage.width, sdrImage.height );
+
+			} )
+			.catch( () => {
+
+				throw new Error(
+					'THREE.UltraHDRLoader Error: Could not parse UltraHDR images'
+				);
+
+			} );
+
+	}
+
+}
+
+export { UltraHDRLoader };

BIN
examples/screenshots/webgl_loader_texture_hdrjpg.jpg


BIN
examples/screenshots/webgl_loader_texture_ultrahdr.jpg


+ 1 - 1
examples/tags.json

@@ -44,7 +44,7 @@
 	"webgl_loader_ifc": [ "external" ],
 	"webgl_loader_ldraw": [ "lego" ],
 	"webgl_loader_pdb": [ "molecules", "css2d" ],
-	"webgl_loader_texture_hdrjpg": [ "external", "hdr", "jpg", "ultrahdr", "gainmap" ],
+	"webgl_loader_texture_ultrahdr": [ "hdr", "jpg", "ultrahdr" ],
 	"webgl_loader_ttf": [ "text", "font" ],
 	"webgl_lod": [ "level", "details" ],
 	"webgl_materials_blending": [ "alpha" ],

BIN
examples/textures/equirectangular/spruit_sunrise_2k.hdr.jpg


+ 0 - 289
examples/webgl_loader_texture_hdrjpg.html

@@ -1,289 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-	<head>
-		<title>three.js webgl - hdr jpg</title>
-		<meta charset="utf-8">
-		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
-		<link type="text/css" rel="stylesheet" href="main.css">
-		<style>
-			.lbl {
-				color: #fff;
-				font-size: 16px;
-				font-weight: bold;
-				position: absolute;
-				bottom: 0px;
-				z-index: 100;
-				text-shadow: #000 1px 1px 1px;
-				background-color: rgba(0,0,0,0.85);
-				padding: 1em;
-			}
-
-			#lbl_left {
-				text-align:left;
-				left:0px;
-			}
-		</style>
-	</head>
-
-	<body>
-		<div id="info">
-			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - hdr jpg loader <br/>
-			Converted from hdr with <a href="https://gainmap-creator.mono-grid.com/" target="_blank" rel="noopener">converter</a>. <br />
-			See external <a href="https://github.com/MONOGRID/gainmap-js" target="_blank" rel="noopener">gainmap-js</a> for more information.
-		</div>
-
-		<div id="lbl_left" class="lbl"></div>
-
-		<script type="importmap">
-			{
-				"imports": {
-					"three": "../build/three.module.js",
-					"three/addons/": "./jsm/",
-					"@monogrid/gainmap-js": "https://cdn.jsdelivr.net/npm/@monogrid/[email protected]/dist/decode.js"
-				}
-			}
-		</script>
-
-		<script type="module">
-
-			import * as THREE from 'three';
-
-			import Stats from 'three/addons/libs/stats.module.js';
-
-			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
-			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
-			import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
-
-			import { HDRJPGLoader } from '@monogrid/gainmap-js';
-
-			const params = {
-				envMap: 'HDR JPG',
-				roughness: 0.0,
-				metalness: 1.0,
-				exposure: 1.0,
-				debug: false
-			};
-
-			let container, stats;
-			let camera, scene, renderer, controls;
-			let torusMesh, planeMesh;
-			let hdrJpg, hdrJpgPMREMRenderTarget, hdrJpgEquirectangularMap;
-			let hdrPMREMRenderTarget, hdrEquirectangularMap;
-
-
-			const fileSizes = {};
-			const resolutions = {};
-
-			init();
-
-			function init() {
-
-				const lbl = document.getElementById( 'lbl_left' );
-
-				container = document.createElement( 'div' );
-				document.body.appendChild( container );
-
-				camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 1, 500 );
-				camera.position.set( 0, 0, - 120 );
-
-				scene = new THREE.Scene();
-
-				renderer = new THREE.WebGLRenderer();
-				renderer.toneMapping = THREE.ACESFilmicToneMapping;
-
-				//
-
-				let geometry = new THREE.TorusKnotGeometry( 18, 8, 200, 40, 1, 3 );
-				let material = new THREE.MeshStandardMaterial( {
-					color: 0xffffff,
-					metalness: params.metalness,
-					roughness: params.roughness
-				} );
-
-				torusMesh = new THREE.Mesh( geometry, material );
-				scene.add( torusMesh );
-
-
-				geometry = new THREE.PlaneGeometry( 200, 200 );
-				material = new THREE.MeshBasicMaterial();
-
-				planeMesh = new THREE.Mesh( geometry, material );
-				planeMesh.position.y = - 50;
-				planeMesh.rotation.x = - Math.PI * 0.5;
-				scene.add( planeMesh );
-
-
-				const pmremGenerator = new THREE.PMREMGenerator( renderer );
-				pmremGenerator.compileEquirectangularShader();
-
-				THREE.DefaultLoadingManager.onLoad = function ( ) {
-
-					pmremGenerator.dispose();
-
-				};
-
-
-
-				hdrJpg = new HDRJPGLoader( renderer )
-					.load( 'textures/equirectangular/spruit_sunrise_4k.hdr.jpg', function ( ) {
-
-						resolutions[ 'HDR JPG' ] = hdrJpg.width + 'x' + hdrJpg.height;
-
-						displayStats( 'HDR JPG' );
-
-						hdrJpgEquirectangularMap = hdrJpg.renderTarget.texture;
-						hdrJpgPMREMRenderTarget = pmremGenerator.fromEquirectangular( hdrJpgEquirectangularMap );
-
-						hdrJpgEquirectangularMap.mapping = THREE.EquirectangularReflectionMapping;
-						hdrJpgEquirectangularMap.needsUpdate = true;
-
-						hdrJpg.dispose();
-
-					}, function ( progress ) {
-
-						fileSizes[ 'HDR JPG' ] = humanFileSize( progress.total );
-
-					} );
-
-				hdrEquirectangularMap = new RGBELoader()
-					.load( 'textures/equirectangular/spruit_sunrise_1k.hdr', function ( ) {
-
-						resolutions[ 'HDR' ] = hdrEquirectangularMap.image.width + 'x' + hdrEquirectangularMap.image.height;
-
-						hdrPMREMRenderTarget = pmremGenerator.fromEquirectangular( hdrEquirectangularMap );
-
-						hdrEquirectangularMap.mapping = THREE.EquirectangularReflectionMapping;
-						hdrEquirectangularMap.minFilter = THREE.LinearFilter;
-						hdrEquirectangularMap.magFilter = THREE.LinearFilter;
-						hdrEquirectangularMap.needsUpdate = true;
-
-					}, function ( progress ) {
-
-						fileSizes[ 'HDR' ] = humanFileSize( progress.total );
-
-					} );
-
-				renderer.setPixelRatio( window.devicePixelRatio );
-				renderer.setSize( window.innerWidth, window.innerHeight );
-				renderer.setAnimationLoop( animate );
-				container.appendChild( renderer.domElement );
-
-				stats = new Stats();
-				container.appendChild( stats.dom );
-
-				controls = new OrbitControls( camera, renderer.domElement );
-				controls.minDistance = 50;
-				controls.maxDistance = 300;
-
-				window.addEventListener( 'resize', onWindowResize );
-
-				const gui = new GUI();
-
-				gui.add( params, 'envMap', [ 'HDR JPG', 'HDR' ] ).onChange( displayStats );
-				gui.add( params, 'roughness', 0, 1, 0.01 );
-				gui.add( params, 'metalness', 0, 1, 0.01 );
-				gui.add( params, 'exposure', 0, 2, 0.01 );
-				gui.add( params, 'debug' );
-				gui.open();
-
-				function displayStats( value ) {
-
-					lbl.innerHTML = value + ' size : ' + fileSizes[ value ] + ', Resolution: ' + resolutions[ value ];
-
-				}
-
-			}
-
-
-			function humanFileSize( bytes, si = true, dp = 1 ) {
-
-				const thresh = si ? 1000 : 1024;
-
-				if ( Math.abs( bytes ) < thresh ) {
-
-					return bytes + ' B';
-
-				}
-
-				const units = si
-					? [ 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' ]
-					: [ 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB' ];
-				let u = - 1;
-				const r = 10 ** dp;
-
-				do {
-
-					bytes /= thresh;
-					++ u;
-
-				} while ( Math.round( Math.abs( bytes ) * r ) / r >= thresh && u < units.length - 1 );
-
-				return bytes.toFixed( dp ) + ' ' + units[ u ];
-
-			}
-
-
-			function onWindowResize() {
-
-				const width = window.innerWidth;
-				const height = window.innerHeight;
-
-				camera.aspect = width / height;
-				camera.updateProjectionMatrix();
-
-				renderer.setSize( width, height );
-
-			}
-
-			function animate() {
-			
-				stats.begin();
-				render();
-				stats.end();
-
-			}
-
-			function render() {
-
-				torusMesh.material.roughness = params.roughness;
-				torusMesh.material.metalness = params.metalness;
-
-				let pmremRenderTarget, equirectangularMap;
-
-				switch ( params.envMap ) {
-
-					case 'HDR JPG':
-						pmremRenderTarget = hdrJpgPMREMRenderTarget;
-						equirectangularMap = hdrJpgEquirectangularMap;
-						break;
-					case 'HDR':
-						pmremRenderTarget = hdrPMREMRenderTarget;
-						equirectangularMap = hdrEquirectangularMap;
-						break;
-
-				}
-
-				const newEnvMap = pmremRenderTarget ? pmremRenderTarget.texture : null;
-
-				if ( newEnvMap && newEnvMap !== torusMesh.material.envMap ) {
-
-					planeMesh.material.map = newEnvMap;
-					planeMesh.material.needsUpdate = true;
-
-				}
-
-				torusMesh.rotation.y += 0.005;
-				planeMesh.visible = params.debug;
-
-				scene.environment = equirectangularMap;
-				scene.background = equirectangularMap;
-				renderer.toneMappingExposure = params.exposure;
-
-				renderer.render( scene, camera );
-
-			}
-
-		</script>
-
-	</body>
-</html>

+ 152 - 0
examples/webgl_loader_texture_ultrahdr.html

@@ -0,0 +1,152 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgl - materials - UltraHDR texture loader</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<link type="text/css" rel="stylesheet" href="main.css">
+	</head>
+	<body>
+
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - UltraHDR texture loader <br/>
+			Converted from hdr with <a href="https://gainmap-creator.mono-grid.com/" target="_blank" rel="noopener">converter</a>. <br />
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+
+			import { UltraHDRLoader } from 'three/addons/loaders/UltraHDRLoader.js';
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+
+			const params = {
+				autoRotate: true,
+				metalness: 1.0,
+				roughness: 0.0,
+				exposure: 1.0,
+				resolution: '2k',
+				type: 'HalfFloatType'
+			};
+
+			let renderer, scene, camera, controls, torusMesh, loader;
+
+			init();
+
+			function init() {
+
+				renderer = new THREE.WebGLRenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				document.body.appendChild( renderer.domElement );
+
+				renderer.toneMapping = THREE.ACESFilmicToneMapping;
+				renderer.toneMappingExposure = params.exposure;
+
+				renderer.setAnimationLoop( render );
+
+				scene = new THREE.Scene();
+
+				torusMesh = new THREE.Mesh(
+					new THREE.TorusKnotGeometry( 1, 0.4, 128, 128, 1, 3 ),
+					new THREE.MeshStandardMaterial( { roughness: params.roughness, metalness: params.metalness } )
+				 );
+				scene.add( torusMesh );
+
+				camera = new THREE.PerspectiveCamera(
+					50,
+					window.innerWidth / window.innerHeight,
+					1,
+					500
+				);
+				camera.position.set( 0.0, 0.0, - 6.0 );
+
+				controls = new OrbitControls( camera, renderer.domElement );
+			
+				loader = new UltraHDRLoader();
+				loader.setDataType( THREE.FloatType );
+
+				const loadEnvironment = function ( resolution = '2k', type = 'HalfFloatType' ) {
+
+					loader.setDataType( THREE[ type ] );
+
+					loader.load( `textures/equirectangular/spruit_sunrise_${resolution}.hdr.jpg`, function ( texture ) {
+
+						texture.mapping = THREE.EquirectangularReflectionMapping;
+						texture.needsUpdate = true;
+
+						scene.background = texture;
+						scene.environment = texture;
+
+					} );
+
+				};
+
+				loadEnvironment( params.resolution, params.type );
+
+				const gui = new GUI();
+
+				gui.add( params, 'autoRotate' );
+				gui.add( params, 'metalness', 0, 1, 0.01 );
+				gui.add( params, 'roughness', 0, 1, 0.01 );
+				gui.add( params, 'exposure', 0, 4, 0.01 );
+				gui.add( params, 'resolution', [ '2k', '4k' ] ).onChange( ( value ) => {
+
+					loadEnvironment( value, params.type );
+			
+				} );
+				gui.add( params, 'type', [ 'HalfFloatType', 'FloatType' ] ).onChange( ( value ) => {
+
+					loadEnvironment( params.resolution, value );
+			
+				} );
+
+				gui.open();
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			function render() {
+
+				torusMesh.material.roughness = params.roughness;
+				torusMesh.material.metalness = params.metalness;
+
+				if ( params.autoRotate ) {
+
+					torusMesh.rotation.y += 0.005;
+
+				}
+
+				renderer.toneMappingExposure = params.exposure;
+
+				controls.update();
+
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+	</body>
+</html>