转 第十七章 背面剔除与flash 3D 灯光 (as3.0)

C# 模拟光照运算以及背面剔除 1.求出顶点的法向量。通过叉乘计算 2.在顶点世界坐标空间 法向量与光向量进行点积(需要单位向量) 3.通过视向量与法线向量点积的结果 是否开启背面剔除1.向量类using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;nam 阅读详情

第十六章介绍了所有建立 3D 立体模型的基础包括:如何创建点,线,用多边形组成
各种形状,以及如何为每个多边形设置颜色。但是,我们只能让颜色的透明度为 50%,才
能看到正确的效果。虽然制作出的 3D 立体模型也不差,但是这样做在真实度上还是有所
欠缺。
    本章,通过介绍背面剔除(不绘制背面的多边形) ,深度排序(第十五章作了一点介绍,
但这次要从多边形的角度重新审视它) ,以及 3D 灯光,来修正这个问题。
    我想应用了这三种技术所得到的 3D 模型一定会让大家感到惊讶。学习过前两项技术
后,我们将能够创建出看上去非常真实的 3D 立体模型。使用 3D 灯光可以让它们变得活
灵活现。
    在开始之前,我想先声明一下技术的来源。本章几乎所有的代码都来自 Todd Yard 的
Macromedia Flash MX Studio 一书第十章的技术。(Todd 也是本书的技术评论员,因此这就
不像未经他人允许剽窃过来的! )Macromedia Flash MX Studio 也许是我所能找到最适合本
章的专用材料,每当我要用到这些高级 3D 技术时都会去查阅它。因为我们这本书已经有
四年多的时间了(按照技术书籍来算,是本古老的书了)跨越了 Flash 的三个版本,我非
常愿意能够让本书的内容继续发展下去并且能够与时俱进。
    本章建立在前一章的旋转的、挤出的、3D 字母 A 的基础上。这会使代码非常复杂,
只要有一点点错误都会非常明显,当一切都正确时获得的效果会是超级棒的哟!
背面剔除
    背面剔除在前一章提到很多次了。下面,我们将了解它的一切以及弄清它是如何工作的。
    别忘了,我们早前的模型都是半透明填充的。原因是每次都在绘制多边形,但是没有控
制绘制的顺序。因此一个处于模型后面的多边形绘制在了模型的前面,  造成了一些奇怪的结
果。在集中讨论建模基础技术时,为了将这一部分的讨论向后推迟,我们将所有多边形的透
明度都设置为 50%。现在到了该处理它的时候了。
    总体而言,背面剔除是非常简单的。只绘制面向我们的多边形,不绘制背面的。这个技
术正如其名。大家应该还记得每个多边形的点都是顺时针安排的。  虽然在前面的例子中是完
全没有必要的,但是我们即将看到它为什么如此重要,  以及为什么从一开始就要养成这样的
习惯。
    很明显,如果面向我们的多边形的点是顺时钟安排的,  那么当它们转而背对我们时它们
都将是逆时针排列的。
    如图 17-1 所示一个面向我们的三角形。(因为我们使用的所有多边形都是三角形,所
以我常常混淆这两个词。多数情况下,我会使用“多边形”作为一个总称,而“三角形”只
是一个特殊的有三个角的多边形)。
转 <wbr>第十七章 <wbr>背面剔除与flash <wbr>3D <wbr>灯光 <wbr>(as3.0)

图 17-1 一个面向我们的三角形的点以顺时针方向排列

 

在图 17-2 中,我将这个三角形进行了旋转,看到了它相反的方向。

转 <wbr>第十七章 <wbr>背面剔除与flash <wbr>3D <wbr>灯光 <wbr>(as3.0)
图 17-2 一个背对我们的三角形以逆时针方向排列
      我们已经看到逆时针方向点的排列情况。
      这里有一点需要澄清。首先,我说的一个多边形“向面我们”是什么意思?意思是多边
形的外部面面向我们。  虽然我在展示单一的一个三角形时看起来并不明显,  但是别忘了我们
是在讨论 3D 立体模型。这样一来,每个多边形都有一个外部表面和一个内部面。
      另一点是,在确定顺时针或逆时针方向时,我们是在讨论点的屏幕上的位置。而不是 3D
x, y, z 坐标,而是经过透视后确定的 screenX, screenY 坐标。
      最后,还应该注意到我们可以将整个设置颠倒过来,让一个逆时针多边形系统面向我们,
让一个顺时针系统背对我们。每种方式都可以像平时一样工作。
      回到问题上,如何确定三个点是以顺时针安排还是以逆时针安排?请思考一下这个问
题。这个问题对于眼睛来说非常好回答,但是要它们变为代码,立即就变成了一个非常抽象
的概念。
      刚刚说过我给大家的解决办法来自 Macromedia Flash MX Studio。请相信我提供给你的
一定是尽可能最好的方法,  我也试着用自己写的函数来区分顺时针和逆时针方法。  我将所有
运行正常的程序放到一起,结果我的方法比现有的解决方法要长上两倍,并且非常复杂,其
中还包括了坐标旋转和其它三角函数。  由于我通常不想作恶人,  所以我决定给大家那个简单
的解决方法。  该方法与我的那个极度复杂的方法同样精确!  我们所要做的就是将这个方法加
入 Triangle 类,名为 isBackFace。它会求出三角形三个点的值,如果是逆时针的方向则返
回 true 如果顺时针则返回 false。以下是加入了该函数的类。同样注意在调用 beginFill 时
不再使用 .5 的透明度了。现在开始我们就要绘制不透明的形状了。
package {
 import flash.display.Graphics;
 public class Triangle {
  private var pointA:Point3D;
  private var pointB:Point3D;
  private var pointC:Point3D;
  private var color:uint;
  public function Triangle(a:Point3D, b:Point3D, c:Point3D, color:uint) {
    pointA = a;
    pointB = b;
    pointC = c;
    this.color = color;
  }
  public function draw(g:Graphics):void {
    if (isBackFace()) {
      return;
    }
    g.beginFill(color);
      g.moveTo(pointA.screenX, pointA.screenY);
      g.lineTo(pointB.screenX, pointB.screenY);
      g.lineTo(pointC.screenX, pointC.screenY);
      g.lineTo(pointA.screenX, pointA.screenY);
      g.endFill();
    }
    private function isBackFace():Boolean {
      var cax:Number = pointC.screenX - pointA.screenX;
      var cay:Number = pointC.screenY - pointA.screenY;
      var bcx:Number = pointB.screenX - pointC.screenX;
      var bcy:Number = pointB.screenY - pointC.screenY;
      return cax * bcy > cay * bcx;
    }
  }
}
    我 们 看 到 这 个网站 的 链 接 , http://www.jurjans.lv/flash/shape.html 。 Todd Yard 在
