【3D技术宅公社】XR数字艺术论坛  XR技术讨论 XR互动电影 定格动画

 找回密码
 立即注册

QQ登录

只需一步,快速开始

调查问卷
论坛即将给大家带来全新的技术服务,面向三围图形学、游戏、动画的全新服务论坛升级为UTF8版本后,中文用户名和用户密码中有中文的都无法登陆,请发邮件到324007255(at)QQ.com联系手动修改密码

3D技术论坛将以计算机图形学为核心,面向教育 推出国内的三维教育引擎该项目在持续研发当中,感谢大家的关注。

查看: 5814|回复: 2

[C/C++] 【转】Kinect V2 开发专题

[复制链接]
发表于 2016-1-4 00:59:03 | 显示全部楼层 |阅读模式
Kinect V2 开发专题(1)

1、软/硬件需求

2、安装测试

1、软/硬件需求

Kinect V1版本只需要Win7+USB2.0+Win7以上系统即可使用,但是到了V2版本之后,需求增高了,当然Kinect的性能也更高了!从软硬件角度讲,有几个硬性的条件:

l CPU必须支持64位,Intel和AMD的无要求。

l USB必须是3.0的,并且芯片厂商是Intel或者Renesas。注意,驱动必须安装正确,在USB设备里面能看到Intel USB 3.0一类的字样,否则肯定会出问题。(Renesas的基本买不到。)一般不能用都是这里的原因,一定要好好检查。

l 必须有显卡,支持DX11。核显和独显无要求,官方列表见后文。

l 操作系统,64位的,Win8版本以上,建议使用Win8.1。

l 内存越大越好,官方建议4G以上。小一点其实用用的话,也能用。

l VS版本2012以上版本,Net版本4.5。

以上均是最低需求,有任何不满足的请自行寻找解决方案。提示:USB3.0可以通过PCIE扩展口安装,一定要注意芯片厂商,侥幸心理是肯定不好用的。

官方指明显卡(更高版本的肯定可以):

◦Intel HD 4400 integrated display adapter

◦ATI Radeon HD 5400 series

◦ATI Radeon HD 6570

◦ATI Radeon HD 7800 (256-bit GDDR5 2GB/1000Mhz)

◦NVidia Quadro 600

◦NVidia GeForce GT 640

◦NVidia GeForce GTX 660

◦NVidia Quadro K1000M

如果需要语音识别,还需要安装微软的语音识别库,需要D3D支持,还需要下载Direct X SDK等,更多内容请自行摸索。

2、安装测试

硬件的检测可以下载硬件的专业检测设备,或者能力高的直接查看设备管理器,推荐使用AIDA64检测,下载地址自行寻找。

软件部分,在安装完成USB3.0驱动或者Kinect SDK开发包之后一定要重启电脑,否则肯定会出现奇奇怪怪的问题。Kinect SDK v2 版本下载地址:http://www.microsoft.com/en-us/download/details.aspx?id=44561 只有英文版本的,这也不是什么大问题。

安装完成之后,系统会多出三个快捷方式:

1、Kinect Studio v2.0 可以查看2D,3D的预览信息,将来肯定很强大,但是现在用处不大。

2、SDK Browser v2.0 所有SDK支持功能的各个版本的Demo,包含可运行程序。语音、骨骼、人脸、3D建模等强大功能都有,有问题看这里,总没错!

我们的测试就基于这个程序进行,分别把有 Run 按钮的程序都跑一遍。Kinect的好坏优劣一目了然。非常赞啊!我就不进行测试效果展示了,大家自己玩玩吧。

3、Visual Gesture Builder - PREVIEW 手势识别的预览版本。我也不太会用。

第一部分完。

第二部分讲解VS项目的配置和编译问题,然后从官方文档出发,讲所有支持的功能和接口理一遍。

BBDXF

2015年3月12日11:45:50


 楼主| 发表于 2016-1-4 00:59:57 | 显示全部楼层
Kinect V2 开发专题(2)

1、项目配置

2、Kinect API概况

1、项目配置

Kinect V2 版本SDK安装之后,默认会在环境变量中添加了一个叫做 KINECTSDK20_DIR 的环境变量,我们添加头文件和库时可以方便地直接使用。

在VS项目属性中,需要添加:

l C++目录 -> 头文件:$(KINECTSDK20_DIR)\inc;

