You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

GL_INVALID_OPERATION错误:片段着色器输出缺失致模型变色异常

问题描述

我正在开发Autodesk Viewer自定义扩展,尝试用THREE.MeshPhongMaterial修改模型特定部件颜色,对象识别正常,但材质显示出错,模型变透明,浏览器控制台报错:GL_INVALID_OPERATION: Active draw buffers with missing fragment shader outputs。相关JavaScript代码如下:

class ChangeColorCustom extends Autodesk.Viewing.Extension {
    constructor(viewer, options) {
        super(viewer, options);
        this._colorPickerId = this.guid();
        this.addCSS();
    }

    addCSS() {
        const link = $('<link/>', {
            rel: 'stylesheet',
            type: 'text/css',
            href: '/extensions/ChangeColorCustom/contents/main.css'
        });
        link.appendTo('head');
    }

    load() {
        this.viewer.addEventListener(
            Autodesk.Viewing.SELECTION_CHANGED_EVENT,
            this.onItemSelected.bind(this)
        );

        this.createUI();

        return true;
    }

    unload() {
        console.log("ChangeColorCustom unloaded");
        this.viewer.removeEventListener(
            Autodesk.Viewing.SELECTION_CHANGED_EVENT,
            this.onItemSelected
        );
        $(`#${this._colorPickerId}-container`).remove();
        return true;
    }

    createUI() {
        const container = this.viewer.container;

        if (container) {
            const htmlString = `
            <div id="${this._colorPickerId}-container" style="position: absolute; right: 25%; top: 5%; z-index: 100;">
                <input type="text" id="${this._colorPickerId}-DBIdInput" placeholder="Enter DBID">
                <input type="range" id="${this._colorPickerId}-opacitySlider" min="0" max="1" step="0.01" value="1">
                <button id="${this._colorPickerId}-applyColorButton">Apply Color</button>
                <button id="${this._colorPickerId}-resetButton">Reset Color</button>
            </div>
        `;
            container.insertAdjacentHTML('beforeend', htmlString);

            // Apply color
            $(`#${this._colorPickerId}-applyColorButton`).click(() => {
                const dbId = parseInt($(`#${this._colorPickerId}-DBIdInput`).val(), 10);
                const opacity = parseFloat($(`#${this._colorPickerId}-opacitySlider`).val());
                if (!isNaN(dbId)) {
                    // set the color to `#ffffff`
                    const fixedColor = new THREE.Color("#ffffff");
                    this.changeColor(dbId, fixedColor, opacity);
                }
            });

            // reset button
            $(`#${this._colorPickerId}-resetButton`).click(() => {
                const dbId = parseInt($(`#${this._colorPickerId}-DBIdInput`).val(), 10);
                if (!isNaN(dbId)) {
                    this.resetColor(dbId);
                }
            });
        }
    }

    changeColor(dbId, color, opacity) {
        const material = new THREE.MeshPhongMaterial({
            color: color,
            transparent: true,
            opacity: opacity
        });

        material.colorWrite = true;

        const instanceTree = this.viewer.model.getData().instanceTree;
        instanceTree.enumNodeFragments(dbId, (fragId) => {
            const proxy = this.viewer.impl.getFragmentProxy(this.viewer.model, fragId);
            proxy.setMaterial(material);
            proxy.updateAnimTransform();
        }, true);

        this.viewer.impl.invalidate(true, true, true);
    }

    resetColor(dbId) {
        // Reset to the default material or reset logic here.
    }

    guid() {
        const d = new Date().getTime();
        return 'xxxx-xxxx-xxxx-xxxx'.replace(/[xy]/g, (c) => {
            const r = (d + Math.random() * 16) % 16 | 0;
            return (c === 'x' ? r : (r & 0x7 | 0x8)).toString(16);
        });
    }

    onItemSelected(event) {
        if (event.dbIdArray.length > 0) {
            const dbId = event.dbIdArray[0];
            this.viewer.getProperties(dbId, (props) => {
                $(`#${this._colorPickerId}-DBIdInput`).val(props.externalId || props.dbId);
            }, (error) => {
                console.error('Error getting properties:', error);
            });
        }
    }
}

Autodesk.Viewing.theExtensionManager.registerExtension(
    'ChangeColorCustom',
    ChangeColorCustom
);
错误含义

这个WebGL错误的核心原因是:Autodesk Viewer的渲染管线采用**多渲染目标(Multiple Render Targets, MRT)**机制,要求片段着色器输出多个缓冲区数据(如基础颜色、深度信息、自定义渲染通道等)。但直接使用原生THREE.MeshPhongMaterial替换时,其默认着色器仅输出基础颜色,缺少Viewer渲染管线依赖的其他输出通道,导致WebGL上下文抛出操作无效错误,最终出现模型透明的渲染异常。

解决步骤
  • 禁止直接使用原生THREE.js材质替换Viewer的片段代理材质,必须基于Viewer内置的材质模板创建兼容材质。
  • 使用this.viewer.impl.phongMaterial()等Viewer提供的材质构造方法,这类材质已预先适配多渲染目标要求。
  • 替换材质前,存储原片段的默认材质,用于后续重置操作。
  • 根据透明度需求动态设置transparent属性,避免不必要的透明渲染开销。
