<!-- NEW3 — TEST 3 · THE RIVER'S JOURNEY · part 394 -->
sary

		const source = texture.source;
		let webglTextures = _sources.get( source );

		if ( webglTextures === undefined ) {

			webglTextures = {};
			_sources.set( source, webglTextures );

		}

		// check if there is already a WebGLTexture object for the given texture parameters

		const textureCacheKey = getTextureCacheKey( texture );

		if ( textureCacheKey !== textureProperties.__cacheKey ) {

			// if not, create a new instance of WebGLTexture

			if ( webglTextures[ textureCacheKey ] === undefined ) {

				// create new entry

				webglTextures[ textureCacheKey ] = {
					texture: _gl.createTexture(),
					usedTimes: 0
				};

				info.memory.textures ++;

				// when a new instance of WebGLTexture was created, a texture upload is required
				// even if the image contents are identical

				forceUpload = true;

			}

			webglTextures[ textureCacheKey ].usedTimes ++;

			// every time the texture cache key changes, it's necessary to check if an instance of
			// WebGLTexture can be deleted in order to avoid a memory leak.

			const webglTexture = webglTextures[ textureProperties.__cacheKey ];

			if ( webglTexture !== undefined ) {

				webglTextures[ textureProperties.__cacheKey ].usedTimes --;

				if ( webglTexture.usedTimes === 0 ) {

					deleteTexture( texture );

				}

			}

			// store references to cache key and WebGLTexture object

			textureProperties.__cacheKey = textureCacheKey;
			textureProperties.__webglTexture = webglTextures[ textureCacheKey ].texture;

		}

		return forceUpload;

	}

	function getRow( index, rowLength, componentStride ) {

		return Math.floor( Math.floor( index / componentStride ) / rowLength );

	}

	function updateTexture( texture, image, glFormat, glType ) {

		const componentStride = 4; // only RGBA supported

		const updateRanges = texture.updateRanges;

		if ( updateRanges.length === 0 ) {

			state.texSubImage2D( _gl.TEXTURE_2D, 0, 0, 0, image.width, image.height, glFormat, glType, image.data );

		} else {

			// Before applying update ranges, we merge any adjacent / overlapping
			// ranges to reduce load on `gl.texSubImage2D`. Empirically, this has led
			// to performance improvements for applications which make heavy use of
			// update ranges. Likely due to GPU command overhead.
			//
			// Note that to reduce garbage collection between frames, we merge the
			// update ranges in-place. This is safe because this method will clear the
			// update ranges once updated.

			updateRanges.sort( ( a, b ) => a.start - b.start );

			// To merge the update ranges in-place, we work from left to right in the
			// existing updateRanges array, merging ranges. This may result in a final
			// array which is smaller than the original. This index tracks the last
			// index representing a merged range, any data after this index can be
			// trimmed once the merge algorithm is completed.
			let mergeIndex = 0;

			for ( let i = 1; i < updateRanges.length; i ++ ) {

				const previousRange = updateRanges[ mergeIndex ];
				const range = updateRanges[ i ];

				// Only merge if in the same row and overlapping/adjacent
				const previousEnd = previousRange.start + previousRange.count;
				const currentRow = getRow( range.start, image.width, componentStride );
				const previousRow = getRow( previousRange.start, image.width, componentStride );

				// We add one here to merge adjacent ranges. This is safe because ranges
				// operate over positive integers.
				if (
					range.start <= previousEnd + 1 &&
					currentRow === previousRow &&
					getRow( range.start + range.count - 1, image.width, componentStride ) === currentRow // ensure range doesn't spill
				) {

					previousRange.count = Math.max(
						previousRange.count,
						range.start + range.count - previousRange.start
					);

				} else {

					++ mergeIndex;
					updateRanges[ mergeIndex ] = range;

				}


			}

			// Trim the array to only contain the merged ranges.
			updateRanges.length = mergeIndex + 1;

			const currentUnpackRowLen = state.getParameter( _gl.UNPACK_ROW_LENGTH );
			const currentUnpackSkipPixels = state.getParameter( _gl.UNPACK_SKIP_PIXELS );
			const currentUnpackSkipRows = state.getParameter( _gl.UNPACK_SKIP_ROWS );

			state.pixelStorei( _gl.UNPACK_ROW_LENGTH, image.width );

			for ( let i = 0, l = updateRanges.length; i < l; i ++ ) {

				const range = updateRanges[ i ];

				const pixelStart = Math.floor( range.start / componentStride );
				const pixelCount = Math.ceil( range.count / componentStride );

				const x = pixelStart % image.width;
				const y = Math.floor( pixelStart / image.width );

				// Assumes update ranges refer to contiguous memory
				const width = pixelCount;
				const height = 1;

				state.pixelStorei( _gl.UNPACK_SKIP_PIXELS, x );
				state.pixelStorei( _gl.UNPACK_SKIP_ROWS, y );

				state.texSubImage2D( _gl.TEXTURE_2D, 0, x, y, width, height, glFormat, glType, image.data );

			}

			texture.c