Hello,
I’m writing a raymarcher using two sets of 256^3 RG8UI Textures. Since the textures are “large” and the data is streamed over network I want to use an async approach so I am dividing them in 32^3 chunks and am leveraging copyTextureToTexture to write them in the texture.
My issue is that the two textures get overwritten. As far as I understand it has to do with them landing on the same texture slot (and if I’m correct I have no access of that in Three.js)
Right now I have set my shader to render one raymarching pass of a texture in the red channel and the other texture in the blue channel.
Sometimes refreshing the page shows the blue and red colors, sometimes I see the wrong texture flashing in red before being overwritten by the correct one.
I hope someone can explain me what I’m understanding wrong
This is the atlas manager I wrote, and in my material setup I don’t do much more than setting up the objects and binding the uniforms:
import \* as THREE from 'three';
export interface AtlasOptions {
width: number;
height: number;
depth: number;
tileSize: number; // e.g. 32
bytesPerVoxel?: number; // e.g. 2 for RG8UI
internalFormat?: THREE.PixelFormatGPU; // e.g. gl.RG8UI
format?: THREE.PixelFormat;
type?: THREE.TextureDataType;
wrapMode?: THREE.Wrapping;
magFilter?: THREE.MagnificationTextureFilter;
minFilter?: THREE.MinificationTextureFilter;
}
export class AtlasManager {
width: number;
height: number;
depth: number;
tileSize: number;
tilesPerAxis: number;
maxSlots: number;
bytesPerVoxel: number;
internalFormat: THREE.PixelFormatGPU;
format: THREE.PixelFormat;
type: THREE.TextureDataType;
wrapMode: THREE.Wrapping;
magFilter: THREE.MagnificationTextureFilter;
minFilter: THREE.MinificationTextureFilter;
renderer: THREE.WebGLRenderer | null;
tex: THREE.Data3DTexture;
constructor(opts: AtlasOptions, renderer?: THREE.WebGLRenderer) {
this.renderer = renderer ?? null;
this.width = opts.width;
this.height = opts.height;
this.depth = opts.depth;
this.tileSize = opts.tileSize;
this.tilesPerAxis = Math.floor(this.width / this.tileSize);
this.maxSlots = this.tilesPerAxis \* this.tilesPerAxis \* Math.floor(this.depth / this.tileSize);
this.bytesPerVoxel = opts.bytesPerVoxel ?? 4;
this.internalFormat = opts.internalFormat ?? 'RGBA8';
this.format = opts.format ?? THREE.RGBAFormat;
this.type = opts.type ?? THREE.UnsignedByteType;
this.wrapMode = opts.wrapMode ?? THREE.ClampToEdgeWrapping;
this.magFilter = opts.magFilter ?? THREE.NearestFilter;
this.minFilter = opts.minFilter ?? THREE.NearestFilter;
const tempTex = new Uint8Array(this.width \* this.height \* this.depth \* this.bytesPerVoxel);
this.tex = new THREE.Data3DTexture(tempTex, this.width, this.height, this.depth);
this.tex.internalFormat = this.internalFormat;
this.tex.format = this.format;
this.tex.type = this.type;
this.tex.wrapS = this.wrapMode;
this.tex.wrapT = this.wrapMode;
this.tex.wrapR = this.wrapMode;
this.tex.magFilter = this.magFilter;
this.tex.minFilter = this.minFilter;
this.tex.generateMipmaps = false;
this.tex.flipY = false;
this.tex.premultiplyAlpha = false;
this.tex.needsUpdate = true;
this.renderer?.initTexture(this.tex);
}
uploadTile(slotIndex: number, tileData: Uint8Array) {
const x = (slotIndex % this.tilesPerAxis) \* this.tileSize;
const y = Math.floor((slotIndex / this.tilesPerAxis) % this.tilesPerAxis) \* this.tileSize;
const z = Math.floor(slotIndex / (this.tilesPerAxis \* this.tilesPerAxis)) \* this.tileSize;
const tempTexture = new THREE.Data3DTexture(tileData as BufferSource, this.tileSize, this.tileSize, this.tileSize);
tempTexture.format = this.format;
tempTexture.type = this.type;
tempTexture.internalFormat = this.internalFormat;
tempTexture.needsUpdate = false;
if (this.renderer) {
try {
this.renderer.copyTextureToTexture(
tempTexture,
this.tex,
new THREE.Box3(
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(this.tileSize, this.tileSize, this.tileSize)
),
new THREE.Vector3(x, y, z),
0
);
this.tex.needsUpdate = false;
} catch (e) {
console.warn('\[AtlasManager\] copyTextureToTexture3D failed', e);
}
}
}
}