[转载]摄像机,光源和阴影 -----OGRE 3D 1.7 Beginner‘s Guide中文版 第四章

译者:whistleofmysong@gmail.com 博客 www.singmelody.com 

  迄今为止,我们总是创建静止的场景并且在场景中没有移动的物体。在这一章,我们将会改变这种现状。 

  在这章,我们将会学到:

l         认识什么是帧监听

l         认识如何处理用户输入

l         结合两种概念创建我们自己的摄像机控制

 

【 准备一个场景 】

   在添加移动物体之前,我们首先应创建一个可添加物体的场景。然后让我们一起创建一个场景吧。 

   我们将使用和之前章节中略微不同的创建场景的版本。 

   1. 删除createScene() 和createCamera() 函数中的所有代码:

   2. 删除createViewports()函数。

   3. 添加一个新的成员变量到类中。这个成员变量是一个场景结点的指针:

private:

Ogre::SceneNode* _SinbadNode;

 4. 使用createScene() 函数创建一个平面并添加它到场景之中:

Ogre::Plane plane(Vector3::UNIT_Y, -10);

Ogre::MeshManager::getSingleton().createPlane("plane",ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, plane,1500,1500,200,200,true,1,5,5,Vector3::UNIT_Z);

Ogre::Entity* ent = mSceneMgr->createEntity("LightPlaneEntity", "plane");

mSceneMgr->getRootSceneNode()->createChildSceneNode()->attachObject(ent);

ent->setMaterialName("Examples/BeachStones");

5. 然后添加一个光源到场景中:

Ogre::Light* light = mSceneMgr->createLight("Light1");

light->setType(Ogre::Light::LT_DIRECTIONAL);

light->setDirection(Ogre::Vector3(1,-1,0));

  6. 我们也需要一个Sinbad的实例,创建一个结点并关联实例。

Ogre::SceneNode* node = mSceneMgr->createSceneNode("Node1");

mSceneMgr->getRootSceneNode()->addChild(node);

Ogre::Entity* Sinbad = mSceneMgr->createEntity("Sinbad", "Sinbad.mesh");

_SinbadNode = node->createChildSceneNode("SinbadNode");

_SinbadNode->setScale(3.0f,3.0f,3.0f);

_SinbadNode->setPosition(Ogre::Vector3(0.0f,4.0f,0.0f));

_SinbadNode->attachObject(Sinbad);

 7. 我们也想在场景中增加阴影;所以添加它们:

mSceneMgr->setShadowTechnique(SHADOWTYPE_STENCIL_ADDITIVE);

8. 创建一个摄像机并放置它在(0,100,200) 并把镜头朝向(0,0,0);记住添加代码到createCamera() 函数中。

mCamera = mSceneMgr->createCamera("MyCamera1");

mCamera->setPosition(0,100,200);

mCamera->lookAt(0,0,0);

mCamera->setNearClipDistance(5);

编译运行,你将会看到下面图中的效果

【 刚刚发生了什么?】

  我们使用在之前章节学到的东西创建了一个场景。我们应可以理解发生了什么。如果没有的话,我们就应该回过头再看看之前的章节直到我们可以理解为止。

 

 

【 添加运动到场景之中 】

  我们已经创建了场景;现在让我们把运动添加进场景中。

  目前为止,我们只有一个类,即是,ExampleApplication。这次我们需要另一个类:

 1. 创建一个新类,命名为Example25FrameListener,并使它公有继承Ogre::FrameListener 

class Example25FrameListener : public Ogre::FrameListener

{

};

 2. 添加一个私有的成员变量,一个Ogre::SceneNode的指针,并把它命名为_node

private:

  Ogre::SceneNode* _node;

3. 添加接收一个Ogre::SceneNode指针为参数的公有构造函数,并把这个指针赋值操作给成员变量node 指针。

public:

  Example25FrameListener(Ogre::SceneNode* node)

  {

   _node = node;

  }

4. 添加一个新的frameStarted(FrameEvent& evt) 函数,在函数中用(0,0,0.1)变换成员变量node,然后返回 true:

bool frameStarted(const Ogre::FrameEvent  &evt)

{

  _node->translate(Ogre::Vector3(0.1,0,0));

  return true;

}