Macromedia Flash MX Studio 中推荐了这个网站,所以我将它发给大家。除此之外,这个网
站还有一些很棒的参考资料以及类似主题的指南。
    快速地解释一下,这个函数计算了三角形两条边的长度,并使用乘法和比较的方式,告
诉我们多边形是向哪个方向运动的。如果您对它的实现很感兴趣,就去看看刚才说到的那个
网站,或者搜索一下“背面剔除”["Backface culling"]。我保证您会找到大量的阅读资料。
现在,只能说它是个简单、快速、高效并且 100% 可以使用的函数,将它放在这里即可!
    那么如何使用这个方法呢?我们并不需要真正去考虑它的实现。这是一个私有方法, 意
味着只有在 Triangle 类中才能调用它。它可以作为 draw 方法的第一句被调用。如果返回
值为 true,则三角形是背面,不应绘制,因此 draw 方法停止并返回。如果 isBackFace 返
回值为 false,三角形是正面,则像平常一样被绘制出来。
    现在可以运行一下 ExtrudedA.as 或其它已经创建的 3D 模型,我们会发现情况发生了
一点点变化。 当旋转这个模型时,我们看到一旦某个面转向了反方向,那么它将不再被绘制。
目前为止效果还不够好,因为这里仍然有些离得远的部分绘制在了距离较近的部分的前面,
不过这是我们曾经学过的。如果“z 排序”或“深度排序”一词出现在您的脑海里,那就对
了。这就是下一节的内容。
    现在我们应该可以看到如图 17-3 所示的效果。

转 <wbr>第十七章 <wbr>背面剔除与flash <wbr>3D <wbr>灯光 <wbr>(as3.0)

图 17-3 运行中的背面剔除

 


深度排序
    深度排序,或叫做 z 排序,我们已经在第十五章为影片应用透视时讨论过。在那个例
子中,为一个 Sprite 影片数组通过 zpos 属性进行排序(确切地说是一个拥有 3D 属性的
Sprite 的子类)。
     而现在我们不是在处理多个影片。所有的多边形都使用主类中相同的 graphics 对象进
行绘制。因此,无论何时绘制多边形,它都将绘制在前一个图形的上面。与其去改变它们的
深度,不如决定何时绘制这个多边形。具体来讲,我们要先绘制距离最远的;然后再依次绘
制其余的,这样最近的多边形会在最后被绘制,盖住所有可能放到前面的图形。
     如何实现呢?在这个名为 triangles 的数组中存着所有多边形。当绘制图形时,循环
triangles,从第 0 个到最后一个依次绘制每个三角形。我们要做的就是将这个数组进行排序
以便让最远的三角形为数组的第 0 个元素,距离观察者最近的成为最后一个元素。
     这与 Sprite 影片数组的排序非常相似。但是本例子中,三角形只是三个 Point3D 对象
的集合。它们没有一个专门描述整个三角形深度的属性。但是要创建这个属性也相当容易。
计算这个值最好的方法就是找到三角形的三个点里面最小的 z 值。换句话讲,如果一个三
角形的三个顶点深度分别为 200, 250, 300,就可以说这个三角形的 z 坐标为 200。我们可
以使用 Math.min 方法来确定三个点最小的 z 值。但需要使用两次,因为一次只能传入两
个数值。我们将在 Triangle 类的 depth 方法中进行实现。以下是更新的类:
package {
 import flash.display.Graphics;
 public class Triangle {
  private var pointA:Point3D;
  private var pointB:Point3D;
  private var pointC:Point3D;
  private var color:uint;
  public function Triangle(a:Point3D, b:Point3D, c:Point3D, color:uint) {
    pointA = a;
    pointB = b;
    pointC = c;
    this.color = color;
  }
  public function draw(g:Graphics):void {
    if (isBackFace()) {
      return;
    }
    g.beginFill(color);
    g.moveTo(pointA.screenX, pointA.screenY);
    g.lineTo(pointB.screenX, pointB.screenY);
    g.lineTo(pointC.screenX, pointC.screenY);
    g.lineTo(pointA.screenX, pointA.screenY);
    g.endFill();
  }
private function isBackFace():Boolean {
  // 见 http://www.jurjans.lv/flash/shape.html
  var cax:Number = PointC.screenX - PointA.screenX;
  var cay:Number = PointC.screenY - PointA.screenY;
  var bcx:Number = PointB.screenX - PointC.screenX;
  var bcy:Number = PointB.screenY - PointC.screenY;
  return cax * bcy > cay * bcx;
}
public function get depth():Number {
  var zpos:Number = Math.min(PointA.z, PointB.z);
      zpos = Math.min(zpos, PointC.z);
      return zpos;
    }
  }
}
       现在我们就能够知道哪个在前哪个在后了,可以为 triangles 数组排序了。同样,要让
数组降序排列,让深度最高的(最远的)在第一个。这个操作要在文档类中完成,并且在绘
制三角形之前进行。这里我使用 ExtrudedA 类,因此这个类的 onEnterFrame 方法像是这
样:
private function onEnterFrame(event:Event):void {
  var angleX:Number = (mouseY - vpY) * .001;
  var angleY:Number = (mouseX - vpX) * .001;
  for (var i:uint = 0; i < points.length; i++) {
    var point:Point3D = points[i];
    point.rotateX(angleX);
    point.rotateY(angleY);
  }
  triangles.sortOn("depth", Array.DESCENDING | Array.NUMERIC);
  graphics.clear();
  for (i = 0; i < triangles.length; i++) {
    triangles[i].draw(graphics);
  }
}
这里只需改变一行代码。面向对象编程的奇迹让我们的生活更简单了!
    运行这个程序后,我们将看到了一个被完美渲染出来的立体模型,如图 17-4 所示。现
在我们已经真正达到了某种境界。下面将带大家步入最 Cool 的巅峰!


