> 浏览贴子 共
篇贴子
浏览
次 回复
Ophone平台2D游戏引擎实现物理引擎二(2)
(吧主)
使用Opengl绘制图形(DrawObject)
DrawObject主要用于替代JBox2d中的图像渲染部分,但是这里我们只是实现了绘制矩形和圆形,还有更多的没有实现,该游戏中也将主要使用这样两种形状。DrawObject的实现几乎就全是Opengl相关的内容,我们先看具体代码,在来分析,如代码清单13-2所示。
代码清单13-2:DrawObject.java
public cla DrawObject { // 顶点缓冲区 private FloatBuffer mVertexBuffer; // 索引缓冲区 private ShortBuffer mIndexBuffer; // 纹理坐标缓冲区 private FloatBuffer mTexBuffer; // 顶点计数 private int vertexCount = 0
; // 是否拥有贴图 private boolean hasTexture = false
; // 纹理 private int
[] mTexture = new int
]; // float[]-FloatBuffer protected static FloatBuffer makeFloatBuffer(
[] arr) { ByteBuffer = ByteBuffer.allocateDirect(arr.length * 4
); .order(ByteOrder.nativeOrder()); FloatBuffer f = .asFloatBuffer(); fb.put(arr); fb.position(
); return f } // hort[]-ShortBuffer protected static ShortBuffer makeShortBuffer(
[] arr) { ByteBuffer = ByteBuffer.allocateDirect(arr.length * 4
); .order(ByteOrder.nativeOrder()); ShortBuffer i = .asShortBuffer(); ib.put(arr); ib.position(
); return i } // 构造(顶点数组,纹理数组,索引数组,顶点数) public DrawObject(
[] coords, float
[] tcoords, short
[] icoords, int vertexes) { this
(coords, icoords, vertexes); mTexBuffer = makeFloatBuffer(tcoords); } // 构造(顶点数组,索引数组,顶点数) public DrawObject(
[] coords, short
[] icoords, int vertexes) { vertexCount = vertexe mVertexBuffer = makeFloatBuffer(coords); mIndexBuffer = makeShortBuffer(icoords); } // 装载贴图(gl,context,资源id) public void loadTexture(GL10 gl, Context mContext, int mTex) { hasTexture = true
; // 生成纹理 gl.glGenTextures(
, mTexture, 0
); // 绑定纹理 gl.glBindTexture(GL10.GL_TEXTURE_2D, mTexture[
]); // 资源Bitmap Bitma itma itma = BitmapFactory.decodeResource(mContext.getResources(), mTex); // 指定纹理图像 GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0
, itmap, 0
); itmap.recycle(); // 设置纹理参数 gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR); gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR); } /*----------------------------------------------------------*/ //渲染obj /*----------------------------------------------------------*/ public void draw(GL10 gl) { //判断是否拥有纹理 if (hasTexture) { //打开2d纹理 gl.glEnable(GL10.GL_TEXTURE_2D); //绑定纹理 gl.glBindTexture(GL10.GL_TEXTURE_2D, mTexture[
]); //设置纹理缓冲区 gl.glTexCoordPointer(
, GL10.GL_FLOAT, 0
, mTexBuffer); } else { //关闭2d纹理 gl.glDisable(GL10.GL_TEXTURE_2D); } //设置逆时针方向为正面 gl.glFrontFace(GL10.GL_CCW); //设置顶点数组 gl.glVertexPointer(
, GL10.GL_FLOAT, 0
, mVertexBuffer); //绘制 gl.glDrawElements(GL10.GL_TRIANGLE_FAN, vertexCount, GL10.GL_UNSIGNED_SHORT, mIndexBuffer); //关闭2d纹理 gl.glDisable(GL10.GL_TEXTURE_2D); } public void draw(GL10 gl, float x, float y, float z, float rot, float cale) { this
.draw(gl, x, y, z, rot, cale, cale); } //draw(gl,x,y,z,旋转角度,x方向缩放,y方向缩放) public void draw(GL10 gl, float x, float y, float z, float rot, float caleX, float caleY) { gl.glPushMatrix(); gl.glTra latef(x, y, z); gl.glRotatef(rot, 0f, 0f, 1f); gl.glScalef(scaleX, caleY, 1f); this
.draw(gl); gl.glPopMatrix(); } public void draw(GL10 gl, float x, float y, float z, float rot) { gl.glPushMatrix(); gl.glTra latef(x, y, z); gl.glRotatef(rot, 0f, 0f, 1f); this
.draw(gl); gl.glPopMatrix(); } public void draw(GL10 gl, float x, float y, float z) { gl.glPushMatrix(); gl.glTra latef(x, y, z); this
.draw(gl); gl.glPopMatrix(); } } 代码并不多,其实一个DrawObject对象将代表一个物体,通常绘制一个物体,主要包括顶点缓冲区(mVertexBuffer),索引缓冲区(mIndexBuffer),纹理坐标缓冲区(mTexBuffer),同时我们通过vertexCount来记录顶点的个数,hasTexture检测是否有纹理,没有就将使用颜色作为纹理,mTexture数组就表示纹理ID。
其中有两个静态函数makeFloatBuffer和makeShortBuffer用于将数组转换成对应的缓冲区,在Android中使用java来编写Opengl ES程序,在传递顶点等数组时需要使用缓冲区,转换过程很简单,首先构建一个和数组一样的缓冲区,然后检索该缓冲区的字节顺序,最后将数组作为缓冲区即可。
构造函数很简单,将绘制该图形所需要数据传入即可,两个构造函数,其中一个就说明了当没有纹理坐标时,我们就不需要设置纹理坐标缓冲区,直接通过颜色来作为材质即可。
装载纹理需要使用loadTexture函数,首先将hasTexture设置为有纹理存在,glGenTextures函数用于根据纹理参数返回n个纹理名称,用来生成纹理名字的数量、存储纹理名称数组、以及存放在纹理数组中的偏移量。glBindTexture函数实现了将调用glGenTextures函数生成的纹理的名字绑定到对应的目标纹理上,其参数分别是:纹理被绑定的目标(在Opengl ES中它只能取值GL_TEXTURE_2D)和纹理的名称,并且,该纹理的名称在当前的应用中不能被再次使用。然后通过BitmapFactory.decodeResource取得纹理图片资源,然后通过GLUtils.texImage2D函数将纹理图片像素数据绑定到Opengl对象中。GLUtils.texImage2D函数参数的含义如下:
target:指定目标纹理,必须为GL_TEXTURE_2D
level:指定图像级别的编号,0表示基本图像
itmap:纹理图片数据
order:纹理图像的边框宽度,必须是0或1
如果我们使用glTexImage2D函数,用来指定二维纹理图像,他还有以下几个参数可以使用:
components:纹理中颜色组件的编号,可是是1或2或3或4
width:纹理图像的宽度
height:纹理图像的高度
format:指定像素数据的格式,一共有9个取值:GL_COLOR_INDEX、GL_RED、GL_GREEN、GL_BLUE、GL_ALPHA、GL_RGB、GL_RGBA、GL_BGR_EXT、GL_BGRA_EXT、GL_LUMINANCE、GL_LUMINANCE_ALPHA
type:像素数据的数据类型,取值可以为GL_UNSIGNED_BYTE, GL_BYTE, GL_BITMAP, GL_UNSIGNED_SHORT, GL_SHORT, GL_UNSIGNED_INT, GL_INT, and GL_FLOAT
ixels:内存中像素数据的指针
设置好纹理图像数据之后,我们需要使用recycle来将该图片数据释放掉,因为在opengl es中它将自己保存一份纹理数据。接着需要设置纹理参数,可以使用glTexParameteri函数或者glTexParameterf函数,其参数的含义如下:
arget:目标纹理,必须为GL_TEXTURE_1D或GL_TEXTURE_2D;
ame:用来设置纹理映射过程中像素映射的问题等,取值可以为:GL_TEXTURE_MIN_FILTER、GL_TEXTURE_MAG_FILTER、GL_TEXTURE_WRAP_S、GL_TEXTURE_WRAP_T
aram:实际上就是 ame的值
另外还可以使用如下代码,实现线形滤波的功能,当纹理映射到图形表面以后,如果因为其它条件的设置导致纹理不能更好地显示的时候,进行过滤,按照指定的方式进行显示,可能会过滤掉显示不正常的纹理像素。
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR) glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR) 剩下的都是一些绘制函数了,用来根据不同的条件进行不同的渲染。最完整的渲染条件是包括:x,y,z,旋转角度,x方向缩放,y方向缩放。整个过程就是将矩阵压栈(glPushMatrix),然后进行变化,绘制等操作之后,在将矩阵弹出栈(glPopMatrix)。为什么要这样呢?因为我们在变换坐标的时候,使用的是glTra latef(),glRotaef()等函数来操作,操作的是当前矩阵,这些变化,将会对矩阵进行相乘,使之改变了当前的矩阵,当我们再次使用该矩阵时,就已经被改变而变得很难控制了,所以,我们在进行变换操作之前都需要将将矩阵进行压栈保存起来,操作完成之后,弹出栈的矩阵则和操作前的矩阵一样。下面我们分析一下具体的绘制函数"public void draw(GL10 gl)"。
首先判断是否有纹理,如果有则通过glEnable(GL10.GL_TEXTURE_2D)打开2D纹理,然后通过glBindTexture来绑定纹理,前面我们允许设置了纹理坐标缓冲区,所以这里也需要通过glTexCoordPointer来设置纹理坐标缓冲区;如果没有纹理,则通过glDisable(GL10.GL_TEXTURE_2D)关闭2D纹理映射。然后通过glFrontFace来设置正面,分为逆时针和顺时针,glVertexPointer可以设置顶点缓冲区,使用glDrawElements进行渲染操作,渲染完成之后切忌关闭2D纹理映射。
glDrawElements函数参数定义如下:
mode:指定绘制图元的类型,它应该是下列值之一,GL_POINTS, GL_LINE_STRIP, GL_LINE_LOOP, GL_LINES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_TRIANGLES
count:为绘制图元的数量
type:为索引值的类型,只能是下列值之一:GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT, or GL_UNSIGNED_INT
indices:索引缓冲区
物理世界(PhysicsWorld)
看这个标题好像很复杂,的确物理世界很复杂,但是我们这里使用JBox2d就会变得很简单,同样,我们先看程序,然后再来分析。如代码清单13-3所示。
代码清单13-3:PhysicsWorld.java
public cla PhysicsWorld { //能够添加的实体总数 final private int MAXBALLS ,= 20
; //f final private float FRAMERATE = 30f; private float timeSte = (1f / FRAMERATE); private int iteratio = 5
; //动态实体计数 private int count = 0
; //世界边界 private AABB worldAABB; //世界场景 private World world; public void createWorld() { // 创建时间边界 worldAABB = new AABB(); //下限 worldAABB.lowerBound.set(
new Vec2(-100f, -100f)); //上限 worldAABB.u erBound.set(
new Vec2(100f, 100f)); // 创建一个世界场景并设置重力系数 Vec2 gravity = new Vec2(0f, -10f); //是否允许引擎睡眠 boolean doSlee = false
; world = new World(worldAABB, gravity, doSleep); } //为世界场景设置重力 public void etGrav(
float x, float y) { world.setGravity(
new Vec2(x, y)); } //更新世界场景 public void update() { world.step(timeStep, iteratio ); } //实体数量 public int getCount() { return count; } //实体列表 public Body getBodyList() { return world.getBodyList(); } // 添加一个box和ball public void addBox(
float x, float y, float xr, float yr, float angle, boolean dynamic) { if (count lt (MAXBALLS - 1
)) { //创建刚体 BodyDef groundBodyDef; groundBodyDef = new BodyDef(); //设置刚体的位置角度 groundBodyDef.position.set(
new Vec2(x, y)); groundBodyDef.angle = angle; //根据定义的刚体创建物体 Body groundBody = world.createBody(groundBodyDef); //创建多边形 PolygonDef groundShapeDef; groundShapeDef = new PolygonDef(); //设置边框 groundShapeDef.setAsBox(xr, yr); //设置物体密度 groundShapeDef.de ity = 1
.0f; //创建图形 groundBody.createShape(groundShapeDef); if (dynamic) { //根据形状计算物体的质量 groundBody.setMa FromShapes(); } // 计数增加 if (dynamic) { count++; } } } public void addBall(
float x, float y, float r, boolean dynamic) { if (count lt (MAXBALLS - 1
)) { BodyDef groundBodyDef2; groundBodyDef2 = new BodyDef(); groundBodyDef2.position.set(
new Vec2(x, y)); Body groundBody2 = world.createBody(groundBodyDef2); //创建一个圆形 CircleDef groundShapeDef2; groundShapeDef2 = new CircleDef(); groundShapeDef2.radiu = r; groundShapeDef2.de ity = 1
.0f; groundBody2.createShape(groundShapeDef2); if (dynamic) { groundBody2.setMa FromShapes(); } if (dynamic) { count++; } } } } 2010-11-17 9:59:45
2010-11-17 13:24:23
(会员)
2010-11-18 10:31:53
2010-12-4 11:06:28
(会员)
2010-12-4 14:21:17
(会员)
2010-12-4 14:23:24
(副吧主)
2010-12-15 18:57:36
内容:
验证码: 请点击后输入验证码,加入会员后本吧发贴免验证码。 !尚未登录请先
最近活跃话题
Copyright 2008 北京汇众益智科技有限公司. All Rights Reserved
京ICP备09092043号> 浏览贴子 共
篇贴子
浏览
次 回复
Ophone平台2D游戏引擎实现物理引擎一(2)
(吧主)
其中org.jbox2d.collision比较重要,主要负责处理碰撞相关,包括对一些多边形的实现,这里所说的多边形主要是一些数据,比如多边形的位置,大小,重力,形状,质量等属性;org.jbox2d.common包主要用来设置一些全局的属性(Setting.java),调试时所使用的颜色(Color3f.java),以及其他的一些数学相关的内容,因为我们说了Box2d他主要不是来做渲染的,但是有时候我们需要知道所设置的这些物体是否正确,进行调试,就需要绘制这些简单的图形,并显示出来,供我们调试;org.jbox2d.dynamics包主要负责动力学相关的内容,下面是常见的功能包描述。
org.jbox2d.collision包
AABB:AABB坐标
OBB:OBB坐标
ContactID:接触ID
ContactPoint:接触点
ManifoldPoint:繁殖点
Segment:线段
Shape:外形基类
ShapeDef:外形定义基类
CircleDef:圆外形定义
CircleShape:圆外形
FilterData:碰撞过滤器
Ma Data:质量运算器
PolygonDef:多边开定义
PolygonShape:凸多边形
org.jbox2d.common包
Color3f:调试绘图颜色
Settings:全局设置
Mat22:2*2 矩阵
Sweep:碰撞描述
Vec2:向量(x ,y)
XForm:坐标转换,平移或旋转
标准的版本中还会存在Mat33表示3*3的矩阵和Vec3向量(x,y,z),该java版本中没有出现这些。
org.jbox2d.dynamics包
Body:刚体或叫物体
BodyDef:刚体定义
BoundaryListener:世界边界侦听
ContactFilter:继承这个类用来获取过滤碰撞
ContactListener:继承这个类用来获取碰撞结果
DebugDraw:调试绘图,用于调试
DestructionListener:关节或外形销毁时处理方法
World:物理世界
org.jbox2d.dynamics.contacts
Contact:管理两个外形接触
ContactEdge:接触边用来连接多个物体和接触到一个接触表
ContactResult:记录接触结果
org.jbox2d.dynamics.Joints
DistanceJoint:距离校正器
DistanceJointDef:距离连接定义
GearJoint:齿轮
GearJointDef:齿轮连接定义
Joint:连接基类
JointDef:连接定义基类
JointEdge:用于组合刚体或连接到一起.刚体相当于节点,而连接相当于边
MouseJoint:鼠标连接
MouseJointDef:鼠标连接定义
PrismaticJoint:棱柱连接
PrismaticJointDef:棱柱连接定义
PulleyJoint:滑轮连接
PulleyJointDef:滑轮连接定义
RevoluteJoint:旋转连接
RevoluteJointDef:旋转连接定义
org.jbox2d.testbed:主要是一些用来测试的程序
添加JBox2d到Ophone项目中
要在工程中使用JBox2d库,需要将JBox2d添加到工程中,添加方法如下:
右键单击工程,选择"Properties",进入项目Properties界面。
选择"Java Build Path",选择"Libraries"选项卡。
在点击"Add Jars..."按钮,添加Jar。
选择当前工程中我们之前放入lib文件夹中的jbox2d-2.0.1-full.jar文件,如图12-5所示,单击"确定"按钮即可。
图12-5 添加jbox2d-2.0.1-full.jar
OphoneBox2d框架
现在工程的结构展开应该如图12-6所示。
图12-6 OphoneBox2d项目结构
其中实现该工程的文件如下:
Box2dTest:工程Activity,入口
GameGLSurfaceView:游戏GLSurfaceView
GLRenderer:Opengl es渲染器
DrawObject:使用Opengl ES来绘制常用图形(矩形,圆形)
PhysicsWorld:物理世界场景
OphoneBox2d实现
开始分析代码之前,我们先确定一下需要准备的资源图片,从图12-2所示,我们可以看出,多少需要一个矩形和一个圆形的图片(当然也可直接指定颜色绘制矩形和圆形),这里我们将使用图片来进行纹理映射,该工程所需要的纹理图片如图12-7所示。
图12-7 资源图片
2010-11-17 9:42:53
2010-12-4 11:03:11
(会员)
2010-12-4 14:29:11
(会员)
2010-12-4 14:29:39
(会员)
2010-12-4 14:32:39
内容:
验证码: 请点击后输入验证码,加入会员后本吧发贴免验证码。 !尚未登录请先
最近活跃话题
Copyright 2008 北京汇众益智科技有限公司. All Rights Reserved
京ICP备09092043号> 浏览贴子 共
篇贴子
浏览
次 回复
Ophone平台2D游戏引擎实现物理引擎二(1)
(吧主)
渲染器(GLRenderer)
上一篇我们完成了GameGLSurfaceView,同时也将其渲染器设置为了GLRenderer,它才是我们所有Opengl ES程序的核心,GLRenderer将继承自GLSurfaceView.Renderer,需要实现以下三个接口:
onSurfaceCreated():该方法在渲染开始前调用,OpenGL ES的绘制上下文被重建时也会被调用。当activity暂停时绘制上下文会丢失,当activity继续时,绘制上下文会被重建。另外,创建长期存在的OpenGL资源(如texture)往往也在这里进行。
onSurfaceChanged():当surface的尺寸发生改变时该方法被调用。我们可以在这里设置视口。若你的 camera 是固定的,也可以在这里设置 camera。
onDrawFrame():每帧都通过该方法进行绘制。绘制时通常先调用glClear函数来清空 framebuffer,然后在调用OpenGL ES的起它的接口进行绘制。
具体实现如代码清单13-1所示,省略部分代码,
代码清单13-1:GLRenderer.java片段
public cla GLRenderer implements GLSurfaceView.Renderer { //窗口的宽度和高度 private int width; private int height;
//当前选择激活的模型
private int activeModel = 1
;
//是否处于编辑状态
private boolean editMode = false
; // 选择激活的模型 public void witchModel() { activeModel++; if (activeModel gt 2
) { activeModel = 0
; } } // 用于切换编辑模式与游戏模式 public void toggleEdit() { if (editMode == false
) { editMode = true
; } else { editMode = false
; } }
public void onDrawFrame(GL10 gl){
//绘图,渲染 } public void onSurfaceChanged(GL10 gl, int w, int h){ // 设置视口 gl.glViewport(
, w, h); } public void onSurfaceCreated(GL10 gl, EGLConfig arg1) { // 设置为正交视口 GLU.gluOrtho2D(gl, -12f, 12f, -20f, 20f); //允许顶点数组和纹理坐标数组 gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); //设置纹理映射方式 gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_REPEAT); gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT); } //设置尺寸 public void etSize(
int x, int y) { this
.width = x; this
.height = y; }
//...... } 在OpenGL初始化完成之后,我们应该进行一些视图设置。首先是设定视见区域,即告诉OpenGL应把渲染之后的图形绘制在窗体的哪个部位。当视见区域是整个窗体时,OpenGL将把渲染结果绘制到整个窗口。我们可以调用glViewPort函数来决定视见区域;在onSurfaceChanged函数中我们通过glViewport设置了Opengl ES的视口,其中参数X,Y指定了视见区域的左下角在窗口中的位置,一般情况下为(0,0),Width和Height指定了视见区域的宽度和高度。注意OpenGL使用的窗口坐标和Android使用的窗口坐标是不一样的。Opengl使用的窗口坐标的远点位于屏幕左下角。
在onSurfaceCreated函数中,首先通过GLU.gluOrtho2D函数设置二维坐标系统参数,函数有4个参数,可以理解为用该函数设置后,这个二维坐标系的左上角的坐标为(left,top),右下角的坐标为(right,bottom)。如果保持画图的参数不变,将左上角和右下角表示的范围扩大,则图像看起来就缩小了,反之就放大了,这些坐标同样以左下角作为坐标原点。
然后,由于我们绘制这些2D的物体时,需要使用顶点数组和纹理坐标数组,所以通过glEnableClientState函数和参数GL_VERTEX_ARRAY和GL_TEXTURE_COORD_ARRAY分别打开了允许设置顶点数组和纹理坐标数组,稍后再具体绘制时,大家会看到我们如何设置顶点数组和纹理数组的。一般情况我们将图象从纹理图象空间映射到帧缓冲图象空间(映射需要重新构造纹理图像,这样就会造成应用到多边形上的图像失真),这时我们就可用glTexParmeterx()函数来确定如何把纹理象素映射成像素,即纹理映射的方式,通常还有glTexParmeteri函数等,因为opengl提供了几套不同数据类型的函数来完成通一个功能,其中第一个参数GL_TEXTURE_2D表示我们将操作的是2D纹理,因为Opengl支持一维纹理、二维纹理,但是Opengl ES只支持二维纹理,所以第一个参数不会怎么变化,后面的参数通常是需要进行组合的,主要有以下几个参数可以选择设置:
GL_TEXTURE_WRAP_S: S方向上的贴图模式
GL_CLAMP: 将纹理坐标限制在0.0,1.0的范围之内。边缘将会拉伸填充。
GL_TEXTURE_MAG_FILTER: 放大过滤
GL_LINEAR: 线性过滤, 使用距离当前渲染像素中心最近的4个纹素加权平均值
GL_TEXTURE_MIN_FILTER: 缩小过滤
GL_LINEAR_MIPMAP_NEAREST: 使用GL_NEAREST对最接近当前多边形的解析度的两个层级贴图进行采样,然后用这两个值进行线性插值
最后,代码中定义了activeModel和editMode分别表示当前所选择激活的模型,和是否处于编辑状态,然后分别通过函数switchModel和toggleEdit来控制操作者两个状态,逻辑非常简单,大家看看代码就明白了,我们就不多浪费时间了。其中的变量width和height主要是表示窗口的宽度和高度,大家可能已经注意到在GameGLSurfaceView中的onTouchEvent函数中,我们每次都调用GLRenderer的setSize函数来设置了窗口的宽度和高度,在GLRenderer中同样是用于触摸事件的处理用,主要是将触摸坐标转换为场景中的坐标,稍后我们会介绍如何转换。到这里我们就实现了GLRenderer的一部分,下面我们开始学习如何在这里来使用JBox2d了,后面还会介绍在GLRenderer中整个该物理引擎的使用。
2010-11-17 10:00:06
2010-12-4 11:05:26
(会员)
2010-12-4 14:26:37
(会员)
2010-12-4 14:27:04
内容:
验证码: 请点击后输入验证码,加入会员后本吧发贴免验证码。 !尚未登录请先
最近活跃话题
Copyright 2008 北京汇众益智科技有限公司. All Rights Reserved
京ICP备09092043号