5. 添加一个FrameListener指针为成员变量。

Ogre::FrameListener* FrameListener;

 6. 添加一个初始化FrameListener为NULL的构造函数和一个当程序结束时删除FrameListener的析构函数。

Example25()

{

  FrameListener = NULL;

}

~Example25()

{

  if(FrameListener)

  {

  delete FrameListener;

  }

}

 

 现在,在ExampleApplication类中创建一个名为createFrameListener的新函数。在这个函数中,创建一个FrameListener的实例并添加它到mRoot:

void createFrameListener()

{

  FrameListener = new Example25FrameListener(_SinbadNode);

  mRoot->addFrameListener(FrameListener);

}

 8. 编译运行程序。你应看到和之前相同的场景。但是这次,Sinbad的实例向右边移动了并且你不能移动摄像机或用Escape键关闭程序。你可以使用控制台窗口的X按钮关闭窗口。如果你从控制台启动程序,你可以使用CTRL+C 来结束程序。

【 刚刚发生了什么?】

   我们添加了一个可以移动我们场景结点的类到我们的代码中。

 

【 帧监听 】(FrameListener)

  我们在此遇到的一个新概念就是帧监听。顾名思义,帧监听是基于一个观察者的模式。我们可以添加一个类的实例。你可以使用Ogre::Root的addFrameListener()方法,通过继承自Ogre::FrameListener的接口,添加一个对象到我们的Ogre3D根对象中。当对象被添加时,在确定的事件发生时我们的类就会得到通知。在这个例子中,我们重写了frameStarted()方法。  

      在帧(帧,意思是指场景中的单个图像)被渲染之前,Ogre::Root 遍历所有被添加的Framelistener并调用其frameStarted()方法。我们第四步中的函数定义中,我们沿X轴每帧0.1单位的速率变换结点。这个结点通过Framelistener的构造函数的传入。因此,场景每次被渲染的时候,结点就变换一点位置。结果,模型就移动了。正如我们在程序运行时看到的,我们不能移动摄像机或使用Escape 键退出。这是因为这些是由Framelistener来操作的,而这个FrameListener是ExampleApplication 框架自带的。ExampleApplication是SDK自带的。现在我们代替其原有的FrameListener,使用我们自己定义的实现过程。这样我们就不能使用自带的Framelistener,但是这章我们将会重新实现他们中的大部分,所以不用担心。如果需要,我们可能仍然调用基类的成员函数来获得默认的行为。在第四步中,我们函数返回true。如果返回false,Ogre 3D 将会解释false为退出渲染循环的标志,而与此同时,程序就会结束。我们将会重新实现“按Escape键退出”的函数。

 

【 简单测试 —— FrameListener的设计模式 】

  1. Framelistener概念的模式是基于?

  a.  Decorator 【装饰者】

  b.  Bridge     【桥】

  c.  Observer 【观察者】

 

 

【 修改代码使其代替基于每帧的运动为基于时间的运动 】

  根据您的电脑,场景中的模型可能移动的或快或慢或是刚刚好的速度。造成不同速度移动模型的原因是,在我们代码中,在每次渲染一个新帧之前,我们沿Z轴每帧移动模型0.1个单位。一个新的电脑可能以每秒100帧的速度渲染,那么模型将会每秒移动10个单位。当使用一个旧电脑的时候,可能就是30帧每秒,那么模型就只会移动3个单位。这相比新电脑只是不到三分之一。通常,我们想要程序在不同的平台和电脑性能下保持一致,这样他们就以相同的速度运行。这可以被Ogre简单的完成。

  我们将会使用之前的代码并指改变一行代码:

  1. 改变我们变换结点的那行代码:

_node->translate(Ogre::Vector3(10,0,0) * evt.timeSinceLastFrame);

  2. 编译运行程序。你可能会看到相同的场景,只有模型会以不同的速度移动。

 