转 <wbr>第十七章 <wbr>背面剔除与flash <wbr>3D <wbr>灯光 <wbr>(as3.0)

图 17-4 深度排序修正了错误
3D 灯光
     刚刚这个例子近乎可以让我们的渲染达到完美的效果,  但是它似乎还缺少点儿什么。有
些单调。OK,OK,大家看到标题就已经知道了,下面就让我们加入 3D 的灯光效果吧。
     同背面剔除一样,3D 灯光的细节也是相当复杂并且需要数学运算的。我实在没有太多
的空间讨论每个漂亮的细节,但是通过快速的网络搜索大家可以获得非常更多的相关资料,
也许这些资料多得我们一生也看不完。 在这里,我给大家的都是一些基础的需要用到的函数。
     首先,需要一个光源。一个最简单的光源只有两个属性:位置和亮度(brightness)。在
更加复杂的 3D 系统中,它也能够指向某个方向,并且还带有颜色,衰减率(falloff rate),
圆锥区域等等。但是这些都超出了本例的范围。
     让我们从制作一个 Light 灯光类开始。它会持有我们刚说的那两个属性 —— 位置和
亮度。
package {
  public class Light {
    public var x:Number;
    public var y:Number;
    public var z:Number;
    private var _brightness:Number;
    public function Light(x:Number = -100,
    y:Number = -100,
    z:Number = -100,
    brightness:Number = 1) {
      this.x = x;
      this.y = y;
      this.z = z;
      this.brightness = brightness;
    }
    public function set brightness(b:Number):void {
      _brightness = Math.max(b, 0);
      _brightness = Math.min(_brightness, 1);
    }
    public function get brightness():Number {
      return _brightness;
    }
  }
}
现在可以在主类的 init 方法中创建一个新的默认灯光:
      var light:Light = new Light();
或者可以创建一个指定位置和区域的灯光:
      var light:Light = new Light(100, 200, 300, .5);
这里有两个重要的地方需要注意。一个是位置,仅用于计算灯光的角度。灯光的亮度不会因
为距离而衰减。因此改变 x, y, z 到 –1,000,000 或 -1 对于照射在物体上的灯光的亮度是
没有区别的。
      只有 brightness 属性才会改变灯光的特性。我们当然可以加入一个函数用以判断灯光
。不会很难,现在已经介绍得差不多了,与物体间的距离来计算灯光的亮度值(brightness)
因此把这个函数留给大家去做。
      brightness 必需是 0.0 到 1.0 之间的数。如果出了这个范围,会带来一些奇怪的结果。
就是这个原因,我创建了一个私有属性 _brightness,并允许通过公共的 getter 和 setter 访
问 brightness。这样做,允许我们传入的数值得到有效性的验证,确保这个数在有效范围内。
      一个理想的类不应该出现公有的属性,即使这些属性不需要验证,也只有私有属性通过
getter 和 setter 函数才能访问。这里我抄了近路,为的是让代码简洁并突出动画编程的原则。
但是在本例中,额外添加的这一步是有必要的。
      下面,光源要做的就是根据灯光照射在多边形上的角度来改变三角形颜色的亮度值。因
此如果一个多边形直接面对灯光,它就会显示出全部的颜色值。当离开灯光时,就会变得越
来越暗。最终,当它完全离开光源时,它将完全变为阴影或黑色。
      由于 Triangle 类的成员知道自己的颜色是什么,并知道如何绘制自己,似乎每个三角
形只需访问这个 light 就可以实现自己 draw 函数。因此,让我们给所有三角形一个 light
属性。我还要超个近路设置它们为公有属性:
      public var light:Light;
然后在主类中,创建这些三角形后,只需要循环它们把灯光的引用赋值给每个三角形:
var light:Light = new Light();
for(i = 0; i < triangles.length; i++) {
  triangles[i].light = light;
}
或者,我们也可以让 light 作为 Triangle 构造函数中的一个附加的参数,让每个三角形都
有一个光源。我将这个方法留给大家去选择。
      现在,Triangle 需要一个关于其灯光颜色、角度、亮度的函数,并返回一个调整后的颜
色值。以下是这个函数:
function getAdjustedColor():uint {
  var red:Number = color >> 16;
  var green:Number = color >> 8 & 0xff;
  var blue:Number = color & 0xff;
  var lightFactor:Number = getLightFactor();
  red *= lightFactor;
  green *= lightFactor;
  blue *= lightFactor;
  return red << 16 | green << 8 | blue;
}
      这个函数首先将三角形的基本颜色分为了 red, green, blue 三个成分(见第四章)。然
后调用另一个方法 getLightFactor,稍后会看到这个函数。现在,只需要知道它返回的是 0.0
到 1.0 之间的一个数, 表示该颜色需要改变的大小,1.0 表示全部亮度, 0.0 表示为全黑色。
      然后将每个颜色成分乘以这个滤光系数 (light factor) 最后再将它们组合为一个 24 位
的颜色值,并且将它作为调整后的颜色返回。它将成为灯光照射下三角形的颜色。
  现在,如何得到这个 lightFactor 呢?让我们看一下:
private function getLightFactor():Number {
  var ab:Object = new Object();
  ab.x = pointA.x - pointB.x;
  ab.y = pointA.y - pointB.y;
  ab.z = pointA.z - pointB.z;
  var bc:Object = new Object();
  bc.x = pointB.x - pointC.x;
  bc.y = pointB.y - pointC.y;
  bc.z = pointB.z - pointC.z;
  var norm:Object = new Object();
  norm.x = (ab.y * bc.z) - (ab.z * bc.y);
  norm.y = -((ab.x * bc.z) - (ab.z * bc.x));
  norm.z = (ab.x * bc.y) - (ab.y * bc.x);
  var dotProd:Number = norm.x * light.x +
  norm.y * light.y +
  norm.z * light.z;
  var normMag:Number = Math.sqrt(norm.x * norm.x +
  norm.y * norm.y +
  norm.z * norm.z);
  var lightMag:Number = Math.sqrt(light.x * light.x +
  light.y * light.y +
  light.z * light.z);
  return Math.acos(dotProd / normMag * lightMag) / Math.PI * light.brightness;
}
      哇,好大一个函数不是吗?要想完全理解它,就一定要对高等向量学有较深的掌握,
