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;

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值