|
| 1 | +import macro from 'vtk.js/Sources/macros'; |
| 2 | +import { extend as extendOpenGLRenderWindow } from 'vtk.js/Sources/Rendering/OpenGL/RenderWindow'; |
| 3 | +import vtkSharedRenderer from 'vtk.js/Sources/Rendering/OpenGL/SharedRenderer'; |
| 4 | + |
| 5 | +const PIXEL_STORE_STATE = [ |
| 6 | + ['packAlignment', 'PACK_ALIGNMENT', 4], |
| 7 | + ['unpackAlignment', 'UNPACK_ALIGNMENT', 4], |
| 8 | + ['unpackFlipY', 'UNPACK_FLIP_Y_WEBGL', false], |
| 9 | + ['unpackPremultiplyAlpha', 'UNPACK_PREMULTIPLY_ALPHA_WEBGL', false], |
| 10 | + [ |
| 11 | + 'unpackColorspaceConversion', |
| 12 | + 'UNPACK_COLORSPACE_CONVERSION_WEBGL', |
| 13 | + 'BROWSER_DEFAULT_WEBGL', |
| 14 | + ], |
| 15 | + ['packRowLength', 'PACK_ROW_LENGTH', 0], |
| 16 | + ['packSkipRows', 'PACK_SKIP_ROWS', 0], |
| 17 | + ['packSkipPixels', 'PACK_SKIP_PIXELS', 0], |
| 18 | + ['unpackRowLength', 'UNPACK_ROW_LENGTH', 0], |
| 19 | + ['unpackImageHeight', 'UNPACK_IMAGE_HEIGHT', 0], |
| 20 | + ['unpackSkipRows', 'UNPACK_SKIP_ROWS', 0], |
| 21 | + ['unpackSkipPixels', 'UNPACK_SKIP_PIXELS', 0], |
| 22 | + ['unpackSkipImages', 'UNPACK_SKIP_IMAGES', 0], |
| 23 | +]; |
| 24 | + |
| 25 | +function getSupportedState(gl, stateSpecs) { |
| 26 | + return stateSpecs.filter(([, valueName]) => gl[valueName] !== undefined); |
| 27 | +} |
| 28 | + |
| 29 | +function isWebGL2Context(gl) { |
| 30 | + return ( |
| 31 | + typeof WebGL2RenderingContext !== 'undefined' && |
| 32 | + gl instanceof WebGL2RenderingContext |
| 33 | + ); |
| 34 | +} |
| 35 | + |
| 36 | +function resetGLState(gl, shaderCache) { |
| 37 | + const pixelStoreState = getSupportedState(gl, PIXEL_STORE_STATE); |
| 38 | + |
| 39 | + gl.disable(gl.BLEND); |
| 40 | + gl.disable(gl.CULL_FACE); |
| 41 | + gl.disable(gl.DEPTH_TEST); |
| 42 | + gl.disable(gl.POLYGON_OFFSET_FILL); |
| 43 | + gl.disable(gl.SCISSOR_TEST); |
| 44 | + gl.disable(gl.STENCIL_TEST); |
| 45 | + if (gl.SAMPLE_ALPHA_TO_COVERAGE) { |
| 46 | + gl.disable(gl.SAMPLE_ALPHA_TO_COVERAGE); |
| 47 | + } |
| 48 | + |
| 49 | + gl.blendEquation(gl.FUNC_ADD); |
| 50 | + gl.blendFunc(gl.ONE, gl.ZERO); |
| 51 | + gl.blendFuncSeparate(gl.ONE, gl.ZERO, gl.ONE, gl.ZERO); |
| 52 | + gl.blendColor(0, 0, 0, 0); |
| 53 | + |
| 54 | + gl.colorMask(true, true, true, true); |
| 55 | + gl.clearColor(0, 0, 0, 0); |
| 56 | + |
| 57 | + gl.depthMask(true); |
| 58 | + gl.depthFunc(gl.LESS); |
| 59 | + gl.clearDepth(1); |
| 60 | + |
| 61 | + gl.stencilMask(0xffffffff); |
| 62 | + gl.stencilFunc(gl.ALWAYS, 0, 0xffffffff); |
| 63 | + gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP); |
| 64 | + gl.clearStencil(0); |
| 65 | + |
| 66 | + gl.cullFace(gl.BACK); |
| 67 | + gl.frontFace(gl.CCW); |
| 68 | + |
| 69 | + gl.polygonOffset(0, 0); |
| 70 | + |
| 71 | + gl.activeTexture(gl.TEXTURE0); |
| 72 | + |
| 73 | + pixelStoreState.forEach(([, paramName, defaultValue]) => { |
| 74 | + const value = |
| 75 | + typeof defaultValue === 'string' ? gl[defaultValue] : defaultValue; |
| 76 | + gl.pixelStorei(gl[paramName], value); |
| 77 | + }); |
| 78 | + |
| 79 | + if (gl.bindRenderbuffer) { |
| 80 | + gl.bindRenderbuffer(gl.RENDERBUFFER, null); |
| 81 | + } |
| 82 | + |
| 83 | + gl.useProgram(null); |
| 84 | + |
| 85 | + gl.lineWidth(1); |
| 86 | + |
| 87 | + const width = gl.drawingBufferWidth; |
| 88 | + const height = gl.drawingBufferHeight; |
| 89 | + gl.scissor(0, 0, width, height); |
| 90 | + gl.viewport(0, 0, width, height); |
| 91 | + |
| 92 | + if (gl.bindVertexArray) { |
| 93 | + gl.bindVertexArray(null); |
| 94 | + } |
| 95 | + |
| 96 | + if (shaderCache) { |
| 97 | + shaderCache.setLastShaderProgramBound(null); |
| 98 | + } |
| 99 | +} |
| 100 | + |
| 101 | +function vtkSharedRenderWindow(publicAPI, model) { |
| 102 | + model.classHierarchy.push('vtkSharedRenderWindow'); |
| 103 | + let renderEventSubscription = null; |
| 104 | + let renderCallback = null; |
| 105 | + let suppressRenderEvent = false; |
| 106 | + let savedEnableRender = null; |
| 107 | + const superGet3DContext = publicAPI.get3DContext; |
| 108 | + |
| 109 | + function getInteractor() { |
| 110 | + return model.renderable?.getInteractor?.(); |
| 111 | + } |
| 112 | + |
| 113 | + function clearRenderEventSubscription() { |
| 114 | + if (renderEventSubscription) { |
| 115 | + renderEventSubscription.unsubscribe(); |
| 116 | + renderEventSubscription = null; |
| 117 | + } |
| 118 | + } |
| 119 | + |
| 120 | + function bindRenderEvent(interactor) { |
| 121 | + if (!interactor?.onRenderEvent || !renderCallback) { |
| 122 | + return; |
| 123 | + } |
| 124 | + |
| 125 | + renderEventSubscription = interactor.onRenderEvent(() => { |
| 126 | + if (!suppressRenderEvent) { |
| 127 | + renderCallback?.(); |
| 128 | + } |
| 129 | + }); |
| 130 | + } |
| 131 | + |
| 132 | + publicAPI.renderShared = (options = {}) => { |
| 133 | + publicAPI.prepareSharedRender(options); |
| 134 | + try { |
| 135 | + if (model.renderable) { |
| 136 | + if (renderCallback && !renderEventSubscription) { |
| 137 | + publicAPI.setRenderCallback(renderCallback); |
| 138 | + } |
| 139 | + |
| 140 | + const interactor = getInteractor(); |
| 141 | + let previousEnableRender; |
| 142 | + if (interactor?.getEnableRender) { |
| 143 | + previousEnableRender = interactor.getEnableRender(); |
| 144 | + if (!previousEnableRender) { |
| 145 | + interactor.setEnableRender(true); |
| 146 | + } |
| 147 | + } |
| 148 | + |
| 149 | + suppressRenderEvent = true; |
| 150 | + try { |
| 151 | + model.renderable.preRender?.(); |
| 152 | + if (interactor) { |
| 153 | + interactor.render(); |
| 154 | + } else { |
| 155 | + const views = model.renderable.getViews?.() || []; |
| 156 | + views.forEach((view) => view.traverseAllPasses()); |
| 157 | + } |
| 158 | + } finally { |
| 159 | + suppressRenderEvent = false; |
| 160 | + if ( |
| 161 | + interactor?.setEnableRender && |
| 162 | + previousEnableRender !== undefined |
| 163 | + ) { |
| 164 | + interactor.setEnableRender(previousEnableRender); |
| 165 | + } |
| 166 | + } |
| 167 | + } |
| 168 | + } finally { |
| 169 | + const shaderCache = publicAPI.getShaderCache(); |
| 170 | + if (shaderCache) { |
| 171 | + shaderCache.setLastShaderProgramBound(null); |
| 172 | + } |
| 173 | + } |
| 174 | + }; |
| 175 | + |
| 176 | + publicAPI.get3DContext = (options) => { |
| 177 | + if (model.context) { |
| 178 | + return model.context; |
| 179 | + } |
| 180 | + return superGet3DContext(options); |
| 181 | + }; |
| 182 | + |
| 183 | + /** |
| 184 | + * Sync internal size state from the canvas's actual drawing buffer dimensions. |
| 185 | + * Use this when sharing a WebGL context with another library (like MapLibre) |
| 186 | + * that manages the canvas size. Returns true if size changed. |
| 187 | + */ |
| 188 | + publicAPI.syncSizeFromCanvas = () => { |
| 189 | + if (!model.context) return false; |
| 190 | + const width = model.context.drawingBufferWidth; |
| 191 | + const height = model.context.drawingBufferHeight; |
| 192 | + return publicAPI.setSize(width, height); |
| 193 | + }; |
| 194 | + |
| 195 | + publicAPI.prepareSharedRender = () => { |
| 196 | + publicAPI.syncSizeFromCanvas(); |
| 197 | + const gl = model.context; |
| 198 | + if (!gl) return; |
| 199 | + resetGLState(gl, publicAPI.getShaderCache()); |
| 200 | + }; |
| 201 | + |
| 202 | + publicAPI.setRenderCallback = (callback) => { |
| 203 | + renderCallback = callback || null; |
| 204 | + clearRenderEventSubscription(); |
| 205 | + |
| 206 | + const interactor = getInteractor(); |
| 207 | + if (renderCallback && interactor?.onRenderEvent) { |
| 208 | + // Render requests flow through the interactor RenderEvent; redirect those |
| 209 | + // to the host render loop while keeping draw calls inside renderShared(). |
| 210 | + if (savedEnableRender === null && interactor.getEnableRender) { |
| 211 | + savedEnableRender = interactor.getEnableRender(); |
| 212 | + } |
| 213 | + interactor?.setEnableRender?.(false); |
| 214 | + bindRenderEvent(interactor); |
| 215 | + return; |
| 216 | + } |
| 217 | + |
| 218 | + if (!renderCallback && interactor && savedEnableRender !== null) { |
| 219 | + interactor.setEnableRender?.(savedEnableRender); |
| 220 | + savedEnableRender = null; |
| 221 | + } |
| 222 | + }; |
| 223 | +} |
| 224 | + |
| 225 | +const DEFAULT_VALUES = { |
| 226 | + autoClear: false, |
| 227 | + autoClearColor: true, |
| 228 | + autoClearDepth: true, |
| 229 | +}; |
| 230 | + |
| 231 | +export function extend(publicAPI, model, initialValues = {}) { |
| 232 | + const mergedValues = { ...DEFAULT_VALUES, ...initialValues }; |
| 233 | + extendOpenGLRenderWindow(publicAPI, model, mergedValues); |
| 234 | + macro.setGet(publicAPI, model, [ |
| 235 | + 'autoClear', |
| 236 | + 'autoClearColor', |
| 237 | + 'autoClearDepth', |
| 238 | + ]); |
| 239 | + vtkSharedRenderWindow(publicAPI, model); |
| 240 | + publicAPI |
| 241 | + .getViewNodeFactory() |
| 242 | + .registerOverride('vtkRenderer', vtkSharedRenderer.newInstance); |
| 243 | +} |
| 244 | + |
| 245 | +export const newInstance = macro.newInstance(extend, 'vtkSharedRenderWindow'); |
| 246 | + |
| 247 | +export function createFromContext(canvas, gl, options = {}) { |
| 248 | + if (!isWebGL2Context(gl)) { |
| 249 | + throw new Error('vtkSharedRenderWindow requires a WebGL2 context'); |
| 250 | + } |
| 251 | + if (gl.canvas && gl.canvas !== canvas) { |
| 252 | + throw new Error( |
| 253 | + 'vtkSharedRenderWindow requires the provided canvas to match gl.canvas' |
| 254 | + ); |
| 255 | + } |
| 256 | + |
| 257 | + return newInstance({ |
| 258 | + ...options, |
| 259 | + canvas, |
| 260 | + context: gl, |
| 261 | + manageCanvas: false, |
| 262 | + webgl2: true, |
| 263 | + }); |
| 264 | +} |
| 265 | + |
| 266 | +export default { newInstance, extend, createFromContext }; |
0 commit comments