但是我也会试将基础的地方解释一下。
      首先,我们需要找到三角形的法线(normal)。它是一个向量,是三角形平面上的一条
垂线,如图 17-5 所示。想象一下,我们拿着一块木制的三角板,然后从背后钉入一根钉子,
它会从正面穿出。这根钉子就代表三角形平面的法线。如果您学过 3D 渲染和灯光的话,
一定看过各种关于法线的资料。


转 <wbr>第十七章 <wbr>背面剔除与flash <wbr>3D <wbr>灯光 <wbr>(as3.0)
图 17-5 法线是到达三角形表面的一条垂线
     我们可以通过该平面的两个向量计算出它们的外积(cross product)从而求出这条法线。
两个向量的积是一条垂直于这两条向量的新向量。我们将使用的这两条向量是点 A 和 B,
点 B 和 C 之间的连线。每个向量都用有带有 x, y, z 的 Object 持有。
var ab:Object = new Object();
ab.x = pointA.x - pointB.x;
ab.y = pointA.y - pointB.y;
ab.z = pointA.z - pointB.z;
var bc:Object = new Object();
bc.x = pointB.x - pointC.x;
bc.y = pointB.y - pointC.y;
bc.z = pointB.z - pointC.z;
然后计算法线,即另一个向量。求该对象的模(norm)。下面的代码用于计算向量 ab 和 bc
的外积:
var norm:Object = new Object();
norm.x = (ab.y * bc.z) - (ab.z * bc.y);
norm.y = -((ab.x * bc.z) - (ab.z * bc.x));
norm.z = (ab.x * bc.y) - (ab.y * bc.x);
我没有太多的篇幅来介绍这种计算方法的细节,这是计算向量外积的标准公式。如果您对它
的推导感兴趣,可以随便找一本线性代数的正规参考书查一查。
      现在我们需要知道这条法线与灯光的角度。向量数学的另一个好东西叫做内积(dot
它与外积不同。我们有了法线的向量和灯光的向量。下面计算点积:product)
      var dotProd:Number = norm.x * light.x + norm.y * light.y + norm.z * light.z;
我们看到,内积要比外积简单一些!都差不多了!接下来,计算法线的量值,以及灯光的量值,

大家应该还认识这个 3D OK,
版的勾股定理吧:
var normMag:Number = Math.sqrt(norm.x * norm.x + norm.y * norm.y + norm.z * norm.z);
var lightMag:Number = Math.sqrt(light.x * light.x + light.y * light.y + light.z * light.z);
请注意,当一个三角形被渲染时,变量 lightMag 每次都要进行计算,这样就允许灯光是移
动的。如果知道光源是固定的,我们可以在代码的一开始就加入这个变量,只需在创建灯光
或为三角形赋值时进行一次计算。或者可以为 Light 类添加 lightMag 属性,让它可以在每
次 x, y, z 属性发生变化时被计算。看,我已经给大家留出了各种发挥的空间!
最后,将前面计算出的这些数放入一个具有魔力公式中:
    return (Math.acos(dotProd / (normMag * lightMag)) / Math.PI) * light.brightness;
其中 dotProd 是一个分量, normMag * lightMag 是另一个分量。而两者相除得出一个比率。
回忆一下第三章,一个角度的余弦给了我们一个比率,而一个比率的反余弦给了我们一个角
度。这就是灯光照射在多边形表面上的角度。它的范围在 0 到 Math.PI 个弧度之间(0 到
180 度),也就是说灯光完全照射在物体前面上或完全照射在物体背面。
    用这个数除以 Math.PI 得出一个百分数,再用它乘以 brightness 的百分比就得出了最
终用于改变底色的滤光系数。
    OK,所有这些仅仅给出了多边形表面颜色!此刻,在现有的代码中实现它就非常简单
了。我们在 draw 方法中使用它。应该像这样直接使用这个调整后的颜色:
    g.beginFill(getAdjustedColor());