【 刚刚发生了什么?】

   我们改变基于帧的运动模式为基于时间的模式。我们添加一个简单的乘法了实现基于时间的模式。正如前面所说,基于帧的运动有一些缺点。基于时间的运动更为出众,因为我们需要在所有的电脑上达到同样的运动效果而且在运动速度上有更高的可控性。在第一步,我们使用Ogre 3D的一个FrameEvent传递给调用的frameStarted() 函数。这个FrameEvent类包含了在短时间内自从上一帧被渲染到现在的时间:

       (Ogre::Vector3(10,0,0) * evt.timeSinceLastFrame);

  这行代码使用这个公式来计算移动模型每帧移动的大小。我们使用一个三维向量乘以自上一帧到现在的秒数来计算。在当前的情况下,我们使用了向量(10,0,0)。这意味着我们想要模型每秒沿X轴移动10个单位。比如说,我们每秒渲染10帧;然后对于每一帧,evt.timeSinceLastFrame将会是0.1f。在每帧中,我们用向量(10,0,0)乘以evt.timeSinceLastFrame,那么结果会是(1,0,0).这一结果将会应用于场景结点移动的每一帧。

 

【 简单测试 —— 基于时间和基于帧运动之间的不同 】

  用你自己的话描述基于帧和基于时间运动的不同。 

  让英雄动起来 —— 添加第二个模型 】

  添加第二个模型到场景并使它沿场景中Sinbad模型的相反方向运动。

 

 

【 添加输入支持 】

  我们现在已经有含移动物体的场景,但是我们想要程序可以像原来一样退出。因此,我们将会添加输入支持,并当Escape键按下的时候,我们可以退出程序。目前为止,我们只使用了Ogre 3D;现在,我们将会使用OIS (Object Oriented Input System[面向对象的输入系统]),OIS是Ogre 3D SDK自带的,因为它被ExampleFrameListener所使用,但是在其他方面它是从Ogre 3D 中完全独立出来的。

  同样的,我们使用之前的代码并添加必要的代码以获得输入支持:

  1. 我们需要添加一个新的参数到Listener的构造函数。

Example27FrameListener(Ogre::SceneNode* node,RenderWindow* win)

   2. 当改变构造函数时,我们也需要改变实例化的地方:

Ogre::FrameListener* FrameListener = new Example27FrameListener(_

SinbadNode,mWindow);

  3. 在这之后,我们需要添加代码到listener的构造函数。首先,我们需要两个辅助变量: 

size_t windowHnd = 0;

std::stringstream windowHndStr;

  4. 现在请求Ogre 3D获取渲染的窗口句柄:

win->getCustomAttribute("WINDOW", &windowHnd);

  5. 转换handle为一个string:

windowHndStr << windowHnd;

  6.创建一个OIS参数表并添加窗口句柄:

(译者注:这章中参数表代表ParamList,而ParamList是OISPrereqs.h中std::multimap<std::string, std::string>的别名,而multimap请参考STL的容器。)

OIS::ParamList pl;

pl.insert(std::make_pair(std::string("WINDOW"), windowHndStr.str()));

   7. 使用参数表创建输入系统:

_man = OIS::InputManager::createInputSystem( pl );

  8. 然后创建一个keyboard,这样我们就可以检查用户的输入了:

_key = static_cast<OIS::Keyboard*>(_man->createInputObject(OIS::OISKeyboard, false ));

9. 我们创建的对象,必须在最后回收。添加一个析构函数到帧监听类中,这将会析构我们的OIS对象:

~Example27FrameListener()

{

  _man->destroyInputObject(_key);

  OIS::InputManager::destroyInputSystem(_man);

}

10. 在我们完成初始化之后,添加下面的代码到frameStarted() 函数中的node变换之后,return之前:

_key->capture();

if(_key->isKeyDown(OIS::KC_ESCAPE))

{

  return false;

}

 11. 添加用户输入对象作为成员变量到帧监听类中:

OIS::InputManager* _man;

OIS::Keyboard* _key;

  12. 编译运行程序。你现在应该可以看到程序可以通过按Escape键退出。

 

【 刚刚发生了什么?】

   我们创建了一个输入系统的实例并使用实例来获取用户的键盘输入。因为我们需要窗口的句柄来创建输入系统,我们改变帧监听类的构造函数,这样它就可以接受传递过来的一个渲染窗口的指针。这已经在第一步中完成了。我们然后从数值到字符串转换句柄并添加字符串到OIS的参数表。通过参数表,我们可以创建我们输入系统的实例。

 

 