修改后的完整代码
class ChangeColorCustom extends Autodesk.Viewing.Extension {
    constructor(viewer, options) {
        super(viewer, options);
        this._colorPickerId = this.guid();
        this._originalMaterials = new Map(); // 存储原材质用于重置
        this.addCSS();
    }

    addCSS() {
        const link = $('<link/>', {
            rel: 'stylesheet',
            type: 'text/css',
            href: '/extensions/ChangeColorCustom/contents/main.css'
        });
        link.appendTo('head');
    }

    load() {
        this.viewer.addEventListener(
            Autodesk.Viewing.SELECTION_CHANGED_EVENT,
            this.onItemSelected.bind(this)
        );

        this.createUI();

        return true;
    }

    unload() {
        console.log("ChangeColorCustom unloaded");
        this.viewer.removeEventListener(
            Autodesk.Viewing.SELECTION_CHANGED_EVENT,
            this.onItemSelected
        );
        $(`#${this._colorPickerId}-container`).remove();
        this._originalMaterials.clear(); // 清理存储的原材质
        return true;
    }

    createUI() {
        const container = this.viewer.container;

        if (container) {
            const htmlString = `
            <div id="${this._colorPickerId}-container" style="position: absolute; right: 25%; top: 5%; z-index: 100;">
                <input type="text" id="${this._colorPickerId}-DBIdInput" placeholder="Enter DBID">
                <input type="range" id="${this._colorPickerId}-opacitySlider" min="0" max="1" step="0.01" value="1">
                <button id="${this._colorPickerId}-applyColorButton">Apply Color</button>
                <button id="${this._colorPickerId}-resetButton">Reset Color</button>
            </div>
        `;
            container.insertAdjacentHTML('beforeend', htmlString);

            // Apply color
            $(`#${this._colorPickerId}-applyColorButton`).click(() => {
                const dbId = parseInt($(`#${this._colorPickerId}-DBIdInput`).val(), 10);
                const opacity = parseFloat($(`#${this._colorPickerId}-opacitySlider`).val());
                if (!isNaN(dbId)) {
                    const fixedColor = new THREE.Color("#ffffff");
                    this.changeColor(dbId, fixedColor, opacity);
                }
            });

            // reset button
            $(`#${this._colorPickerId}-resetButton`).click(() => {
                const dbId = parseInt($(`#${this._colorPickerId}-DBIdInput`).val(), 10);
                if (!isNaN(dbId)) {
                    this.resetColor(dbId);
                }
            });
        }
    }

    changeColor(dbId, color, opacity) {
        const instanceTree = this.viewer.model.getData().instanceTree;
        instanceTree.enumNodeFragments(dbId, (fragId) => {
            const proxy = this.viewer.impl.getFragmentProxy(this.viewer.model, fragId);
            // 保存原材质
            if (!this._originalMaterials.has(fragId)) {
                this._originalMaterials.set(fragId, proxy.getMaterial());
            }
            // 创建Viewer兼容的Phong材质
            const material = new this.viewer.impl.phongMaterial();
            material.color.set(color);
            material.opacity = opacity;
            material.transparent = opacity < 1;
            material.colorWrite = true;
            material.depthWrite = true;
            // 应用材质
            proxy.setMaterial(material);
            proxy.updateAnimTransform();
        }, true);

        this.viewer.impl.invalidate(true, true, true);
    }

    resetColor(dbId) {
        const instanceTree = this.viewer.model.getData().instanceTree;
        instanceTree.enumNodeFragments(dbId, (fragId) => {
            const proxy = this.viewer.impl.getFragmentProxy(this.viewer.model, fragId);
            // 恢复原材质
            const originalMat = this._originalMaterials.get(fragId);
            if (originalMat) {
                proxy.setMaterial(originalMat);
                proxy.updateAnimTransform();
                this._originalMaterials.delete(fragId);
            }
        }, true);

        this.viewer.impl.invalidate(true, true, true);
    }

    guid() {
        const d = new Date().getTime();
        return 'xxxx-xxxx-xxxx-xxxx'.replace(/[xy]/g, (c) => {
            const r = (d + Math.random() * 16) % 16 | 0;
            return (c === 'x' ? r : (r & 0x7 | 0x8)).toString(16);
        });
    }

    onItemSelected(event) {
        if (event.dbIdArray.length > 0) {
            const dbId = event.dbIdArray[0];
            this.viewer.getProperties(dbId, (props) => {
                $(`#${this._colorPickerId}-DBIdInput`).val(props.externalId || props.dbId);
            }, (error) => {
                console.error('Error getting properties:', error);
            });
        }
    }
}

Autodesk.Viewing.theExtensionManager.registerExtension(
    'ChangeColorCustom',
    ChangeColorCustom
);
关键改动说明
  • 新增_originalMaterials Map,存储每个片段的原材质,确保重置功能正常。
  • 使用this.viewer.impl.phongMaterial()创建兼容Viewer渲染管线的Phong材质,避免缺失片段着色器输出。
  • 动态设置transparent属性:仅当opacity小于1时启用透明渲染,优化性能。
  • 完善重置逻辑,准确恢复原材质。

内容的提问来源于stack exchange,提问作者ryoyu tanaka

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 19:42:08