为了把上述内容综合起来,以下是全部最终的 Triangle.as 和 ExtrudedA.as 代码,列出了我
们本章所有发生变化的部分:
首先是 Triangle:
package {
 import flash.display.Graphics;
 public class Triangle {
  private var pointA:Point3D;
  private var pointB:Point3D;
  private var pointC:Point3D;
  private var color:uint;
  public var light:Light;
  public function Triangle(a:Point3D, b:Point3D,
  c:Point3D, color:uint) {
    pointA = a;
    pointB = b;
    pointC = c;
    this.color = color;
  }
  public function draw(g:Graphics):void {
    if (isBackFace()) {
      return;
    }
    g.beginFill(getAdjustedColor());
    g.moveTo(pointA.screenX, pointA.screenY);
    g.lineTo(pointB.screenX, pointB.screenY);
    g.lineTo(pointC.screenX, pointC.screenY);
    g.lineTo(pointA.screenX, pointA.screenY);
    g.endFill();
  }
  private function getAdjustedColor():uint {
    var red:Number = color >> 16;
  var green:Number = color >> 8 & 0xff;
  var blue:Number =color & 0xff;
  var lightFactor:Number = getLightFactor();
  red *= lightFactor;
  green *= lightFactor;
  blue *= lightFactor;
  return red << 16 | green << 8 | blue;
}
private function getLightFactor():Number {
  var ab:Object = new Object();
  ab.x = pointA.x - pointB.x;
  ab.y = pointA.y - pointB.y;
  ab.z = pointA.z - pointB.z;
  var bc:Object = new Object();
  bc.x = pointB.x - pointC.x;
  bc.y = pointB.y - pointC.y;
  bc.z = pointB.z - pointC.z;
  var norm:Object = new Object();
  norm.x = (ab.y * bc.z) - (ab.z * bc.y);
  norm.y = -((ab.x * bc.z) - (ab.z * bc.x));
  norm.z = (ab.x * bc.y) - (ab.y * bc.x);
  var dotProd:Number = norm.x * light.x +
  norm.y * light.y +
  norm.z * light.z;
  var normMag:Number = Math.sqrt(norm.x * norm.x +
  norm.y * norm.y +
  norm.z * norm.z);
  var lightMag:Number = Math.sqrt(light.x * light.x +
  light.y * light.y +
  light.z * light.z);
  return Math.acos(dotProd / normMag * lightMag) / Math.PI * light.brightness;
}
    private function isBackFace():Boolean {
      // 见 http://www.jurjans.lv/flash/shape.html
      var cax:Number = pointC.screenX - pointA.screenX;
      var cay:Number = pointC.screenY - pointA.screenY;
      var bcx:Number = pointB.screenX - pointC.screenX;
      var bcy:Number = pointB.screenY - pointC.screenY;
      return cax * bcy > cay * bcx;
    }
    public function get depth():Number {
      var zpos:Number = Math.min(pointA.z, pointB.z);
      zpos = Math.min(zpos, pointC.z);
      return zpos;
    }
  }
}
然后是 ExtrudedA:
package {
  import flash.display.Sprite;
  import flash.events.Event;
  public class ExtrudedA extends Sprite {
    private var points:Array;
    private var triangles:Array;
    private var fl:Number = 250;
    private var vpX:Number = stage.stageWidth / 2;
    private var vpY:Number = stage.stageHeight / 2;
public function ExtrudedA() {
  init();
}
private function init():void {
  points = new Array();
  points[0] = new Point3D( -50, -250, -50);
  points[1] = new Point3D( 50, -250, -50);
  points[2] = new Point3D( 200, 250, -50);
  points[3] = new Point3D( 100, 250, -50);
  points[4] = new Point3D( 50, 100, -50);
  points[5] = new Point3D( -50, 100, -50);
  points[6] = new Point3D(-100, 250, -50);
  points[7] = new Point3D(-200, 250, -50);
  points[8] = new Point3D( 0, -150, -50);
  points[9] = new Point3D( 50, 0, -50);
  points[10] = new Point3D( -50, 0, -50);
  points[11] = new Point3D( -50, -250, 50);
  points[12] = new Point3D( 50, -250, 50);
  points[13] = new Point3D( 200, 250, 50);
  points[14] = new Point3D( 100, 250, 50);
  points[15] = new Point3D( 50, 100, 50);
  points[16] = new Point3D( -50, 100, 50);
  points[17] = new Point3D(-100, 250, 50);
  points[18] = new Point3D(-200, 250, 50);
  points[19] = new Point3D( 0, -150, 50);
  points[20] = new Point3D( 50, 0, 50);
  points[21] = new Point3D( -50, 0, 50);
  for (var i:uint = 0; i < points.length; i++) {
  points[i].setVanishingPoint(vpX, vpY);
  points[i].setCenter(0, 0, 200);
}
triangles = new Array();
triangles[0] =new Triangle(points[0], points[1],
points[8], 0xcccccc);
triangles[1] =new Triangle(points[1], points[9],
points[8], 0xcccccc);
triangles[2] =new Triangle(points[1], points[2],
points[9], 0xcccccc);
triangles[3] =new Triangle(points[2], points[4],
points[9], 0xcccccc);
triangles[4] =new Triangle(points[2], points[3],
points[4], 0xcccccc);
triangles[5] =new Triangle(points[4], points[5],
points[9], 0xcccccc);
triangles[6] =new Triangle(points[9], points[5],
points[10], 0xcccccc);
triangles[7] =new Triangle(points[5], points[6],
points[7], 0xcccccc);
triangles[8] =new Triangle(points[5], points[7],
points[10], 0xcccccc);
triangles[9] =new Triangle(points[0], points[10],
points[7], 0xcccccc);
triangles[10] =new Triangle(points[0], points[8],
points[10], 0xcccccc);
triangles[11] =new Triangle(points[11], points[19],
points[12], 0xcccccc);
triangles[12] =new Triangle(points[12], points[19],
points[20], 0xcccccc);
triangles[13] =new Triangle(points[12], points[20],
points[13], 0xcccccc);
triangles[14] =new Triangle(points[13], points[20],
points[15], 0xcccccc);
triangles[15] =new Triangle(points[13], points[15],
points[14], 0xcccccc);
triangles[16] =new Triangle(points[15], points[20],
points[16], 0xcccccc);
triangles[17] =new Triangle(points[20], points[21],
points[16], 0xcccccc);
triangles[18] =new Triangle(points[16], points[18],
points[17], 0xcccccc);
triangles[19] =new Triangle(points[16], points[21],
points[18], 0xcccccc);
triangles[20] =new Triangle(points[11], points[18],
points[21], 0xcccccc);
triangles[21] =new Triangle(points[11], points[21],
points[19], 0xcccccc);
triangles[22] =new Triangle(points[0], points[11],
points[1], 0xcccccc);
triangles[23] =new Triangle(points[11], points[12],
points[1], 0xcccccc);
triangles[24] =new Triangle(points[1], points[12],
points[2], 0xcccccc);
triangles[25] =new Triangle(points[12], points[13],
points[2], 0xcccccc);
triangles[26] =new Triangle(points[3], points[2],
points[14], 0xcccccc);
triangles[27] =new Triangle(points[2], points[13],
points[14], 0xcccccc);
triangles[28] =new Triangle(points[4], points[3],
points[15], 0xcccccc);
triangles[29] =new Triangle(points[3], points[14],
points[15], 0xcccccc);
triangles[30] =new Triangle(points[5], points[4],
points[16], 0xcccccc);
triangles[31] =new Triangle(points[4], points[15],
points[16], 0xcccccc);
triangles[32] =new Triangle(points[6], points[5],
points[17], 0xcccccc);
triangles[33] =new Triangle(points[5], points[16],
points[17], 0xcccccc);
triangles[34] =new Triangle(points[7], points[6],
points[18], 0xcccccc);
triangles[35] =new Triangle(points[6], points[17],
points[18], 0xcccccc);
triangles[36] =new Triangle(points[0], points[7],
points[11], 0xcccccc);
triangles[37] =new Triangle(points[7], points[18],
points[11], 0xcccccc);
triangles[38] =new Triangle(points[8], points[9],
points[19], 0xcccccc);
triangles[39] =new Triangle(points[9], points[20],
points[19], 0xcccccc);
triangles[40] =new Triangle(points[9], points[10],
points[20], 0xcccccc);
triangles[41] =new Triangle(points[10], points[21],
points[20], 0xcccccc);
triangles[42] =new Triangle(points[10], points[8],
points[21], 0xcccccc);
triangles[43] =new Triangle(points[8], points[19],
points[21], 0xcccccc);
var light:Light = new Light();
for (i = 0; i < triangles.length; i++) {
  triangles[i].light = light;
}
      addEventListener(Event.ENTER_FRAME, onEnterFrame);
    }
    private function onEnterFrame(event:Event):void {
      var angleX:Number = (mouseY - vpY) * .001;
      var angleY:Number = (mouseX - vpX) * .001;
      for (var i:uint = 0; i < points.length; i++) {
        var point:Point3D = points[i];
        point.rotateX(angleX);
        point.rotateY(angleY);
      }
      triangles.sortOn("depth", Array.DESCENDING | Array.NUMERIC);
      graphics.clear();
      for (i = 0; i < triangles.length; i++) {
        triangles[i].draw(graphics);
      }
    }
  }
}
        我们看到,在文档类中只有两个次要的变化。主要的工作都集中在 Triangle 中。同时,
