• cesium 源码解析 ShaderProgram


    // 着色程序id

    var nextShaderProgramId = 0;

    /**

     * @private

     */

    // 着色程序

    function ShaderProgram(options) {

      // 顶点、像所着色程序

      var vertexShaderText = options.vertexShaderText;

      var fragmentShaderText = options.fragmentShaderText;

      if (typeof spector !== "undefined") {

        // The #line statements common in Cesium shaders interfere with the ability of the

        // SpectorJS to show errors on the correct line. So remove them when SpectorJS

        // is active.

        // 注释

        vertexShaderText = vertexShaderText.replace(/^#line/gm, "//#line");

        fragmentShaderText = fragmentShaderText.replace(/^#line/gm, "//#line");

      }

      // vs和fs中都有uniform但是精度不同

      var modifiedFS = handleUniformPrecisionMismatches(

        vertexShaderText,

        fragmentShaderText

      );

      // 上下文谢谢

      this._gl = options.gl;

      // 编译是否错误的日志

      this._logShaderCompilation = options.logShaderCompilation;

      // gl的扩展功能,用来获取shader中的源码字符串

      this._debugShaders = options.debugShaders;

      // 属性和位置

      this._attributeLocations = options.attributeLocations;

      // 着色程序

      this._program = undefined;

      // 顶点属性数量

      this._numberOfVertexAttributes = undefined;

      // 顶点属性对象

      this._vertexAttributes = undefined;

      // uniform与对应名称的对象

      this._uniformsByName = undefined;

      // uniform数组

      this._uniforms = undefined;

      // 自动uniform对象

      this._automaticUniforms = undefined;

      // 手动uniform对象

      this._manualUniforms = undefined;

      // 修改过的uniform名字

      this._duplicateUniformNames = modifiedFS.duplicateUniformNames;

      this._cachedShader = undefined; // Used by ShaderCache

      /**

       * @private

       */

      // 最大纹理单元索引

      this.maximumTextureUnitIndex = undefined;

      // 顶点着色器源码和处理过的代码

      this._vertexShaderSource = options.vertexShaderSource;

      this._vertexShaderText = options.vertexShaderText;

      // 像素着色器源码和处理过的代码

      this._fragmentShaderSource = options.fragmentShaderSource;

      this._fragmentShaderText = modifiedFS.fragmentShaderText;

      /**

       * @private

       */

      // 程序id全局唯一

      this.id = nextShaderProgramId++;

    }

    // 缓存中获取着色程序(如果已经存在,不存在就创建)

    ShaderProgram.fromCache = function (options) {

      options = defaultValue(options, defaultValue.EMPTY_OBJECT);

      //>>includeStart('debug', pragmas.debug);

      Check.defined("options.context", options.context);

      //>>includeEnd('debug');

      // 缓存中获取着色程序(如果已经存在,不存在就创建)

      return options.context.shaderCache.getShaderProgram(options);

    };

    // 替换着色程序

    ShaderProgram.replaceCache = function (options) {

      options = defaultValue(options, defaultValue.EMPTY_OBJECT);

      //>>includeStart('debug', pragmas.debug);

      Check.defined("options.context", options.context);

      //>>includeEnd('debug');

      return options.context.shaderCache.replaceShaderProgram(options);

    };

    Object.defineProperties(ShaderProgram.prototype, {

      /**

       * GLSL source for the shader program's vertex shader.

       * @memberof ShaderProgram.prototype

       *

       * @type {ShaderSource}

       * @readonly

       */

      vertexShaderSource: {

        get: function () {

          return this._vertexShaderSource;

        },

      },

      /**

       * GLSL source for the shader program's fragment shader.

       * @memberof ShaderProgram.prototype

       *

       * @type {ShaderSource}

       * @readonly

       */

      // 得到像素着色器

      fragmentShaderSource: {

        get: function () {

          return this._fragmentShaderSource;

        },

      },

      // 得到顶点属性

      vertexAttributes: {

        get: function () {

          initialize(this);

          return this._vertexAttributes;

        },

      },

      // 得到顶点属性数量

      numberOfVertexAttributes: {

        get: function () {

          initialize(this);

          return this._numberOfVertexAttributes;

        },

      },

      // 得到uniforms

      allUniforms: {

        get: function () {

          initialize(this);

          return this._uniformsByName;

        },

      },

    });

    // 查找glsl中的uniform变量

    function extractUniforms(shaderText) {

      var uniformNames = [];

      var uniformLines = shaderText.match(/uniform.*?(?![^{]*})(?=[=\[;])/g);

      if (defined(uniformLines)) {

        var len = uniformLines.length;

        for (var i = 0; i < len; i++) {

          var line = uniformLines[i].trim();

          var name = line.slice(line.lastIndexOf(" ") + 1);

          uniformNames.push(name);

        }

      }

      return uniformNames;

    }

    function handleUniformPrecisionMismatches(

      vertexShaderText,

      fragmentShaderText

    ) {

      // If a uniform exists in both the vertex and fragment shader but with different precision qualifiers,

      // give the fragment shader uniform a different name. This fixes shader compilation errors on devices

      // that only support mediump in the fragment shader.

      /*

      如果统一存在于顶点和片段着色器中,但具有不同的精度限定符,请为片段着色器统一指定不同的名称。这修复了在片段着色器中

      仅支持mediump的设备上的着色器编译错误。

      */

      var duplicateUniformNames = {};

      // 不支持高精度

      if (!ContextLimits.highpFloatSupported || !ContextLimits.highpIntSupported) {

        var i, j;

        var uniformName;

        var duplicateName;

        // 查找glsl中的uniform变量

        var vertexShaderUniforms = extractUniforms(vertexShaderText);

        var fragmentShaderUniforms = extractUniforms(fragmentShaderText);

        var vertexUniformsCount = vertexShaderUniforms.length;

        var fragmentUniformsCount = fragmentShaderUniforms.length;

        // 查重

        for (i = 0; i < vertexUniformsCount; i++) {

          for (j = 0; j < fragmentUniformsCount; j++) {

            // 顶点着色器和像素着色器中都存在这个uniform

            if (vertexShaderUniforms[i] === fragmentShaderUniforms[j]) {

              // 获取值

              uniformName = vertexShaderUniforms[i];

              // 名字上拼接中精度前缀czm_mediump_

              duplicateName = "czm_mediump_" + uniformName;

              // Update fragmentShaderText with renamed uniforms

              // 替换shader中的相关字符

              var re = new RegExp(uniformName + "\\b", "g");

              fragmentShaderText = fragmentShaderText.replace(re, duplicateName);

              // 存储对应关系

              duplicateUniformNames[duplicateName] = uniformName;

            }

          }

        }

      }

      // 返回新的shader和

      return {

        fragmentShaderText: fragmentShaderText,

        duplicateUniformNames: duplicateUniformNames,

      };

    }

    // 调试信息前缀,glsl错误前缀

    var consolePrefix = "[Cesium WebGL] ";

    // 创建着色器、连接到着色程序

    function createAndLinkProgram(gl, shader) {

      // 顶点着色器代码

      var vsSource = shader._vertexShaderText;

      // 像素着色器代码

      var fsSource = shader._fragmentShaderText;

      // 创建、编译顶点着色器

      var vertexShader = gl.createShader(gl.VERTEX_SHADER);

      gl.shaderSource(vertexShader, vsSource);

      gl.compileShader(vertexShader);

      // 创建、编译像素着色器

      var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);

      gl.shaderSource(fragmentShader, fsSource);

      gl.compileShader(fragmentShader);

      //创建着色程序、绑定着色器

      var program = gl.createProgram();

      gl.attachShader(program, vertexShader);

      gl.attachShader(program, fragmentShader);

      // 删除着色器

      gl.deleteShader(vertexShader);

      gl.deleteShader(fragmentShader);

      // 属性位置信息

      var attributeLocations = shader._attributeLocations;

      if (defined(attributeLocations)) {

        // 遍历属性位置信息

        for (var attribute in attributeLocations) {

          if (attributeLocations.hasOwnProperty(attribute)) {

            // 绑定属性位置信息

            gl.bindAttribLocation(

              program,                        // 着色程序

              attributeLocations[attribute],  // 属性位置(layout(1))

              attribute                       // 属性名 (position:)

            );

          }

        }

      }

      // 连接程序

      gl.linkProgram(program);

      var log;

      // 获取连接状态

      if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {

        // glsl扩展的调试信息,用来获取着色器源码

        var debugShaders = shader._debugShaders;

        // For performance, only check compile errors if there is a linker error.

        // 提高执行效率,只是检查连接状态

        // 得到像素着色器错误

        if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {

          // 获取错误日志

          log = gl.getShaderInfoLog(fragmentShader);

          // 错误日志

          console.error(consolePrefix + "Fragment shader compile log: " + log);

          // 定义了调试信息

          if (defined(debugShaders)) {

            // 获取着色器源码

            var fragmentSourceTranslation = debugShaders.getTranslatedShaderSource(

              fragmentShader

            );

            // 打印像素着色器源码

            if (fragmentSourceTranslation !== "") {

              console.error(

                consolePrefix +

                  "Translated fragment shader source:\n" +

                  fragmentSourceTranslation

              );

            } else {

              console.error(consolePrefix + "Fragment shader translation failed.");

            }

          }

          // 删除着色程序

          gl.deleteProgram(program);

          // 异常中断

          throw new RuntimeError(

            "Fragment shader failed to compile.  Compile log: " + log

          );

        }

        // 得到顶点着色器错误

        if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {

          // 获取错误日志

          log = gl.getShaderInfoLog(vertexShader);

          // 错误日志

          console.error(consolePrefix + "Vertex shader compile log: " + log);

          // 定义了调试信息

          if (defined(debugShaders)) {

            // 获取着色器源码

            var vertexSourceTranslation = debugShaders.getTranslatedShaderSource(

              vertexShader

            );

            // 打印像素着色器源码

            if (vertexSourceTranslation !== "") {

              console.error(

                consolePrefix +

                  "Translated vertex shader source:\n" +

                  vertexSourceTranslation

              );

            } else {

              console.error(consolePrefix + "Vertex shader translation failed.");

            }

          }

          // 删除着色程序

          gl.deleteProgram(program);

          // 异常中断

          throw new RuntimeError(

            "Vertex shader failed to compile.  Compile log: " + log

          );

        }

        // 得到着色程序错误

        log = gl.getProgramInfoLog(program);

        // 打印日志

        console.error(consolePrefix + "Shader program link log: " + log);

        if (defined(debugShaders)) {

          // 打印顶点着色器源码

          console.error(

            consolePrefix +

              "Translated vertex shader source:\n" +

              debugShaders.getTranslatedShaderSource(vertexShader)

          );

          // 打印像素着色器源码

          console.error(

            consolePrefix +

              "Translated fragment shader source:\n" +

              debugShaders.getTranslatedShaderSource(fragmentShader)

          );

        }

        // 删除着色程序

        gl.deleteProgram(program);

        // 异常中断

        throw new RuntimeError("Program failed to link.  Link log: " + log);

      }

      // 着色器编译日志是否启用

      var logShaderCompilation = shader._logShaderCompilation;

      // 得到顶点着色器编译日志

      if (logShaderCompilation) {

        log = gl.getShaderInfoLog(vertexShader);

        if (defined(log) && log.length > 0) {

          console.log(consolePrefix + "Vertex shader compile log: " + log);

        }

      }

      // 得到像素着色器编译日志

      if (logShaderCompilation) {

        log = gl.getShaderInfoLog(fragmentShader);

        if (defined(log) && log.length > 0) {

          console.log(consolePrefix + "Fragment shader compile log: " + log);

        }

      }

      // 得到着色程序编译日志

      if (logShaderCompilation) {

        log = gl.getProgramInfoLog(program);

        if (defined(log) && log.length > 0) {

          console.log(consolePrefix + "Shader program link log: " + log);

        }

      }

      // 返回着色程序

      return program;

    }

    // 查找顶点属性

    function findVertexAttributes(gl, program, numberOfAttributes) {

      var attributes = {};

      for (var i = 0; i < numberOfAttributes; ++i) {

        // 获取顶点属性对象(包括类型、名称等)

        var attr = gl.getActiveAttrib(program, i);

        // 获取顶点属性的位置

        var location = gl.getAttribLocation(program, attr.name);

        // 保存顶点属性信息

        attributes[attr.name] = {

          name: attr.name,  // 属性名称

          type: attr.type,  // 属性类型

          index: location,  // 属性位置

        };

      }

      return attributes;

    }

    // 动态查找着色程序中的uniform

    function findUniforms(gl, program) {

      var uniformsByName = {};

      var uniforms = [];

      var samplerUniforms = [];

      // 获取着色程序中的活动的uniform数量

      var numberOfUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);

      // 遍历uniform数量下的uniform数据

      for (var i = 0; i < numberOfUniforms; ++i) {

        // 遍历uniform(包括名称、类型等信息)

        var activeUniform = gl.getActiveUniform(program, i);

        var suffix = "[0]";

        var uniformName =    // 分离出uniform的名字,如果是数组,去掉数组[0]子串

          activeUniform.name.indexOf(

            suffix,

            activeUniform.name.length - suffix.length

          ) !== -1

            ? activeUniform.name.slice(0, activeUniform.name.length - 3)   // 是数组,返回字串

            : activeUniform.name;                                          // 不是数组,返回名称

        // Ignore GLSL built-in uniforms returned in Firefox.

        // 忽略内建的uniform,在火狐浏览器中存在这中情况

        if (uniformName.indexOf("gl_") !== 0) { // 不是开头位置

          if (activeUniform.name.indexOf("[") < 0) {  // 不是数组

            // Single uniform

            // 单个uniform,获取位置

            var location = gl.getUniformLocation(program, uniformName);

            // IE 11.0.9 needs this check since getUniformLocation can return null

            // if the uniform is not active (e.g., it is optimized out).  Looks like

            // getActiveUniform() above returns uniforms that are not actually active.

            if (location !== null) {  // 位置不是null

              // 创建各种uniform,uniform封装了很多的gl相关操作

              var uniform = createUniform(gl, activeUniform, uniformName, location);

              // 将封装的uniform列表保存起来

              uniformsByName[uniformName] = uniform;

              uniforms.push(uniform);

              // 采样器存在,另外保存

              if (uniform._setSampler) {

                samplerUniforms.push(uniform);

              }

            }

          } else {// 是数组

            // Uniform array

           

            var uniformArray;

            var locations;

            var value;

            var loc;

            // On some platforms - Nexus 4 in Firefox for one - an array of sampler2D ends up being represented

            // as separate uniforms, one for each array element.  Check for and handle that case.

            var indexOfBracket = uniformName.indexOf("[");

            if (indexOfBracket >= 0) {

              // We're assuming the array elements show up in numerical order - it seems to be true.

              uniformArray = uniformsByName[uniformName.slice(0, indexOfBracket)];

              // Nexus 4 with Android 4.3 needs this check, because it reports a uniform

              // with the strange name webgl_3467e0265d05c3c1[1] in our globe surface shader.

              if (!defined(uniformArray)) {

                continue;

              }

              locations = uniformArray._locations;

              // On the Nexus 4 in Chrome, we get one uniform per sampler, just like in Firefox,

              // but the size is not 1 like it is in Firefox.  So if we push locations here,

              // we'll end up adding too many locations.

              if (locations.length <= 1) {

                value = uniformArray.value;

                loc = gl.getUniformLocation(program, uniformName);

                // Workaround for IE 11.0.9.  See above.

                if (loc !== null) {

                  locations.push(loc);

                  value.push(gl.getUniform(program, loc));

                }

              }

            } else {

              locations = [];

              for (var j = 0; j < activeUniform.size; ++j) {

                loc = gl.getUniformLocation(program, uniformName + "[" + j + "]");

                // Workaround for IE 11.0.9.  See above.

                if (loc !== null) {

                  locations.push(loc);

                }

              }

              uniformArray = createUniformArray(

                gl,

                activeUniform,

                uniformName,

                locations

              );

              uniformsByName[uniformName] = uniformArray;

              uniforms.push(uniformArray);

              if (uniformArray._setSampler) {

                samplerUniforms.push(uniformArray);

              }

            }

          }

        }

      }

      // 返回unifrom相关数据

      return {

        uniformsByName: uniformsByName,

        uniforms: uniforms,

        samplerUniforms: samplerUniforms,

      };

    }

    // 分离手动、自动uniform

    function partitionUniforms(shader, uniforms) {

      // 自动对象、手动对象

      var automaticUniforms = [];

      var manualUniforms = [];

      // 遍历uniform

      for (var uniform in uniforms) {

        // uniform名称对应的自身属性

        if (uniforms.hasOwnProperty(uniform)) {

          // uniform名称对应的对象

          var uniformObject = uniforms[uniform];

          // uniform名称

          var uniformName = uniform;

          // if it's a duplicate uniform, use its original name so it is updated correctly

          // 如果是重复的uniform,请使用其原始名称,以便正确更新

          var duplicateUniform = shader._duplicateUniformNames[uniformName];  // 别名

          if (defined(duplicateUniform)) {

            uniformObject.name = duplicateUniform;  // 别名(位置对应上就可以了)

            uniformName = duplicateUniform;         // 别名

          }

          // 自动uniform还是手动uniform,自动的应该是内置的(模型、视图、投影等uniform矩阵),不用自己创建,手动的是自己创建的uniform

          var automaticUniform = AutomaticUniforms[uniformName];  // AutomaticUniforms大写字母开头

          if (defined(automaticUniform)) {

            // 将uniform对象放到自动数组中

            automaticUniforms.push({

              uniform: uniformObject,

              automaticUniform: automaticUniform,

            });

          } else {

            // 将uniform对象放到手动数组中

            manualUniforms.push(uniformObject);

          }

        }

      }

      // 返回自动对象和手动对象

      return {

        automaticUniforms: automaticUniforms,

        manualUniforms: manualUniforms,

      };

    }

    function setSamplerUniforms(gl, program, samplerUniforms) {

      // 启动着色程序

      gl.useProgram(program);

      var textureUnitIndex = 0;

      var length = samplerUniforms.length;

      for (var i = 0; i < length; ++i) {

        // 根据采样器数量设置采样器索引

        textureUnitIndex = samplerUniforms[i]._setSampler(textureUnitIndex);

      }

      // 禁用着色程序

      gl.useProgram(null);

      return textureUnitIndex;

    }

    // 初始化着色程序

    function initialize(shader) {

      if (defined(shader._program)) {

        return;

      }

      // 重新初始化着色程序

      reinitialize(shader);

    }

    // 重新初始化

    function reinitialize(shader) {

      // 老的着色程序

      var oldProgram = shader._program;

      var gl = shader._gl;

      // 创建并且连接着色器

      var program = createAndLinkProgram(gl, shader, shader._debugShaders);

      // 得到活动的顶点属性数量

      var numberOfVertexAttributes = gl.getProgramParameter(

        program,

        gl.ACTIVE_ATTRIBUTES   // 活动的属性数量

      );

      // 查找uniform相关结构(包括,位置,创建对应的uniform类型对象)

      var uniforms = findUniforms(gl, program);

      // 分离手动自动uniform

      var partitionedUniforms = partitionUniforms(shader, uniforms.uniformsByName);

      // 新的着色程序

      shader._program = program;

      // 顶点属性数量

      shader._numberOfVertexAttributes = numberOfVertexAttributes;

      // 查找顶点属性

      shader._vertexAttributes = findVertexAttributes(

        gl,

        program,

        numberOfVertexAttributes

      );

      // uniform的名称-对象

      shader._uniformsByName = uniforms.uniformsByName;

      // uniform数组

      shader._uniforms = uniforms.uniforms;

      // 设置手动、自动uniform

      shader._automaticUniforms = partitionedUniforms.automaticUniforms;

      shader._manualUniforms = partitionedUniforms.manualUniforms;

      // 设置采样器绑定的纹理单元

      shader.maximumTextureUnitIndex = setSamplerUniforms(

        gl,

        program,

        uniforms.samplerUniforms    // 采样器对象数组

      );

      // 删除老的着色对象

      if (oldProgram) {

        shader._gl.deleteProgram(oldProgram);

      }

      // If SpectorJS is active, add the hook to make the shader editor work.

      // 支持SpectorJS调试

      // https://github.com/BabylonJS/Spector.js/blob/master/documentation/extension.md#shader-editor

      if (typeof spector !== "undefined") {

        shader._program.__SPECTOR_rebuildProgram = function (

          vertexSourceCode, // The new vertex shader source

          fragmentSourceCode, // The new fragment shader source

          onCompiled, // Callback triggered by your engine when the compilation is successful. It needs to send back the new linked program.

          onError // Callback triggered by your engine in case of error. It needs to send the WebGL error to allow the editor to display the error in the gutter.

        ) {

          var originalVS = shader._vertexShaderText;

          var originalFS = shader._fragmentShaderText;

          // SpectorJS likes to replace `!=` with `! =` for unknown reasons,

          // and that causes glsl compile failures. So fix that up.

          var regex = / ! = /g;

          shader._vertexShaderText = vertexSourceCode.replace(regex, " != ");

          shader._fragmentShaderText = fragmentSourceCode.replace(regex, " != ");

          try {

            reinitialize(shader);

            onCompiled(shader._program);

          } catch (e) {

            shader._vertexShaderText = originalVS;

            shader._fragmentShaderText = originalFS;

            // Only pass on the WebGL error:

            var errorMatcher = /(?:Compile|Link) error: ([^]*)/;

            var match = errorMatcher.exec(e.message);

            if (match) {

              onError(match[1]);

            } else {

              onError(e.message);

            }

          }

        };

      }

    }

    // 绑定着色程序

    ShaderProgram.prototype._bind = function () {

      // 还没有初始化时,先初始化

      initialize(this);

      // 启用着色程序

      this._gl.useProgram(this._program);

    };

    // 向glsl中设置uniform数据

    ShaderProgram.prototype._setUniforms = function (

      uniformMap,       // 命令中的uniform

      uniformState,     // 上下文中的uniform

      validate

    ) {

      var len;

      var i;

      if (defined(uniformMap)) {

        // 遍历手动uniform对象,将数据保存

        var manualUniforms = this._manualUniforms;

        len = manualUniforms.length;

        for (i = 0; i < len; ++i) {

          var mu = manualUniforms[i];

          // 将命令中的uniform数据添加到uniform接口中,用于后续传到gpu中

          try {

            mu.value = uniformMap[mu.name]();

          }catch(e) {

            console.log(mu.name);

          }

         

        }

      }

      // 遍历自动uniform对象,将数据保存

      var automaticUniforms = this._automaticUniforms;

      len = automaticUniforms.length;

      for (i = 0; i < len; ++i) {

        var au = automaticUniforms[i];

        // 将context上下文中的uniform数据添加

        au.uniform.value = au.automaticUniform.getValue(uniformState);

      }

      ///

      // It appears that assigning the uniform values above and then setting them here

      // (which makes the GL calls) is faster than removing this loop and making

      // the GL calls above.  I suspect this is because each GL call pollutes the

      // L2 cache making our JavaScript and the browser/driver ping-pong cache lines.

      // _uniforms是uniform数组,将数据传入到gpu中

      var uniforms = this._uniforms;

      len = uniforms.length;

      for (i = 0; i < len; ++i) {

        uniforms[i].set();

      }

      // 有效性验证

      if (validate) {

        var gl = this._gl;

        var program = this._program;

        // 验证着色程序是否有效

        gl.validateProgram(program);

        //>>includeStart('debug', pragmas.debug);

        if (!gl.getProgramParameter(program, gl.VALIDATE_STATUS)) {

          throw new DeveloperError(

            "Program validation failed.  Program info log: " +

              gl.getProgramInfoLog(program)

          );

        }

        //>>includeEnd('debug');

      }

    };

    // 是否销毁

    ShaderProgram.prototype.isDestroyed = function () {

      return false;

    };

    // 销毁

    ShaderProgram.prototype.destroy = function () {

      this._cachedShader.cache.releaseShaderProgram(this);

      return undefined;

    };

    // 最终销毁

    ShaderProgram.prototype.finalDestroy = function () {

      this._gl.deleteProgram(this._program);

      return destroyObject(this);

    };

    export default ShaderProgram;

  • 相关阅读:
    通达信接口公式怎样进行破解?
    当npm下载库失败时可以用cnpm替代
    【前端面试问题总结】2022.9.18
    2023北京市人工智能大模型场景融合与产业发展专场活动盛大召开
    JShaman JavaScript混淆加密工具,中英版本区别
    Linux友人帐之网络编程基础FTP服务器
    一个快递包裹的跨国之旅
    RFID服装管理系统改善零售供应链
    Java 基础语法
    A-Level化学例题解析及练习2
  • 原文地址:https://blog.csdn.net/tianyapai/article/details/126263681