【 窗口句柄 】

    一个窗口句柄仅仅是一个识别窗口的数值。这个数值是由操作系统创建并且每个窗口都有自己独一无二的句柄。输入系统需要这个句柄,因为没有它,输入系统就不能获取输入事件。Ogre 3D 为我们创建了一个窗口。所以为了获得窗口句柄,我们需要请求Ogre 3D执行下面一行代码:

win->getCustomAttribute("WINDOW", &windowHnd);

  渲染的窗口有多种属性,Ogre 3D 实现了一个通用getter 函数。Ogre3D也需要平台独立,因为每个平台都有自己的窗口句柄的变量类型,所以通用的函数是跨平台的唯一方法。这段代码中,WINDOW是窗口句柄的标识符。我们需要传递传递一个指针来存贮句柄的值;在函数中这个指针将会被覆写。在我们接受句柄之后,我们使用stringstream来转换它为一个string类型,因为这是OIS所需要的。OIS有同样的问题并使用同样的解决办法。在创建时,我们给OIS一个对组的参数表,这个对组是由一个标识字符串和一个字符串的值组成。  

  在第六步中,我们创建了这个参数表并添加字符串类型的句柄。在第七步使用这个参数表创建了输入系统。通过运用输入系统,我们可以在第八步中创建我们键盘接口。这个接口将会用于查询系统——用户按下的是哪个键?这一步将会在第九步中完成(译者:原作者应该指的是第十步)。每次在我们渲染帧之前,我们使用capture() 函数来获取键盘的新状态。如果不调用这个函数,我们将不会得到键盘的新状态,并此因我们将永远得不到键盘事件了。在更新完状态之后,我们查询键盘是否现在按下Escape键。当按下的时候,我们了解到用户想要退出用户程序。这意味着我们必须返回false让Ogre 3D知道我们关闭程序。否则,如用户想要程序持续运行,我们可以返回true来使程序持续运行。 

【 简单测试 —— 有关窗口的问题 】

  什么是窗口句柄还有它是如何被程序和操作系统所使用的?

 

 

【 给模型添加动作 】

   现在我们已经有能力获取用户的输入,现在让我们使用它来控制Sinbad的在平面上的运动吧。

  我们使用以前的代码并添加代码到我们需要的地方,正如之前所做的一样。我们将会使用以下代码,使用WASD键来控制Sinbad。

if(_key->isKeyDown(OIS::KC_W))

{

  translate += Ogre::Vector3(0,0,-10);

}

if(_key->isKeyDown(OIS::KC_S))

{

  translate += Ogre::Vector3(0,0,10);

}

if(_key->isKeyDown(OIS::KC_A))

{

  translate += Ogre::Vector3(-10,0,0);

}

if(_key->isKeyDown(OIS::KC_D))

{

  translate += Ogre::Vector3(10,0,0);

}

  4. 现在使用向量来变换模型,并记住使用基于时间的运动而非基于帧的运动:

_node->translate(translate*evt.timeSinceLastFrame);

  5. 编译运行程序,然后你就可以用WASD键来控制Sinbad了。

【 刚刚发生了什么?】

   我们使用WASD键为用户添加基本的运动控制。我们查询所有的四个键并创建了存储运动的变量。我们使用基于时间的方式应用这个变量到模型。

 

 

【 让英雄动起来 —— 使用决定运动的速度因素 】

   上面方法的缺点是我们想要改变模型的运动速度的时候,我们不得不改变四个向量。更好的方法是使用一个向量来表示运动方向,并使用一个float作为速度因子并使速度因子乘以变换向量。使用一个速度变量来改变代码。

【 添加一个摄像机 】

   我们已经可以使用Escape并且我们可以移动Sinbad了。现在轮到我们的摄像机工作了。 

   我们已经创建了我们摄像机。现在我们将会让它和我们用户输入结合起来。

  1. 扩展帧监听的构造函数,让它接受一个camera指针:

Example30FrameListener(Ogre::SceneNode* node,RenderWindow* win,Ogre::Camera* cam)

  2. 同样添加一个成员变量来储存camera指针:

Ogre::Camera* _Cam;

   3.然后添加参数到成员变量:

_Cam = cam;

  4. 修改帧监听的初始化并添加camera指针:

Ogre::FrameListener* FrameListener = new Example30FrameListener(_

SinbadNode,mWindow,mCamera);

  5.我们需要获得鼠标的输入以移动摄像机。所以创建一个新的成员变量来存贮鼠标:

OIS::Mouse* _mouse;

  6.在构造函数中,在键盘操作后初始化鼠标:

_mouse = static_cast<OIS::Mouse*>(_man->createInputObject( OIS::OISMouse, false ));

  7. 现在我们已经有鼠标的状态了,我们也需要获取鼠标的状态。在获取键盘状态后面添加下面一行代码:

_mouse->capture();

  8. 删去下面变换结点的代码:

_node->translate(translate*evt.timeSinceLastFrame * _movementspeed);

  9. 在frameStarted()函数中处理完键盘状态后,添加下面的代码来处理鼠标状态:

float rotX = _mouse->getMouseState().X.rel * evt.timeSinceLastFrame* -1;

float rotY = _mouse->getMouseState().Y.rel * evt.timeSinceLastFrame * -1;

  10. 现在应用旋转和变换到摄像机:

_Cam->yaw(Ogre::Radian(rotX));

_Cam->pitch(Ogre::Radian(rotY));

_Cam->moveRelative(translate*evt.timeSinceLastFrame * _movementspeed);

  11. 我们创建了一个鼠标对象,所以我们在帧监听的析构函数中销毁它。

_man->destroyInputObject(_mouse);

  12 编译运行程序。你将会像之前一样操控场景了。

 

【 刚刚发生了什么?】

   我们使用创建的摄像机同用户输入结合起来。为了控制摄像机,我们需要传递它到帧监听。我们在第一步和第二部中的构造函数中实现了这一点。我们也需要使用鼠标来控制我们的摄像机。所以第一我们不得不创建一个鼠标接口,这个已经在第六步中完成了,和我们创建键盘控制的方式一样。在第七步,我们调用了新鼠标接口的capture() 函数来更新我们的鼠标状态。

 

【 鼠标状态 】

   我们使用isKeyDown() 函数来完成查询键盘状态。这次我们使用getMouseState() 函数来完成查询鼠标状态的操作。这个函数返回一个鼠标状态作为MouseState类的实例,它包含了按钮按下与否和从上次获取鼠标状态开始鼠标是如何移动的按钮状态信息。

  我们想要移动的信息是为计算摄像机需要旋转的大小。鼠标移动发生在了两个轴,即X轴和Y轴。两个轴的移动是由两个鼠标状态变量分开存贮的。我们然后就可以得到相对或绝对值。因为我们只对鼠标移动而非鼠标的位置的感兴趣,我们使用相对值。而绝对值包含包含鼠标在屏幕上的信息。当鼠标点进我们程序的确切区域的时候绝对值很是需要了。对于摄像机旋转,我们只需要鼠标移动信息,所以使用相对值。相对值只是描述出鼠标移动过的速度和方向是否改变,并不是描述所在位置的值。

  这些值乘以自上一帧到现在的时间和-1。使用-1是因为我们想让摄像机根据我们的需要进行旋转运动。所以我们需改变移动的方向。在计算完旋转的值,我们应用它们到yaw() 和pitch() 函数。最后一件事是应用我们从键盘输入创建的变换向量到摄像机。这个很有用,因为我们知道在局部空间中使用 (0,0,-1) 可以把摄像机向前移动。但当旋转应用于摄像机,这个就不一定正确了。请参考有关在三维空间中不同的空间的第二章寻找更为详尽的解释。

 

【 简单测试 —— 获取输入 】

   为什么我们调用鼠标和键盘的capture() 方法?

 

 

【 让英雄动起来 —— 试玩一下例子 】

   尝试从旋转计算中移除-1并观察摄像机控制如何改变。

 

【 添加线框模式和点模式 】

  在之前的第三章(摄像机,光源和阴影),我们使用R键来改变渲染模式为线框模式或点模式。现在我们添加这个特性到我们的帧监听。

  我们使用刚创建的代码,并一如既往简单的添加的代码来实现我们想要的新特性:

  1. 我们需要在帧监听中添加一个新的成员变量在保存现在的渲染模式:

Ogre::PolygonMode _PolyMode;

  2. 在构造函数中用PM_SOLID初始化创建的变量。

_PolyMode = Ogre::PolygonMode::PM_SOLID;

  3. 然后我们在frameStarted()中添加一个新的函数,来测试R键是否按下。如果情况是这样,我们可以改变渲染模式。如果目前的模式为实体模式,我们想要它成为线框渲染模式。

if(_key->isKeyDown(OIS::KC_R))

{

  if(_PolyMode == PM_SOLID)

  {

   _PolyMode = Ogre::PolygonMode::PM_WIREFRAME;

  }

  如果它是线框型的,我们想要改变为点模式:

else if(_PolyMode == PM_WIREFRAME)

  {

   _PolyMode = Ogre::PolygonMode::PM_POINTS;

  }

  4. 如果它是点模式,最后让它变回实体模式:

else if(_PolyMode == PM_POINTS)

  {

   _PolyMode = Ogre::PolygonMode::PM_SOLID;

  }

  6. 现在我们已经设置了新的渲染模式,我们可以应用它并关闭if 语句了:

_Cam->setPolygonMode(_PolyMode);

}

  7. 编译运行程序;你应会看到按过R键后,渲染模式改变:

 

【 刚刚发生了什么?】

   我们使用了函数setPolygonMode() 来改变实体模式为线框模式,然后是点模式。我们总是保存最后一个值。这样当改变模式时,我们知道当下的模式时什么,我们将会改变为什么模式。我们从实体模式改变为线框模式到点模式,然后又变为实体模式。我们注意到,当按下R键时,渲染模式改变的相当迅速。这个因为我们每一帧都检查R键是否犯下并且相对于计算机的每帧的速度,人类按下R键的速度是很慢的。这导致,我们的程序会认为我们在短时间内按下多次R键,这就意味着不定的哪帧将会切换到线框模式。这不是最理想的情况,但存在一种办法让我们做的的更好,我们下面将会看到。

 

【 添加一个计时器 】

   解决渲染模式改变过快的一种解决办法就是使用计时器。每一次我们按下R键,一个计时器就启动了,并且只有当足够的时间过去,我们才能处理另一个R键的按下。】

  1. 添加一个计时器作为帧监听的成员变量:

Ogre::Timer _timer;

  2. 在构造函数中复位计时器:

_timer.reset();

  3. 现在添加一个来检查自从上次按下R键是否有0.25秒过去:

if(_key->isKeyDown(OIS::KC_R) && _timer.getMilliseconds() > 250)

{}

  4. 如果过去足够的时间,我们需要复位。否则,R键只能按下一次:

_timer.reset();

  5. 编译运行程序;当现在按下R键,它将改变渲染模式为下一个。

【 刚刚发生了什么?】

  我们使用了Ogre 3D的另一个新类,即是Ogre::Timer。顾名思义,这个类提供了计时器的功能.我们在帧监听的构造函数中复位了计时器,并在每次用户R键,我们会检查自动上次调用reset()函数,0.25秒是否过去。如果是这种情况,我们输入if的代码段,复位计时器而且像之前一样的方式改变需渲染模式。这必须保证需渲染模式时在0.25秒之后改变的。当我们一直按着R键,我们看到程序在每次0.25秒的等待之后,将会改变渲染模式。

 

 

【 让英雄动起来 —— 改变输入模式 】

  通过改变代码使渲染模式不是在一个确定的时间过去才可改变,只有当释放R键并再次按下的时候才改变。

 

 

【 概要 】

  在这一章,我们学习了帧监听的借口和如何使用它们。我们也学习了如何启动OIS,在这之后,我们学习了如何查询键盘和鼠标的状态:】

   

  具体的,我们学习了:

       *     当新一帧被渲染的时候,如何得到通知。

       *     基于帧和基于时间运动的重要不同。

       *     如何使用用户输入实现我们的摄像机移动。

       *     如何改变摄像机的渲染模式。

 

  现在我们已经实现了我们帧监听的基础函数,我们将在下一章学习如何运动模型。

转载于:https://www.cnblogs.com/oneDouble/articles/2545706.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值