我还让所有的三角形使用相同的颜色,我认为这样做可以更好地观察灯光效果(见图 17-6)
                                                                                             。

转 <wbr>第十七章 <wbr>背面剔除与flash <wbr>3D <wbr>灯光 <wbr>(as3.0)

图 17-6 带有背面剔除,深度排序及 3D 灯光的三维立体模型

 

 

(如果要转载请注明出处http://blog.sina.com.cn/jooi,谢谢)

HTML5与WebGL协同原理:构建高性能Web 3D渲染管线 HTML5并非仅是语义化标签升级,而是将浏览器重塑为轻量级应用运行时的系统性演进;WebGL则作为其关键图形扩展,实现了JavaScript对GPU硬件能力的直接调用。二者协同的本质在于:HTML5提供媒体管道、离线存储、事件调度等底层支撑,WebGL专注高效三维渲染,共同支撑数字孪生、工业可视化、Web AR等实时交互场景。理解其双缓冲机制、视频直通纹理、IndexedDB预加载及WebGL状态机模型,是突破页面卡顿、黑屏、移动端适配等工程瓶颈的技术前提。本文深入解析从上下文初始化、结构化顶点上传到glT 阅读详情

相关推荐

基于ESP8266与WS2812B的百元内智能光效时钟DIY全攻略

物联网(IoT)项目开发的核心在于将微控制器、传感器与执行器通过网络连接,实现智能化控制。其基本原理是通过嵌入式系统编程,驱动硬件执行特定任务,并通过Wi-Fi等通信协议实现数据交互。这项技术的价值在于能够以极低的成本,将创意快速转化为功能完备的智能设备,极大地降低了电子制作和原型开发的门槛。在众多应用场景中,智能家居装饰和个性化电子制作是热门方向。本文聚焦于一个具体的工程实践:如何利用ESP8266微控制器和WS2812B可寻址LED灯带,结合3D打印外壳,打造一个成本控制在百元内、具备网络自动对时功能的

weixin_34129696的博客 467

游戏场景管理(三)背面剔除

在图形渲染中有一个很大的敌人就是渲染不必要的多边形,比如处于背面的三角面片。拿起一本数,无论你怎么看最多也只能看到书的三个面,有的时候只能看到书的一个面。看不到的面我们完全可以把它剔除掉,这门武功就叫做背面剔除。 如果是软光栅化,背面剔除通常在世界空间或相机空间中做,算法很简单如果平面的法线和视向量(平面上的一个点到视点的向量)的夹角小于90度那么这个三角面就是正面,否则是背面。这里的背面剔除只和平面法线及相机位置有关,与相机的朝向无关,也就是说即便背面剔除测试通过,这个三角面片也有可能不被渲染,比如它根

liran2019的博客 2502

UsageLog电脑使用记录软件

UsageLog电脑使用记录软件

3D图形数学6-背面剔除

数学中点乘和叉乘的几何意义与图形学相结合的应用,如何背面剔除

weixin_44200074的博客 1091

最新推荐-2026年固态电池+锂电正极+硅基负极等关键技术实践PPT合集(55份).zip

2026年最新固态电池+锂电正极+硅基负极等关键技术实践PPT合集,共55份。供大家查阅。 一、固态电池 1.全固态电池用硅基负极材料研究进展 2.全固态电池硅基负极的开发思考 3.功能型固态电解质设计及界面调控 4.固态电池及关键材料研发与产业化进展 5.固态电池的产业化应用探索 6.固态锂电池开发现状及未来展望 7.固态锂离子电池正极结构设计与材料选择 8.固态锂金属电池设计与应用 9.硫化物全固态电池石墨和硅负极性能调控技术 10.硫化物固态电解质行业应用 11.聚合物基固态电解质与固态电池的关键技术 12.蜂鸟声共振技术-固态电池产业化的革命性创新 13.长安汽车固态电池应用探索 14.高安全性固态电池技术发展趋势 15.高性能硫化物全固态锂电池解决方案 16.高硅体系的膨胀解决方案 二、硅基负极 1.化学气相沉积碳包覆技术在硅基负极材料中的应用 2.新型粘结剂解决 硅基负极膨胀问题 3.氧化亚硅负极材料化学预锂化与界面设计 4.流化床法生产磷酸铁锂及硅负极材料研究 5.石墨烯包覆硅碳负极 6.硅基材料的发展现状及展望 7.硅基负极与固态电解质 8.硅基负极技术与进展 9.硅基负极材料体积膨胀解决策略 10.硅基负极材料及其结构设计 11.硅基负极材料的冶金法制备 12.硅基负极材料的发展与应用 13.硅基负极材料研究进展 14.硅基负极材料:从基础研究到产业应用 15.硅基负极析锂行为及调制研究 16.硅碳负极低成本制造技术报告 17.硅碳负极制备用多孔碳的开发 18.硅碳负极挑战和进展 19.硅碳负极新路线及关键材料制备 20.硅碳负极锂离子电池应力特性研究 21.锂电石墨负极材料工程化技术进步 22.锂离子电池负极材料 23.高容量石墨烯-硅基复合负极材料的研究进展 三、锂电正极 1.基于AI和图论结构化学的锂电池的材料基因探索 2.废旧磷酸铁锂电池梯次及再生利用进展 3.电动汽车大功率充电标准体系与检测认证实践 4.磷酸铁及磷酸铁锂异物防控管理 5.磷酸铁锂正极材料:液相法合成及产业化应用研究 6.磷酸锰铁锂正极材料的研究现状 7.车用动力电池安全标准化工作进展及未来展望 8.锂电层状氧化物正极材料的原子尺度失效机制 9.锂电正极材料工厂异物防控管理图册 10.锂离子电池高熵正极材料成分设计及储锂机制研究 11.高压实磷酸铁锂正极材料生产工艺配方.docx 12.高安全正极材料添加剂提升三元锂电安全性 13.高比能富锂锰基电池关键技术 14.高能量密度锂离子电池正极材料及应用研究 15.高镍单晶三元正极材料研究 16.三元高电压单晶正极材料

Zotero 是学术管理领域非常优秀的软件 它功能成熟、生态稳定,帮助许多科研人员建立了最初的文献归档体系

Zotero 是学术管理领域非常优秀的软件。它功能成熟、生态稳定,帮助许多科研人员建立了最初的文献归档体系。但在当下日常科研与深度使用各类 Agent 时,传统的文献工具链暴露出一些断点:上下文严重分散:论文存储在 Zotero,笔记记在 Obsidian,与模型的推导和分析留在各个网页对话框中。三者数据彼此隔离,当需要 Agent 结合某篇具体论文与历史笔记进行深入推导时,用户必须反复手动复制粘贴。文献格式对模型不友好:人类习惯阅读排版良好的 PDF 文件;但对于模型而言,直接解析双栏排版、跨页表格、复杂数学公式和交叉引用的成本极高,极易产生识别错误与幻觉。结构化 Markdown 与 LaTeX 才是模型容易准确理解的格式。维护成本较高:AI 伴读、全文翻译、笔记同步等高频需求,往往需要安装和调试多个社区插件,配置门槛高且在版本升级时容易出现兼容性问题。批量自动化能力有限:通过 Deep Research 或自动化检索工具发现的大量文献,目前仍需人工在浏览器中逐篇点击保存,难以顺畅衔接批量研究流。Agentero 并非旨在替代 Zotero,而是探索一种人与 Agent 协同处理科研上下文的新方式:人类负责:提出研究问题、进行学术价值判断、深度研读与核心结论推导;Agent 负责:批量检索筛选、元数据抓取、图表与公式提取、引用关系梳理及基础总结。

3D图形学学习总结(六)—背面消除与物体剔除

原文链接:http://www.nicemxp.com/articles/23 物体从局部坐标最后渲染到屏幕上我们已经基本了解了,但是在3d流水线中我们不能把世界中所有的物体和物体所有的面都渲染出来,这样在视景体中物体的背面或者不在视景体中的物体,对他们进行计算都是做无用功,所以有了物体的背面消除和物体剔除。 背面消除 物体的背面消除很简单,在世界坐标中进行,即相机坐标转换操作之前,物体的面...

nice_xp的博客 2280

基于 .net 8、blazor 、furion 开发的后台管理系统,功能全(完善的用户权限管理、定时任务、附件管理、日志审计、物联网、企业门户等)、技术新

基于 .net 7开发的后台管理系统,系统前后台分离,api 是基于Furion 框架开发,前端是基于ant-design-blazor开发,系统使用技术或框架较新

关于顶点法线向量的计算

在学习OpenGL的过程中,发现光照模型的渲染是需要指定法线向量的。无论是面法线还是点法线,不指定的话可能会造成不可预知的渲染效果。面法线的计算相对来说容易一些,说说点法线如何计算的。

Felix8Bit的专栏 1万+

材料科学基于电致塑性效应的多场耦合精密成形:难变形合金超薄带材低缺陷轧制工艺

内容概要:本文系统阐述了合金在电、热、力多场耦合作用下的电致塑性精密成形工艺(Electrically-Assisted Rolling, EAR),重点介绍其通过瞬时脉冲电流实现材料“成形时软、用时强”的核心技术原理。该工艺利用电子风力、焦耳热微区软化等非热与热效应协同作用,降低难变形合金的流动应力,提升单道次压下率(如SUS304由33.8%提升至41.5%),显著减少中间退火次数,实现数十微米级极薄带材的高效、低残余应力、高平整度轧制。文章详细分析了工艺参数(电流密度、频率、脉宽、温度窗口等)、装备配置(脉冲电源、绝缘辊系、在线检测)及失效模式控制,并强调需以“变形区有效电流密度”和“窄温区闭环控制”为核心进行工艺设计与优化。; 适合人群:从事金属材料加工、精密轧制、先进成形技术研发的科研人员与工程技术人员,具备材料科学与工程、机械制造或相关领域背景的研发工程师及研究生。; 使用场景及目标:①开发高强钛合金、不锈钢、特种合金等难变形金属的超薄精密箔材(如折叠屏基材、光刻机钢带);②优化现有冷轧工艺,降低能耗与退火次数,提升产品尺寸精度与表面质量;③研究电-热-力多场耦合成形机理,指导新型电辅助制造装备开发。; 阅读建议:本文兼具理论深度与工程实践指导意义,建议结合图表与实测数据理解多机制协同效应,重点关注电流回路设计、温度场闭环控制与参数标定方法,避免直接套用文献参数;在应用中应坚持“实验标定+仿真验证”原则,确保工艺安全性与可重复性。

电气工程基于电磁原理的继电器工作机理与应用:工业控制中强弱电隔离开关的设计选型及保护电路分析

内容概要:本文深入浅出地介绍了继电器的工作原理、核心结构、分类、常见应用场景及选型要点。继电器是一种利用小电流控制大电流的“自动开关”,通过线圈通电产生磁场,驱动衔铁吸合或释放,从而控制触点的通断。文章详细解析了其五大组成部分(线圈、铁芯、衔铁、弹簧、触点)和工作三步循环过程,强调了强弱电隔离的优势,并区分了常开、常闭和转换型触点的应用。同时,文章列举了继电器在工业控制、汽车电子、家用电器、新能源和电力系统中的典型应用,重点指出高压直流继电器在新能源汽车中的关键作用。针对选型,强调需根据负载类型(阻性、感性、容性)进行降额使用,并介绍续流二极管、RC吸收电路等保护措施以延长寿命。最后对比了电磁继电器与固态继电器的优缺点,帮助读者合理选择。; 适合人群:具备基础电工知识,从事电子、电气、自动化相关工作的技术人员或爱好者,以及工科类学生。; 使用场景及目标:①理解继电器在电路控制中的隔离与放大作用;②掌握继电器的选型方法与常见故障排查技巧;③应用于实际项目中如电机控制、电源管理、自动化系统设计等; 阅读建议:结合实物或电路图对照学习,重点关注触点保护与负载匹配部分,在实际应用中注意降额设计与环境因素影响。

Flash/Flex学习笔记(49):背面剔除与 3D 灯光

今天继续:上一回Flash/Flex学习笔记(50):3D线条与填充 里,我们知道任何一个3D多面体上的某一个面,都可以分解为多个三角形的组合。比立方体为例,每个面都由二个三角形组成,但在那一篇的示例中明显有一个问题:不管立方体的某一个面是不是应该被人眼看见(比如转到背面的部分,应该是看不见的),这一面都被绘制出来了。   在这一篇的学习中,我将带大家一起学习如何将背面(即看不见的面)删除掉,...

madfroghe 198

智算中心AI大模型赋能智能体平台及AI场景应用规划方案(决策评审版).pptx

智算中心AI大模型赋能智能体平台及AI场景应用规划方案(决策评审版).pptx

Making Things Move! 中文版

 Making Things Move! 中文版 今已全部杀青今天是我最后一次发部关于这本书的中文版(非官方“邮电”译版)的消息了,因为我已经将它全部译完了。    历经四个月的时间,今天终于把 Foundation Actionscript 3.0 Animation —— Making Things Move! 一书全部译完。非常感谢本书作者 Keith Peters 带给我们这么好的

xyzvalue的专栏 1296

Everything-v.1.4.1.1032 -X64-号称全世界查找文件最快的工具-便携版 .zip

Everything_v.1.4.1.1032 _X64_号称全世界查找文件最快的工具_便携版 .zip

基于可靠性评估序贯蒙特卡洛模拟法的配电网可靠性评估研究(Matlab代码实现)

内容概要:本文系统研究了基于序贯蒙特卡洛模拟法的配电网可靠性评估方法,重点在于通过Matlab编程实现对含分布式电源的配电网在多种运行条件下的可靠性量化分析。文章深入阐述了序贯蒙特卡洛模拟的核心原理,包括系统状态的时序抽样、状态转移机制以及故障与修复过程的随机建模,并构建了完整的仿真流程以计算系统平均停电频率(SAIFI)、系统平均停电持续时间(SAIDI)等关键可靠性指标。研究结合典型配电网络算例,验证了该方法在评估复杂电力系统可靠性方面的有效性与实用性,尤其分析了分布式电源接入对系统可靠性的影响机制,为现代配电网规划与运行提供了科学依据和技术支撑。; 适合人群:具备电力系统基础理论知识和一定Matlab编程能力的高校研究生、科研机构研究人员,以及从事配电网规划、运行与可靠性管理的工程技术人员。; 使用场景及目标:①深入掌握序贯蒙特卡洛模拟法在电力系统可靠性评估中的具体实施过程与技术细节;②学习并实践利用Matlab进行电力系统时序仿真与可靠性指标统计分析的方法;③探究分布式电源渗透对配电网整体可靠性水平的影响规律,为提升电网韧性提供决策参考; 阅读建议:读者应结合文中提供的Matlab代码实例进行实操演练,建议在理解算法逻辑的基础上,尝试修改系统参数、网络拓扑结构或调整分布式电源的配置方案,通过对比仿真结果加深对可靠性评估过程和影响因素的理解,该资源亦可直接应用于相关科研课题或工程项目的技术论证。

“AS3.0高级动画编程”学习:第三章等角投影(上)

什么是等角投影(isometric)? 刚接触这个概念时,我也很茫然,百度+google了N天后,找到了一些文章: [转载]等角(斜45度)游戏与数学 [转载]使用illustrator和正交投影原理以及基本三视图制图 以及这篇ppt:http://files.cnblogs.com/yjmyzz/Isometric.rar 建议先耐心看完这三篇文章,再往下看: 在之前学习的3D基础、...

weixin_30455067的博客 141

【Educoder作业】※ 数字图像——绿幕抠像

这次作业比上一篇博客基本得多,大多都是基础函数的运用。没啥说的,就整个sizesizesize即可。 T2 获取一个像素 T3 图像反色 T4 修改透明度 ′RGBA′'RGBA'′RGBA′也是一种图像格式 T5 抠图 T6 添加场景 这个主要是pastepastepaste函数里面的maskmaskmask参量没整明白,转载一篇博客供参考。...

JZYshuraK的博客 7571

脑语言2500单字v1.5.1

如果人和 AI 说同一种语言,文明会怎样?我用 2500 个单字试了试。2500个单字是不同领域、不同知识的共同体,无论你学编程或者是数学或写作,你都要用汉字,而常用的2500个汉字几乎可以涵盖不同行业做的不同事情。

脑语言 2020

【Embabel Agent】生产级最佳实践与避坑指南

本文围绕架构师最关心的 8 大维度展开:测试策略(FakeOperationContext 实现 Action 级单元测试、Prompt 质量与敏感信息校验)、MCP 集成实战(STDIO/HTTP 双模式、自建 Server、Docker MCP 生态)、RAG 检索增强、OpenTelemetry 全链路可观测性与追踪、Docker/K8s 生产部署、API Key 管理与 @Tool 安全防护、模型选择/缓存/并行等性能优化,以及过度依赖 LLM、忽略错误处理等 8 大常见反模式避坑指南。

program哲学 311

9、储备池学习:从学习到学习的新范式

本文探讨了基于学习到学习(L2L)框架的储备池计算新范式,提出通过在外循环中优化储备池的超参数(如递归连接权重、输入权重和读出初始化),以提升其在多种任务中的内循环学习性能。研究展示了两种主要实验设置:一是优化储备池用于回归二阶Volterra滤波器任务,显著优于随机基线;二是实现无需调整读出权重、仅依赖内部动态学习非线性函数的能力,表明储备池可快速构建目标函数的内部模型并安装任务先验知识。结合流程图与表格,文章系统呈现了L2L的训练流程、技术细节及实验结果,并讨论了性能提升、泛化能力等开放问题,指出了未来

ujm56789012的博客 506
上一篇: 转 第十六章 3D 线条与填充(2) (as3.0)
下一篇: 转 第十八章 矩阵数学 (as3.0)
coding_most
博客等级 码龄15年 13粉丝 38原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值