WebGL 踩坑记:上下文数量限制

1,713 阅读2分钟

背景——在Electron工程中,我创建了大量的视频播放窗口(大约50多个吧😥😥),结果发现只能显示最新的16个视频渲染窗口。同时,Developer Tools 弹出了警告:three.module.js:23675 WARNING: Too many active WebGL contexts. Oldest context will be lost.

原因探究

在使用 WebGL 的应用(例如基于 Three.js 的应用)时,“Too many active WebGL contexts” 警告提示我们当前已经创建了过多的 WebGL 上下文。大多数浏览器对同时存在的 WebGL 上下文数量有硬性限制,通常是 8 到 16 个。(参考讨论: Mozilla Bugzilla)

不同浏览器对 WebGL 上下文数量的限制略有不同:

  • Chrome: 16 个上下文
  • Firefox: 8 个上下文 (移动端为 4 个)
  • Safari: 16 个上下文

这些限制可能会随着浏览器版本的更新而改变。

当超过这个限制时,最旧的 WebGL 上下文会被丢弃,那么它所渲染的内容也就随之丢失啦。

解决策略

  • 复用 WebGL 上下文

    复用现有的 WebGL 上下文,而不是为每个视频流创建新的上下文。通过共享同一个 WebGLRenderer 实例,可以大幅减少 WebGL 上下文的数量(但需要注意的是,可能会导致渲染设置的冲突)。

    import { Injectable } from '@angular/core';
    import * as THREE from 'three';
    
    @Injectable({
        providedIn:'root',
    })
    
    export class WebGLRendererService {
        private renderer: THREE.WebGLRenderer;
    
        constructor () {
            this.renderer = new THREE.WebGLRenderer();
            this.renderer.setSize(window.innerWidth, window.innerHeight);
            document.body.appendChild(this.renderer.domElement);
        }
    
        getRenderer(): THREE.WebGLRenderer {
            return this.renderer;
        }
    }
    
    export class MyComponent implements OnInit {
        @Input () renderer: THREE.WebGLRenderer;
    
        constructor(private rendererService: WebGLRendererService) {}
    
        ngOnInit(){
            this.renderer = this.rendererService.getRenderer();
        }
    }
    
  • 销毁 WebGL 上下文

    当不再需要某些资源时,务必及时销毁其对应的 WebGL 上下文,以释放资源。

    const gl = this.canvas.nativeElement,getContext('webgl');
    gl.getExtension('WEBGL_lose_context').loseContext ();
    
  • 限制组件数量

    通过分页加载或懒加载的方式来限制一次性展示的组件数量,只显示当前可见范围内的资源,其他资源则延迟加载或按需加载(但是在需要快速切换或预览大量内容的场景中可能会影响体验吧)。

  • 动态管理上下文

    为了更高效地管理 WebGL 上下文,可以实现一个动态管理系统,根据当前的上下文使用情况来决定是否创建新的上下文或复用现有上下文。

    export class WebGLContextManager {
        private contexts: Map<string, WebGLRenderingContext> = new Map();
        private maxContexts = 16;
    
        getContext(id: string): WebGLRenderingContext {
            if (this.contexts.has(id)) {
                return this.contexts.get(id);
            } else if (this.contexts.size < this.maxContexts) {
                const newContext = this.createNewContext();
                this.contexts.set(id, newContext);
                return newContext;
            } else {
                const oldestId = this.getOldestContextId();
                this.destroyContext(oldestId);
                const newContext = this.createNewContext();
                this.contexts.set(id, newContext);
                return newContext;
            }
        }
    
        private createNewContext(): WebGLRenderingContext {
            const canvas = document.createElement('canvas');
            return canvas.getContext('webgl');
        }
    
        private destroyContext(id: string): void {
            const context = this.contexts.get(id);
            context.getExtension('WEBGL_lose_context').loseContext();
            this.contexts.delete(id);
        }
    
        private getOldestContextId(): string {
            return this.contexts.keys().next().value;
        }
    }
    

其实我最后就是简单限制了一下数量解决的问题😭😭