diff --git a/Sources/Rendering/OpenGL/Texture/index.d.ts b/Sources/Rendering/OpenGL/Texture/index.d.ts index 3157e6418b4..54994cdfb03 100644 --- a/Sources/Rendering/OpenGL/Texture/index.d.ts +++ b/Sources/Rendering/OpenGL/Texture/index.d.ts @@ -218,6 +218,26 @@ export interface vtkOpenGLTexture extends vtkViewNode { flip?: boolean; }): boolean; + /** + * Updates existing 2D texture data without recreating the texture. + * This method uses texSubImage2D to efficiently update texture data when + * the texture dimensions and format haven't changed, avoiding GL_INVALID_OPERATION + * errors that occur when trying to recreate immutable WebGL2 textures. + * @param data The raw data for the texture update. + * @param dataType The data type of the texture data. + * @param flip Whether to flip the texture vertically. Defaults to false. + * @returns {boolean} True if the texture was successfully updated, false otherwise. + */ + update2DFromRaw({ + data, + dataType, + flip, + }: { + data: any; + dataType: VtkDataTypes; + flip?: boolean; + }): boolean; + /** * Creates a cube texture from raw data. * @param width The width of each face of the cube texture. diff --git a/Sources/Rendering/OpenGL/Texture/index.js b/Sources/Rendering/OpenGL/Texture/index.js index 6adf753cf9e..28e7324ae65 100644 --- a/Sources/Rendering/OpenGL/Texture/index.js +++ b/Sources/Rendering/OpenGL/Texture/index.js @@ -143,7 +143,10 @@ function vtkOpenGLTexture(publicAPI, model) { const input = model.renderable.getInputData(0); if (input && input.getPointData().getScalars()) { const ext = input.getExtent(); + const newWidth = ext[1] - ext[0] + 1; + const newHeight = ext[3] - ext[2] + 1; const inScalars = input.getPointData().getScalars(); + const newComponents = inScalars.getNumberOfComponents(); // do we have a cube map? Six inputs const data = []; @@ -156,6 +159,31 @@ function vtkOpenGLTexture(publicAPI, model) { data.push(scalars); } } + + // Check if we can update existing texture without recreation. + // Avoid GL_INVALID_OPERATION: glTexStorage2D: Texture is immutable. + const isSameGeometry = + model.width === newWidth && + model.height === newHeight && + model.components === newComponents; + const canUpdate = + model.handle && + isSameGeometry && + model.numberOfDimensions === 2 && + data.length <= 1; // Not a cube map + + if (canUpdate) { + const success = publicAPI.update2DFromRaw({ + data: inScalars.getData(), + dataType: inScalars.getDataType(), + }); + if (success) { + model.textureBuildTime.modified(); + return; + } + } + + // Full texture creation needed if ( model.renderable.getInterpolate() && inScalars.getNumberOfComponents() === 4 @@ -165,17 +193,17 @@ function vtkOpenGLTexture(publicAPI, model) { } if (data.length % 6 === 0) { publicAPI.createCubeFromRaw({ - width: ext[1] - ext[0] + 1, - height: ext[3] - ext[2] + 1, - numComps: inScalars.getNumberOfComponents(), + width: newWidth, + height: newHeight, + numComps: newComponents, dataType: inScalars.getDataType(), data, }); } else { publicAPI.create2DFromRaw({ - width: ext[1] - ext[0] + 1, - height: ext[3] - ext[2] + 1, - numComps: inScalars.getNumberOfComponents(), + width: newWidth, + height: newHeight, + numComps: newComponents, dataType: inScalars.getDataType(), data: inScalars.getData(), }); @@ -1091,6 +1119,52 @@ function vtkOpenGLTexture(publicAPI, model) { return true; }; + //---------------------------------------------------------------------------- + // Update existing 2D texture data without recreating the texture + publicAPI.update2DFromRaw = ({ + data = requiredParam('data'), + dataType = requiredParam('dataType'), + flip = false, + } = {}) => { + if (!model.handle) { + vtkErrorMacro('No texture to update'); + return false; + } + + model._openGLRenderWindow.activateTexture(publicAPI); + publicAPI.bind(); + + const dataArray = [data]; + const pixData = publicAPI.updateArrayDataTypeForGL(dataType, dataArray); + const scaledData = scaleTextureToHighestPowerOfTwo(pixData); + + model.context.pixelStorei(model.context.UNPACK_FLIP_Y_WEBGL, flip); + model.context.pixelStorei(model.context.UNPACK_ALIGNMENT, 1); + + model.context.texSubImage2D( + model.target, + 0, // level + 0, // xoffset + 0, // yoffset + model.width, + model.height, + model.format, + model.openGLDataType, + scaledData[0] + ); + + if (model.generateMipmap) { + model.context.generateMipmap(model.target); + } + + if (flip) { + model.context.pixelStorei(model.context.UNPACK_FLIP_Y_WEBGL, false); + } + + publicAPI.deactivate(); + return true; + }; + //---------------------------------------------------------------------------- publicAPI.createCubeFromRaw = ({ width = requiredParam('width'), diff --git a/Sources/Rendering/OpenGL/Texture/test/testTextureDataUpdate.js b/Sources/Rendering/OpenGL/Texture/test/testTextureDataUpdate.js new file mode 100644 index 00000000000..8ce5660d935 --- /dev/null +++ b/Sources/Rendering/OpenGL/Texture/test/testTextureDataUpdate.js @@ -0,0 +1,95 @@ +/* eslint-disable no-await-in-loop */ +import test from 'tape'; +import testUtils from 'vtk.js/Sources/Testing/testUtils'; +import vtkOpenGLRenderWindow from 'vtk.js/Sources/Rendering/OpenGL/RenderWindow'; +import vtkRenderWindow from 'vtk.js/Sources/Rendering/Core/RenderWindow'; +import vtkRenderer from 'vtk.js/Sources/Rendering/Core/Renderer'; +import vtkActor from 'vtk.js/Sources/Rendering/Core/Actor'; +import vtkMapper from 'vtk.js/Sources/Rendering/Core/Mapper'; +import vtkPlaneSource from 'vtk.js/Sources/Filters/Sources/PlaneSource'; +import vtkTexture from 'vtk.js/Sources/Rendering/Core/Texture'; +import vtkImageData from 'vtk.js/Sources/Common/DataModel/ImageData'; +import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray'; + +test.onlyIfWebGL( + 'Test Texture Data Update Without GL_INVALID_OPERATION', + async (t) => { + const gc = testUtils.createGarbageCollector(); + + // Helper function to check for GL errors + // Dont want to see GL_INVALID_OPERATION: glTexStorage2D: Texture is immutable. + function checkGLError(gl) { + const error = gl.getError(); + if (error !== gl.NO_ERROR) { + const errorMsg = + error === gl.INVALID_OPERATION + ? 'GL_INVALID_OPERATION detected' + : `GL Error ${error} (0x${error.toString(16)})`; + t.fail(errorMsg); + t.end(); + return true; + } + return false; + } + + // Create render setup + const container = gc.registerDOMElement(document.createElement('div')); + document.querySelector('body').appendChild(container); + + const renderWindow = gc.registerResource(vtkRenderWindow.newInstance()); + const renderer = gc.registerResource(vtkRenderer.newInstance()); + renderWindow.addRenderer(renderer); + + const glwindow = gc.registerResource(vtkOpenGLRenderWindow.newInstance()); + glwindow.setContainer(container); + renderWindow.addView(glwindow); + glwindow.setSize(64, 64); + + // Create textured plane + const plane = gc.registerResource(vtkPlaneSource.newInstance()); + const mapper = gc.registerResource(vtkMapper.newInstance()); + mapper.setInputConnection(plane.getOutputPort()); + + const actor = gc.registerResource(vtkActor.newInstance()); + actor.setMapper(mapper); + renderer.addActor(actor); + + // Create texture with red data + const texture = gc.registerResource(vtkTexture.newInstance()); + const imageData = gc.registerResource(vtkImageData.newInstance()); + imageData.setDimensions(64, 64, 1); + + const data = new Uint8Array(64 * 64 * 3).fill(255); // red + const scalars = gc.registerResource( + vtkDataArray.newInstance({ + numberOfComponents: 3, + values: data, + }) + ); + imageData.getPointData().setScalars(scalars); + texture.setInputData(imageData); + actor.addTexture(texture); + + renderWindow.render(); + + // Update texture to green + data.fill(0); + for (let i = 1; i < data.length; i += 3) { + data[i] = 255; // green channel + } + scalars.modified(); + imageData.modified(); + texture.modified(); + + renderWindow.render(); + + // Check for GL errors + if (checkGLError(glwindow.getContext())) { + gc.releaseResources(); + return; + } + + t.pass('No GL errors occurred during texture update'); + gc.releaseResources(); + } +);