l C++目录 -> 库文件:$(KINECTSDK20_DIR)\lib\x86; (64位的为x64)

l 连接器 -> 输入:Kinect20.lib

至此,在项目中引入头文件 #include “kinect.h”,就可以随意使用啦!

2、Kinect API 概况

近几年,微软对C++的发展真的是没做多大贡献,C++的native编程一再地收到冷漠对待,从新技术的参考文档就可以看出来。同样,官方给出的参考文档基本都是对应托管C++和C#,对于非托管的,虽然有接口表,但是说了和没说差不多了,不过好歹还是有的。

对于C#和托管C++请参考官方文档。这里由于自己需要使用非托管C++进行编程,所以,重点讨论这些内容。

首先,接口表和函数文档:https://msdn.microsoft.com/en-us/library/hh855364.aspx 。特别是接口表,很长一大串,似乎还没有一代的好用。但是,它确实比一代好用多了。我们这里做简要说明。

以下内容来源:http://blog.csdn.net/jiangfan2014/article/details/40760543

在Kinect 2.0中,每个类型的数据都有三个类与之对应:Source,Reader和Frame。比如如果要读取骨架,就有IBodyFrameSource, IBodyFrameReader, IBodyFrame这三个类,而要读取深度数据,就有IDepthFrameSource, IDepthFrameReader, IDepthFrame这三个类,以此类推其他的如Body Index,Infrared,Color数据也是这样命名的。

然后,这三个接口是什么关系呢?

1 Source

在我们初始化并打开了Kinect后,我们需要请求Kinect打开一个源,我们将从这个源不断获得信息。其代码为:

m_pKinectSensor->get_BodyFrameSource(&pBodyFrameSource);  

其中m_pKinectSensor是我们的Kinect总端口,pBodyFrameSource是一个IBodyFrameSource类。

2 Reader

由于Source是Kinect端拥有的,不是我们电脑拥有的,所以我们需要创建一个读口,这个读口和上述的源绑定,之后我们读取信息都通过调用这个Reader来获得。其代码为:

pBodyFrameSource->OpenReader(&m_pBodyFrameReader);  

其中m_pBodyFrameReader是一个IBodyFrameReader类。

3 Frame

Frame是真正存储数据的类,每一次都让Reader把数据读到Frame中,然后我们再从Frame中提取各种各样最后使用的数据。代码为:

m_pBodyFrameReader->AcquireLatestFrame(&pBodyFrame);  

其中pBodyFrame是一个IBodyFrame类。

4 如何从Frame中获得数据

请求Source和创建Reader对于每一个数据类型都是一模一样的,但是从Frame中提取信息则各有不同。下面讲讲深度信息、骨架信息、手势状态和人物二值图信息的提取方法。

4.1 深度信息:

在Kinect 2.0中,深度坐标空间的范围是(高*宽 = 424*512)(官网有说明)。从深度信息Frame中提取数据,主要就是把Frame中的数据转存到一个数组中(官网链接)。代码为:

pBodyIndexFrame->CopyFrameDataToArray(cDepthHeight*cDepthWidth, odyIndexArray);  

这里cDepthHeight是424,cDepthWidth是512,bodyIndexArray就是一个424*512大小的16位unsigned int数组,用来存储深度数据。

4.2 骨架信息:

kinect 2.0可以同时追踪六个人的骨架,因此每次我们需要先调用函数,获得六个骨架信息(如果没有人,那么那个骨架类就是空指针)。代码为:

pBodyFrame->GetAndRefreshBodyData(_countof(ppBodies), ppBodies);  

这里ppBodies是一个长度为6的IBody数组,IBody是用来存储追踪到的骨架信息的类。

在获得了这个类后,我们需要进一步从类中提取骨架位置,对于ppBodies中的每一个元素pBody,代码为:

pBody->GetJoints(_countof(joints), joints);  

这里的joints是一个长度为25的数组,每一个元素就是骨架的位置信息。然而, 这个骨架位置信息是照相机坐标系(camera view)下的位置,x和y的范围都是-1到1。因此我们需要将它转化到深度坐标系中。这里要用到一个coordinateMapper类,具体代码为:

m_pCoordinateMapper->MapCameraPointToDepthSpace(joints[j].Position, &depthSpacePosition[j]);  

coordinateMapper类的创建非常简单,具体可以参考代码。depthSpacePosition是一个长度也为25的数组,每一个元素是DepthSpacePoint,这个元素包含了在深度坐标系下的x和y坐标。

最后,是否发现,我们通过这些对应关系很容易就能拿到我们需要的数据!哈哈,即便没发现也没关系,我们下一节进行更具体的开发。


 楼主| 发表于 2016-1-4 01:02:59 | 显示全部楼层
Kinect V2 开发专题(3)

1、Kinect设备信息获取

2、音频功能探索

1、Kinect设备信息获取

由于Kinect支持多台设备同连,所以,理论上,我们应该可以获取所有的设备信息,这里我们探索一下。

通过官方的AudioBasics-D2D这个Demo发现,获取Kinect设备只需要一步:

IKinectSensor* m_pKinectSensor;  GetDefaultKinectSensor(&m_pKinectSensor);

我们在接口列表中(https://msdn.microsoft.com/en-us/library/dn791996.aspx)查找,发现一个叫做 IKinectSensorCollection 的类,很显眼的是关于Sensor收集的类,我们的脚步就从这里开始。

新建一个工程,按照第二节的内容,设置属性,加入头文件。好啦,我们开始:

额,不好意思!为什么呢?因为坑x的微软在去年八月份的时候把这个功能给Cut了,所以...你是不可能用的,将来可能还会加上。我们了解下就好了,下面我找了一份旧代码大家看看就可以了。

[mw_shl_code=cpp,true]    IKinectSensorCollection* pKinectCollection = nullptr;
    IEnumKinectSensor* pEnumKinect = nullptr;
    IKinectSensor* pKinect = nullptr;
    // 获取Kinect集合
    HRESULT hr = ::GetKinectSensorCollection(&pKinectCollection);
    // 获取Kinect枚举器
    if (SUCCEEDED(hr)){
        hr = pKinectCollection->get_Enumerator(&pEnumKinect);
    }
    // 枚举Kinect
    if (SUCCEEDED(hr)){
        BOOLEAN available = false;
        while (true){
            // 获取下一个
            if (SUCCEEDED(pEnumKinect->GetNext(&pKinect))){
                // 判断有效性
                pKinect->get_IsAvailable(&available);
                if (available && YourJudgmentFunc(pKinect)){
                    break;
                }
                SafeRelease(pKinect);
            }
            else
                break;
        }
    }
    SafeRelease(pEnumKinect);
    SafeRelease(pKinectCollection);[/mw_shl_code]

不要在意这些细节,这都是小事。我们可以通过 GetDefaultKinectSensor(_COM_Outptr_ IKinectSensor** defaultKinectSensor); 这个函数获取默认的Kinect设备,哈哈哈!

下面是一段测试的小程序:

[mw_shl_code=cpp,true]#include "stdafx.h"
#include "kinect.h"

int _tmain(int argc, _TCHAR* argv[])
{
        printf("Hello, Wellcome to kinect world!\n");
        //IKinectSensorCollection aa;
        IKinectSensor* bb;
        HRESULT hr = GetDefaultKinectSensor(&bb);
        if ( FAILED(hr) )
        {
                printf("No Kinect connect to your pc!\n");
                goto endstop;
        }
        BOOLEAN bAvaliable = 0;
        bb->get_IsAvailable(&bAvaliable);
        printf("bAvaliable: %d\n", bAvaliable);
        BOOLEAN bIsOpen = 0;
        bb->get_IsOpen(&bIsOpen);
        printf("bIsOpen: %d\n", bIsOpen);
        DWORD dwCapability = 0;
        bb->get_KinectCapabilities(&dwCapability);
        printf("dwCapability: %d\n", dwCapability);
        TCHAR bbuid[256] = { 0 };
        bb->get_UniqueKinectId(256, bbuid);
        printf("UID: %s\n",bbuid);


endstop:
        system("pause");
        return 0;
}[/mw_shl_code]

在没有插上Kinect时的结果:

这里很容易发现一个奇怪的问题,获取默认Kinect设备成功,但是这个设备是不可用的,什么信息都获取不到,看来 GetDefaultKinectSensor 很不靠谱啊,需要通过 isAvaliable 判断一下才能使用。

下面,我们插上Kinect设备继续测试:

晕啊,傻眼了,为什么会这样子呢?我调试啊调试,然后拿着SDK中的Demo调试,甚至加上了bb->Open()调试,还是那个样子。经多方折腾,最终发现了Kinect不得不说的秘密啊!

Kinect默认是关闭状态,你必须Open之后才能判断是否有效等等内容。但是呢,Kinect的Open是需要时间的,在我的电脑上需要3S左右,否则即便isOpen为1,但是avaliable可能还是0。

再继续研究Kinect的头文件发现,它使用了rpc技术,这样导致函数的状态的回复不可能像本地机器码跑得那么快,所以你需要一定的时间等待Kinect更新自身的状态!

新的代码如下:

[mw_shl_code=cpp,true]#include "stdafx.h"
#include "kinect.h"

int _tmain(int argc, _TCHAR* argv[])
{
        printf("Hello, Wellcome to kinect world!\n");
        IKinectSensor* bb; //申请一个Sensor指针
        HRESULT hr = GetDefaultKinectSensor(&bb); // 获取一个默认的Sensor
        if ( FAILED(hr) )
        {
                printf("No Kinect connect to your pc!\n");
                goto endstop;
        }
        BOOLEAN bIsOpen = 0;
        bb->get_IsOpen(&bIsOpen); // 查看下是否已经打开
        printf("bIsOpen: %d\n", bIsOpen);

        if ( !bIsOpen ) // 没打开,则尝试打开
        {
                hr = bb->Open();
                if ( FAILED(hr) )
                {
                        printf("Kinect Open Failed!\n");
                        goto endstop;
                }
                printf("Kinect opened! But it need sometime to work!\n");
                // 这里一定要多等会,否则下面的判断都是错误的
                printf("Wait For 3000 ms...\n");
                Sleep(3000);
        }
        bIsOpen = 0;
        bb->get_IsOpen(&bIsOpen); // 是否已经打开
        printf("bIsOpen: %d\n", bIsOpen);
        BOOLEAN bAvaliable = 0;
        bb->get_IsAvailable(&bAvaliable); // 是否可用
        printf("bAvaliable: %d\n", bAvaliable);

        DWORD dwCapability = 0;
        bb->get_KinectCapabilities(&dwCapability); // 获取容量
        printf("dwCapability: %d\n", dwCapability);
        TCHAR bbuid[256] = { 0 };
        bb->get_UniqueKinectId(256, bbuid); // 获取唯一ID
        printf("UID: %s\n",bbuid);

        bb->Close();
endstop:
        system("pause");
        return 0;
}[/mw_shl_code]

接上Kinect结果如下:

哈哈,圆满完成任务。只不过,实际情况应该轮询检查Kinect的状态,不应该使用Sleep()草草替代了。

2、Kinect音频功能探索

由于Kinect音频是一个麦克风矩阵,所以可以进行声音方向的探测,这个功能比较喜人。当然,你也可以使用它结合微软的语音识别引擎,进行语音识别和控制,这个是个比较大的专题,这里不做讨论。

我们先来进行声源的判断。接着上面的内容,通过GetDefaultKinectSensor 获取默认Kinect设备,然后Open,接着就可以通过它的成员函数 get_AudioSource 获取音频源,然后根据音频源的成员函数 get_AudioBeams 获取波束列表,然后 OpenAudioBeam 获取第一个波束,为什么是第一个呢?因为现在只支持第一个。最后波束beam的成员函数get_BeamAngle 获取角度,get_BeamAngleConfidence 获取对应的可信度。

[mw_shl_code=cpp,true]#include "stdafx.h"
#include "kinect.h"

int _tmain(int argc, _TCHAR* argv[])
{
        printf("Hello, Wellcome to kinect world!\n");
        IKinectSensor* bb; //申请一个Sensor指针
        HRESULT hr = GetDefaultKinectSensor(&bb); // 获取一个默认的Sensor
        if ( FAILED(hr) )
        {
                printf("No Kinect connect to your pc!\n");
                goto endstop;
        }
        BOOLEAN bIsOpen = 0;
        bb->get_IsOpen(&bIsOpen); // 查看下是否已经打开
        printf("bIsOpen: %d\n", bIsOpen);

        if ( !bIsOpen ) // 没打开,则尝试打开
        {
                hr = bb->Open();
                if ( FAILED(hr) )
                {
                        printf("Kinect Open Failed!\n");
                        goto endstop;
                }
                printf("Kinect opened! But it need sometime to work!\n");
                // 这里一定要多等会,否则下面的判断都是错误的
                printf("Wait For 3000 ms...\n");
                Sleep(3000);
        }
        bIsOpen = 0;
        bb->get_IsOpen(&bIsOpen); // 是否已经打开
        printf("bIsOpen: %d\n", bIsOpen);
        BOOLEAN bAvaliable = 0;
        bb->get_IsAvailable(&bAvaliable); // 是否可用
        printf("bAvaliable: %d\n", bAvaliable);

        DWORD dwCapability = 0;
        bb->get_KinectCapabilities(&dwCapability); // 获取容量
        printf("dwCapability: %d\n", dwCapability);
        TCHAR bbuid[256] = { 0 };
        bb->get_UniqueKinectId(256, bbuid); // 获取唯一ID
        printf("UID: %s\n",bbuid);

        // 音频数据获取
        IAudioSource* audios = nullptr;
        UINT nAudioCount = 0;
        hr = bb->get_AudioSource(&audios);
        if ( FAILED(hr) )
        {
                printf("Audio Source get failed!\n");
                goto endclose;
        }
        IAudioBeam* audiobm = nullptr;
        IAudioBeamList* audiobml = nullptr;
        audios->get_AudioBeams(&audiobml);
        audiobml->OpenAudioBeam(0, &audiobm); // 目前只支持第一个

        float fAngle = 0.0f;
        float fAngleConfidence = 0.0f;
        while (true)
        {
                fAngle = 0.0f;
                fAngleConfidence = 0.0f;
                audiobm->get_BeamAngle(&fAngle); // 获取音频的角度,[ -0.872665f, 0.8726665f ]
                audiobm->get_BeamAngleConfidence(&fAngleConfidence); // 获取音频的可信度(0 - 1)
                printf("Angle: %3.2f (%1.2f)\n", (fAngle/3.1415926f)*180.0f, fAngleConfidence);
                Sleep(200);
        }

endclose:
        bb->Close();
endstop:
        system("pause");
        return 0;
}
[/mw_shl_code]

下面进行音频数据的获取。

我们可以用一般获取录音一样获取音频流,请注意,从这里获取的音频流是原始数据:麦克风列阵获取的多声道音频,并且没有利用麦克风列阵进行降噪处理。代码可以查看SDK自带的获取原始数据的例子,因为与通用设备打交道,很麻烦,这里不做说明。

这里说的是利用自带的方法,获取经处理的音频数据。经过处理的数据信息如下:

l 编码:    32位标准浮点(IEEE FLOAT)

l 声道:    1

l 采样率: 16000Hz

SDK中获取处理后的音频流有两种方法,一种是音频帧,和之前的各种帧差不多:

[mw_shl_code=cpp,true]// 获取音频源(AudioSource)  
    if (SUCCEEDED(hr)){  
        hr = m_pKinect->get_AudioSource(&pAudioSource);  
    }  
    // 再获取音频帧读取器  
    if (SUCCEEDED(hr)){  
        hr = pAudioSource->OpenReader(&m_pAudioBeamFrameReader);  
    }
    // 注册临帧事件  
    if (SUCCEEDED(hr)){  
        m_pAudioBeamFrameReader->SubscribeFrameArrived(&m_hAudioBeamFrameArrived);  
    }  
[/mw_shl_code]

这样初始化。使用后,像之前那样,根据事件获取 AudioBeamFrameArrivedEventArgs, 再获取 AudioBeamFrameReference 音频帧引用,再获取 AudioBeamFrameList 音频帧链表,目前链表只有一个元素,直接获取 AudioBeamFrame音频帧。音频帧可能包含复数 AudioBeamSubFrame 音频副帧(比如本人这里包含2个),这个东西才能获取音频流的真正信息。

还有就是IStream,前面的这不是指C++标准库的输入流,而是COM组件的“流接口”,可读可写。初始化代码如下:

[mw_shl_code=cpp,true] if (SUCCEEDED(hr))
    {
        hr = m_pKinectSensor->get_AudioSource(&pAudioSource);
    }
    if (SUCCEEDED(hr))
    {
        hr = pAudioSource->get_AudioBeams(&pAudioBeamList);
    }
    if (SUCCEEDED(hr))
    {
        hr = pAudioBeamList->OpenAudioBeam(0, &m_pAudioBeam);
    }
    if (SUCCEEDED(hr))
    {        
        hr = m_pAudioBeam->OpenInputStream(&m_pAudioStream);
    }
[/mw_shl_code]

m_pAudioSteam就是Steam对象,使用时ISteam::Read(void*, ULONG, ULONG*)主动获取音频数据。相比而言,使用音频帧既可以主动获取,又能使用事件机制,而Stream只能主动获取。

我们这里使用第二种流的方式,编写一个Demo,代码如下:

[mw_shl_code=cpp,true]#include "stdafx.h"
#include "kinect.h"
#define _USE_MATH_DEFINES
#include <math.h>

int _tmain(int argc, _TCHAR* argv[])
{
        printf("Hello, Wellcome to kinect world!\n");
        IKinectSensor* bb; //申请一个Sensor指针
        HRESULT hr = GetDefaultKinectSensor(&bb); // 获取一个默认的Sensor
        if ( FAILED(hr) )
        {
                printf("No Kinect connect to your pc!\n");
                goto endstop;
        }
        BOOLEAN bIsOpen = 0;
        bb->get_IsOpen(&bIsOpen); // 查看下是否已经打开
        printf("bIsOpen: %d\n", bIsOpen);

        if ( !bIsOpen ) // 没打开,则尝试打开
        {
                hr = bb->Open();
                if ( FAILED(hr) )
                {
                        printf("Kinect Open Failed!\n");
                        goto endstop;
                }
                printf("Kinect opened! But it need sometime to work!\n");
                // 这里一定要多等会,否则下面的判断都是错误的
                printf("Wait For 3000 ms...\n");
                Sleep(3000);
        }
        bIsOpen = 0;
        bb->get_IsOpen(&bIsOpen); // 是否已经打开
        printf("bIsOpen: %d\n", bIsOpen);
        BOOLEAN bAvaliable = 0;
        bb->get_IsAvailable(&bAvaliable); // 是否可用
        printf("bAvaliable: %d\n", bAvaliable);

        DWORD dwCapability = 0;
        bb->get_KinectCapabilities(&dwCapability); // 获取容量
        printf("dwCapability: %d\n", dwCapability);
        TCHAR bbuid[256] = { 0 };
        bb->get_UniqueKinectId(256, bbuid); // 获取唯一ID
        printf("UID: %s\n",bbuid);

        // 音频数据获取
        IAudioSource* audios = nullptr;
        UINT nAudioCount = 0;
        hr = bb->get_AudioSource(&audios);
        if ( FAILED(hr) )
        {
                printf("Audio Source get failed!\n");
                goto endclose;
        }
        IAudioBeam* audiobm = nullptr;
        IAudioBeamList* audiobml = nullptr;
        audios->get_AudioBeams(&audiobml);
        audiobml->OpenAudioBeam(0, &audiobm); // 目前只支持第一个
        IStream* stm = nullptr;
        audiobm->OpenInputStream(&stm);
        audios->Release();
        audiobm->Release();

        float fAngle = 0.0f;
        float fAngleConfidence = 0.0f;
        ULONG lRead = 0;
        const ULONG lBufferSize =3200;
        float* fDataArr = new float[lBufferSize];
        while (true)
        {
                fAngle = 0.0f;
                fAngleConfidence = 0.0f;
                audiobm->get_BeamAngle(&fAngle); // 获取音频的角度,[ -0.872665f, 0.8726665f ]
                audiobm->get_BeamAngleConfidence(&fAngleConfidence); // 获取音频的可信度(0 - 1)
                if ( fAngleConfidence > 0.5f )
                         printf("Angle: %3.2f (%1.2f)\n", (fAngle)*180.0f/static_cast<float>(M_PI), fAngleConfidence);
                // audio  data
                lRead = 0;
                memset(fDataArr, 0, lBufferSize);
                stm->Read(fDataArr, lBufferSize, &lRead);
                if ( lRead > 0 )
                {
                        printf("Audio Buffer: %d\n", lRead);
                }
                Sleep(200);
        }

endclose:
        bb->Close();
endstop:
        system("pause");
        return 0;
}
[/mw_shl_code]


您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|3D数字艺术论坛 ( 沪ICP备14023054号 )

GMT+8, 2024-11-23 15:26

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表