Cocos2d-x 3.3 的3D开发功能介绍

昨天去成都参加GMGDC 全球移动游戏开发者大会,据蓝港互动CEO王峰谈到《手机游戏开发如何走好第一步》时谈到

目前手游公司有10000家,没错,红海,都快变黑海了

这么多公司和产品如何竞争,大量的同质化产品,(王峰给我的建议是与其在红海中厮杀不如想象未来的蓝海在哪,听明白的请举起右手)

先不管这些了,作为程序员来讲我们看到了20万的人才缺口(如果每家公司招20个人,听明白的请举起左手)

今天下载了Cocos2d-x 3.3,3D功能果然强大

主要有以下功能:

1. 基本的Sprite3D使用,加载静态模型和动态模型,看 Sprite3DBasicTest

2.Sprite3D对象的旋转,缩放等Action操作

3.Sprite3D中使用Shader特效,实现outLine

4.Animate3D来创建3D动画

5.动态增加3D骨骼,实现怪物添加手持武器功能

6,动态修改骨骼皮肤实现换装功能Sprite3DReskinTest

7.通过包围盒实现3D模型碰撞,Sprite3DWithOBBPerfromanceTest

8.实现水平镜像3D模型,Sprite3DMirrorTest

下面介绍一下Sprite3DTest里面的源码

#include "Sprite3DTest.h"

#include "3d/CCAnimation3D.h"

#include "3d/CCAnimate3D.h"

#include "3d/CCAttachNode.h"

#include "3d/CCRay.h"

#include "3d/CCSprite3D.h"

#include "renderer/CCVertexIndexBuffer.h"

#include "DrawNode3D.h"

1.在Scene中添加3D模型

void Sprite3DBasicTest::addNewSpriteWithCoords(Vec2 p)

{

//这里的obj可以使用3dmax直接导出

//    //option 1: load a obj that contain the texture in it  第一种方法是在模型文件中包含了纹理

//    auto sprite = Sprite3D::create("sprite3dTest/scene01.obj");

//option 2: load obj and assign the texture   第二种方法是在模型文件中不包含纹理

auto sprite =
Sprite3D::create("Sprite3DTest/boss1.obj");

sprite->setScale(3.f);

sprite->setTexture("Sprite3DTest/boss.png");

//在Sprite3D中包含了一些基本的Shader特效,下面是让3D模型实现outline

//sprite->setEffect(cocos2d::EFFECT_OUTLINE);

//add to scene

addChild( sprite );

sprite->setPosition(
Vec2( p.x, p.y) );

ActionInterval* action;

float random = CCRANDOM_0_1();

if( random < 0.20 )

action = ScaleBy::create(3,
2);

else if(random <
0.40)

action = RotateBy::create(3,
360);

else if( random <
0.60)

action = Blink::create(1,
3);

else if( random <
0.8 )

action = TintBy::create(2,
0, -255, -255);

else

action = FadeOut::create(2);

auto action_back = action->reverse();

auto seq = Sequence::create( action, action_back,
nullptr );

sprite->runAction( RepeatForever::create(seq) );

//以上大家看到Sprite3D起始和Sprite类似都可以实现各种Action

}

void Sprite3DBasicTest::onTouchesEnded(const
std::vector<Touch*>& touches,
Event* event)

{

for (auto touch: touches)

{

auto location = touch->getLocation();

//触摸屏幕添加3D模型

addNewSpriteWithCoords( location );

}

}

2.透过触摸屏幕拖动模型移动

Sprite3DHitTest::Sprite3DHitTest()

{

auto s =
Director::getInstance()->getWinSize();

auto sprite1 =
Sprite3D::create("Sprite3DTest/boss1.obj");

sprite1->setScale(4.f);

sprite1->setTexture("Sprite3DTest/boss.png");

sprite1->setPosition(
Vec2(s.width/2, s.height/2) );

//add to scene

addChild( sprite1 );

sprite1->runAction(RepeatForever::create(RotateBy::create(3,
360)));

auto sprite2 =
Sprite3D::create("Sprite3DTest/boss1.obj");

sprite2->setScale(4.f);

sprite2->setTexture("Sprite3DTest/boss.png");

sprite2->setPosition(
Vec2(s.width/2, s.height/2) );

sprite2->setAnchorPoint(Vec2(0.5,
0.5));

//add to scene

addChild( sprite2 );

sprite2->runAction(RepeatForever::create(RotateBy::create(3, -360)));

// Make sprite1 touchable

auto listener1 =
EventListenerTouchOneByOne::create();

listener1->setSwallowTouches(true);

    listener1->onTouchBegan = [](Touch* touch,
Event* event){

        auto target =
static_cast<Sprite3D*>(event->getCurrentTarget());

      

        Rect rect = target->getBoundingBox();        

        if (rect.containsPoint(touch->getLocation()))

        {

            log("sprite3d began... x = %f, y = %f", touch->getLocation().x, touch->getLocation().y);

            target->setOpacity(100);

            return
true;

        }

        return
false;

    };

    

    listener1->onTouchMoved = [](Touch* touch,
Event* event){

        auto target =
static_cast<Sprite3D*>(event->getCurrentTarget());

        target->setPosition(target->getPosition() + touch->getDelta());

    };

listener1->onTouchEnded = [=](Touch* touch,
Event* event){

auto target = static_cast<Sprite3D*>(event->getCurrentTarget());

log("sprite3d onTouchesEnded.. ");

target->setOpacity(255);

};

_eventDispatcher->addEventListenerWithSceneGraphPriority(listener1, sprite1);

_eventDispatcher->addEventListenerWithSceneGraphPriority(listener1->clone(), sprite2);

}

3.在一个Sprite3D上使用Shader

Effect3D继承REF封装了Shader的处理

Effect3DOutline继承了Effect3D,封装了Outline类型的Shader

EffectSprite3D继承了Sprite3D封装了对Effect3DOutline的调用

class Effect3D : public
Ref

{

public:

virtual void draw(const
Mat4 &transform) = 0;

virtual void setTarget(EffectSprite3D *sprite) =
0;

protected:

Effect3D() : _glProgramState(nullptr) {}

virtual ~Effect3D()

{

CC_SAFE_RELEASE(_glProgramState);

}

protected:

GLProgramState* _glProgramState;

};

class Effect3DOutline: public
Effect3D

{

public:

static Effect3DOutline* create();

void setOutlineColor(const
Vec3& color);

void setOutlineWidth(float width);

virtual void draw(const
Mat4 &transform) override;

virtual void setTarget(EffectSprite3D *sprite) override;

protected:

Effect3DOutline();

virtual ~Effect3DOutline();

bool init();

Vec3 _outlineColor;

float _outlineWidth;

//weak reference

EffectSprite3D* _sprite;

#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)

EventListenerCustom* _backToForegroundListener;

#endif

protected:

static const
std::string _vertShaderFile;

static const
std::string _fragShaderFile;

static const
std::string _keyInGLProgramCache;

static const
std::string _vertSkinnedShaderFile;

static const
std::string _fragSkinnedShaderFile;

static const
std::string _keySkinnedInGLProgramCache;

static GLProgram* getOrCreateProgram(bool isSkinned =
false);

};

class EffectSprite3D : public
Sprite3D

{

public:

static EffectSprite3D* createFromObjFileAndTexture(const
std::string& objFilePath,
const std::string& textureFilePath);

static EffectSprite3D* create(const
std::string& path);

void setEffect3D(Effect3D* effect);

void addEffect(Effect3DOutline* effect,
ssize_t order);

virtual void draw(Renderer *renderer,
const Mat4 &transform,
uint32_t flags) override;

protected:

EffectSprite3D();

virtual ~EffectSprite3D();

std::vector<std::tuple<ssize_t,Effect3D*,CustomCommand>> _effects;

Effect3D* _defaultEffect;

CustomCommand _command;

};

class Sprite3DEffectTest :
public Sprite3DTestDemo

{

public:

CREATE_FUNC(Sprite3DEffectTest);

Sprite3DEffectTest();

virtual std::string title()
const override;

virtual std::string subtitle()
const override;

void addNewSpriteWithCoords(Vec2 p);

void onTouchesEnded(const
std::vector<Touch*>& touches,
Event* event);

};

const std::string
Effect3DOutline::_vertShaderFile = "Shaders3D/OutLine.vert";

const std::string
Effect3DOutline::_fragShaderFile = "Shaders3D/OutLine.frag";

const std::string
Effect3DOutline::_keyInGLProgramCache =
"Effect3DLibrary_Outline";

const std::string
Effect3DOutline::_vertSkinnedShaderFile =
"Shaders3D/SkinnedOutline.vert";

const std::string
Effect3DOutline::_fragSkinnedShaderFile =
"Shaders3D/OutLine.frag";

const std::string
Effect3DOutline::_keySkinnedInGLProgramCache =
"Effect3DLibrary_Outline";

GLProgram* Effect3DOutline::getOrCreateProgram(bool isSkinned
/* = false */ )

{

if(isSkinned)

{

auto program = GLProgramCache::getInstance()->getGLProgram(_keySkinnedInGLProgramCache);

if(program == nullptr)

{

program = GLProgram::createWithFilenames(_vertSkinnedShaderFile,
_fragSkinnedShaderFile);

GLProgramCache::getInstance()->addGLProgram(program,
_keySkinnedInGLProgramCache);

}

return program;

}

else

{

auto program = GLProgramCache::getInstance()->getGLProgram(_keyInGLProgramCache);

if(program == nullptr)

{

program = GLProgram::createWithFilenames(_vertShaderFile,
_fragShaderFile);

GLProgramCache::getInstance()->addGLProgram(program,
_keyInGLProgramCache);

}

return program;

}

}

Effect3DOutline* Effect3DOutline::create()

{

Effect3DOutline* effect =
new (std::nothrow)
Effect3DOutline();

if(effect && effect->init())

{

effect->autorelease();

return effect;

}

else

{

CC_SAFE_DELETE(effect);

return nullptr;

}

}

bool Effect3DOutline::init()

{

return true;

}

Effect3DOutline::Effect3DOutline()

: _outlineWidth(1.0f)

, _outlineColor(1,
1, 1)

, _sprite(nullptr)

{

#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)

_backToForegroundListener = EventListenerCustom::create(EVENT_RENDERER_RECREATED,

[this](EventCustom*)

{

auto glProgram = _glProgramState->getGLProgram();

glProgram->reset();

glProgram->initWithFilenames(_vertShaderFile, _fragShaderFile);

glProgram->link();

glProgram->updateUniforms();

}

);

Director::getInstance()->getEventDispatcher()->addEventListenerWithFixedPriority(_backToForegroundListener, -1);

#endif

}

Effect3DOutline::~Effect3DOutline()

{

#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)

Director::getInstance()->getEventDispatcher()->removeEventListener(_backToForegroundListener);

#endif

}

void Effect3DOutline::setOutlineColor(const
Vec3& color)

{

if(_outlineColor != color)

{

_outlineColor = color;

if(_glProgramState)

_glProgramState->setUniformVec3("OutLineColor",
_outlineColor);

}

}

void Effect3DOutline::setOutlineWidth(float width)

{

if(_outlineWidth != width)

{

_outlineWidth = width;

if(_glProgramState)

_glProgramState->setUniformFloat("OutlineWidth",
_outlineWidth);

}

}

void Effect3DOutline::setTarget(EffectSprite3D *sprite)

{

CCASSERT(nullptr != sprite &&
nullptr != sprite->getMesh(),"Error: Setting a null pointer or a null mesh EffectSprite3D to Effect3D");

if(sprite != _sprite)

{

GLProgram* glprogram;

if(!sprite->getMesh()->getSkin())

glprogram = GLProgram::createWithFilenames(_vertShaderFile,
_fragShaderFile);

else

glprogram = GLProgram::createWithFilenames(_vertSkinnedShaderFile,
_fragSkinnedShaderFile);

_glProgramState =
GLProgramState::create(glprogram);

_glProgramState->retain();

_glProgramState->setUniformVec3("OutLineColor",
_outlineColor);

_glProgramState->setUniformFloat("OutlineWidth",
_outlineWidth);

_sprite = sprite;

auto mesh = sprite->getMesh();

long offset = 0;

for (auto i =
0; i < mesh->getMeshVertexAttribCount(); i++)

{

auto meshvertexattrib = mesh->getMeshVertexAttribute(i);

_glProgramState->setVertexAttribPointer(s_attributeNames[meshvertexattrib.vertexAttrib],

meshvertexattrib.size,

meshvertexattrib.type,

GL_FALSE,

mesh->getVertexSizeInBytes(),

(void*)offset);

offset += meshvertexattrib.attribSizeBytes;

}

Color4F color(_sprite->getDisplayedColor());

color.a = _sprite->getDisplayedOpacity() /
255.0f;

_glProgramState->setUniformVec4("u_color",
Vec4(color.r, color.g, color.b, color.a));

}

}

static void MatrixPalleteCallBack(
GLProgram* glProgram, Uniform* uniform,
int paletteSize, const
float* palette)

{

glUniform4fv( uniform->location, (GLsizei)paletteSize, (const
float*)palette );

}

void Effect3DOutline::draw(const
Mat4 &transform)

{

//draw

Color4F color(_sprite->getDisplayedColor());

color.a = _sprite->getDisplayedOpacity() /
255.0f;

_glProgramState->setUniformVec4("u_color",
Vec4(color.r, color.g, color.b, color.a));

if(_sprite &&
_sprite->getMesh())

{

glEnable(GL_CULL_FACE);

glCullFace(GL_FRONT);

glEnable(GL_DEPTH_TEST);

auto mesh = _sprite->getMesh();

glBindBuffer(GL_ARRAY_BUFFER, mesh->getVertexBuffer());

auto skin = _sprite->getMesh()->getSkin();

if(_sprite && skin)

{

auto function =
std::bind(MatrixPalleteCallBack,
std::placeholders::_1,
std::placeholders::_2,

skin->getMatrixPaletteSize(), (float*)skin->getMatrixPalette());

_glProgramState->setUniformCallback("u_matrixPalette", function);

}

if(_sprite)

_glProgramState->apply(transform);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh->getIndexBuffer());

glDrawElements(mesh->getPrimitiveType(), mesh->getIndexCount(), mesh->getIndexFormat(),
0);

CC_INCREMENT_GL_DRAWN_BATCHES_AND_VERTICES(1, mesh->getIndexCount());

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,
0);

glBindBuffer(GL_ARRAY_BUFFER,
0);

glDisable(GL_DEPTH_TEST);

glCullFace(GL_BACK);

glDisable(GL_CULL_FACE);

}

}

void EffectSprite3D::draw(cocos2d::Renderer *renderer,
const cocos2d::Mat4 &transform,
uint32_t flags)

{

for(auto &effect : _effects)

{

if(std::get<0>(effect) >=0)

break;

CustomCommand &cc =
std::get<2>(effect);

cc.func = CC_CALLBACK_0(Effect3D::draw,std::get<1>(effect),transform);

renderer->addCommand(&cc);

}

if(!_defaultEffect)

{

Sprite3D::draw(renderer, transform, flags);

}

else

{

_command.init(_globalZOrder);

_command.func =
CC_CALLBACK_0(Effect3D::draw,
_defaultEffect, transform);

renderer->addCommand(&_command);

}

for(auto &effect : _effects)

{

if(std::get<0>(effect) <=0)

continue;

CustomCommand &cc =
std::get<2>(effect);

cc.func = CC_CALLBACK_0(Effect3D::draw,std::get<1>(effect),transform);

renderer->addCommand(&cc);

}

}

//Sprite3DEffectTest中实现了对Sprite3DEffect的加载

Sprite3DEffectTest::Sprite3DEffectTest()

{

auto s =
Director::getInstance()->getWinSize();

addNewSpriteWithCoords(
Vec2(s.width/2, s.height/2)
);

auto listener =
EventListenerTouchAllAtOnce::create();

listener->onTouchesEnded =
CC_CALLBACK_2(Sprite3DEffectTest::onTouchesEnded,
this);

_eventDispatcher->addEventListenerWithSceneGraphPriority(listener,
this);

}

void Sprite3DEffectTest::addNewSpriteWithCoords(Vec2 p)

{

//option 2: load obj and assign the texture

auto sprite =
EffectSprite3D::createFromObjFileAndTexture("Sprite3DTest/boss1.obj",
"Sprite3DTest/boss.png");

Effect3DOutline* effect =
Effect3DOutline::create();

sprite->addEffect(effect, -1);

effect->setOutlineColor(Vec3(1,0,0));

effect->setOutlineWidth(0.01f);

Effect3DOutline* effect2 =
Effect3DOutline::create();

sprite->addEffect(effect2, -2);

effect2->setOutlineWidth(0.02f);

effect2->setOutlineColor(Vec3(1,1,0));

//sprite->setEffect3D(effect);

sprite->setScale(6.f);

//add to scene

addChild( sprite );

sprite->setPosition(
Vec2( p.x, p.y) );

ActionInterval* action;

float random = CCRANDOM_0_1();

if( random < 0.20 )

action = ScaleBy::create(3,
2);

else if(random <
0.40)

action = RotateBy::create(3,
360);

else if( random <
0.60)

action = Blink::create(1,
3);

else if( random <
0.8 )

action = TintBy::create(2,
0, -255, -255);

else

action = FadeOut::create(2);

auto action_back = action->reverse();

auto seq = Sequence::create( action, action_back,
nullptr );

sprite->runAction( RepeatForever::create(seq) );

}

void Sprite3DEffectTest::onTouchesEnded(const
std::vector<Touch*>& touches,
Event* event)

{

for (auto touch: touches)

{

auto location = touch->getLocation();

addNewSpriteWithCoords( location );

}

}

4.加载包含了3D纹理和动画的C3B文件,具体转化方法为在tools下使用fbx-conv.exe将3dmax导出的文件转化为c3b

用Sprite3D来加载c3b,用Animation3D,和Animate3D来创建动画

   auto animation = Animation3D::create(fileName);

    if (animation)

    {

        auto animate = Animate3D::create(animation);

       sprite->runAction(RepeatForever::create(animate));

}

void Sprite3DWithSkinTest::addNewSpriteWithCoords(Vec2 p)

{

std::string fileName =
"Sprite3DTest/orc.c3b";

    auto sprite =
EffectSprite3D::create(fileName);

    sprite->setScale(3);

    sprite->setRotation3D(Vec3(0,180,0));

    addChild(sprite);

    sprite->setPosition(
Vec2( p.x, p.y) );

    auto animation =
Animation3D::create(fileName);

    if (animation)

    {

        auto animate =
Animate3D::create(animation);

bool inverse = (std::rand() %
3 == 0);

int rand2 = std::rand();

float speed = 1.0f;

if(rand2 % 3 ==
1)

{

speed = animate->getSpeed() +
CCRANDOM_0_1();

}

else if(rand2 %
3 == 2)

{

speed = animate->getSpeed() -
0.5 * CCRANDOM_0_1();

}

animate->setSpeed(inverse ? -speed : speed);

sprite->runAction(RepeatForever::create(animate));

}

}

5.在以上模型和动画中添加特效(方法和在静态模型上一样)

void Sprite3DWithSkinOutlineTest::addNewSpriteWithCoords(Vec2 p)

{

std::string fileName =
"Sprite3DTest/orc.c3b";

auto sprite =
EffectSprite3D::create(fileName);

    

    Effect3DOutline* effect =
Effect3DOutline::create();

    effect->setOutlineColor(Vec3(1,0,0));

    effect->setOutlineWidth(0.01f);

    sprite->addEffect(effect, -1);

    

    Effect3DOutline* effect2 =
Effect3DOutline::create();

    effect2->setOutlineWidth(0.02f);

    effect2->setOutlineColor(Vec3(1,1,0));

    sprite->addEffect(effect2, -2);

sprite->setScale(3);

sprite->setRotation3D(Vec3(0,180,0));

addChild(sprite);

sprite->setPosition(
Vec2( p.x, p.y) );

auto animation = Animation3D::create(fileName);

if (animation)

{

auto animate = Animate3D::create(animation);

bool inverse = (std::rand() %
3 == 0);

int rand2 = std::rand();

float speed = 1.0f;

if(rand2 % 3 ==
1)

{

speed = animate->getSpeed() +
CCRANDOM_0_1();

}

else if(rand2 %
3 == 2)

{

speed = animate->getSpeed() -
0.5 * CCRANDOM_0_1();

}

animate->setSpeed(inverse ? -speed : speed);

sprite->runAction(RepeatForever::create(animate));

}

}

6.动画的切换

既然每个3D动画是Action就可以通过切换每个Sprite3D的Action来切换动画,下面是一个小乌龟的2个动画之间的切换

Animate3DTest::Animate3DTest()

: _hurt(nullptr)

, _swim(nullptr)

, _sprite(nullptr)

, _moveAction(nullptr)

, _elapseTransTime(0.f)

{ //添加小乌龟

    addSprite3D();

auto listener =
EventListenerTouchAllAtOnce::create();

listener->onTouchesEnded =
CC_CALLBACK_2(Animate3DTest::onTouchesEnded,
this);

_eventDispatcher->addEventListenerWithSceneGraphPriority(listener,
this);

scheduleUpdate();

}

Animate3DTest::~Animate3DTest()

{

CC_SAFE_RELEASE(_moveAction);

CC_SAFE_RELEASE(_hurt);

CC_SAFE_RELEASE(_swim);

}

void Animate3DTest::update(float dt)

{

if (_state ==
State::HURT_TO_SWIMMING)

{

_elapseTransTime += dt;

if (_elapseTransTime >=
Animate3D::getTransitionTime())

{

_sprite->stopAction(_hurt);

_state = State::SWIMMING;

}

}

else
if (_state ==
State::SWIMMING_TO_HURT)

{

_elapseTransTime += dt;

if (_elapseTransTime >=
Animate3D::getTransitionTime())

{

_sprite->stopAction(_swim);

_state = State::HURT;

}

}

}

void Animate3DTest::addSprite3D()

{

std::string fileName =
"Sprite3DTest/tortoise.c3b";

auto sprite = Sprite3D::create(fileName);

sprite->setScale(0.1f);

auto s =
Director::getInstance()->getWinSize();

sprite->setPosition(Vec2(s.width *
4.f / 5.f, s.height /
2.f));

addChild(sprite);

_sprite = sprite;

auto animation = Animation3D::create(fileName);

if (animation)

{   //2个动画的时间不同,这些在3Dmax中定义

auto animate =
Animate3D::create(animation, 0.f,
1.933f);

        _swim = RepeatForever::create(animate);

        sprite->runAction(_swim);

        

        _swim->retain();

        _hurt = Animate3D::create(animation,
1.933f, 2.8f);

        _hurt->retain();

_state = State::SWIMMING;

}

_moveAction = MoveTo::create(4.f,
Vec2(s.width /
5.f, s.height / 2.f));

_moveAction->retain();

auto seq = Sequence::create(_moveAction,
CallFunc::create(CC_CALLBACK_0(Animate3DTest::reachEndCallBack,
this)), nullptr);

seq->setTag(100);

sprite->runAction(seq);

}

//当触摸小乌龟则改变动画

void Animate3DTest::reachEndCallBack()

{

    _sprite->stopActionByTag(100);

    auto inverse = (MoveTo*)_moveAction->reverse();

    inverse->retain();

    _moveAction->release();

    _moveAction = inverse;

    auto rot = RotateBy::create(1.f,
Vec3(0.f, 180.f,
0.f));

    auto seq = Sequence::create(rot,
_moveAction, CallFunc::create(CC_CALLBACK_0(Animate3DTest::reachEndCallBack,
this)), nullptr);

    seq->setTag(100);

    _sprite->runAction(seq);

}

void Animate3DTest::renewCallBack()

{

//rerun swim action

_sprite->runAction(_swim);

_state =
State::HURT_TO_SWIMMING;

_elapseTransTime =
0.0f;

}

void Animate3DTest::onTouchesEnded(const
std::vector<Touch*>& touches,
Event* event)

{

for (auto touch: touches)

{

auto location = touch->getLocation();

if (_sprite)

{

float len = (_sprite->getPosition() - location).length();

if (len < 40)

{

//hurt the tortoise 在游动状态改变为被抓住的动画

  if (_state ==
State::SWIMMING)

                {

                    _elapseTransTime =
0.0f;

                    _state =
State::SWIMMING_TO_HURT;

                    _sprite->stopAction(_hurt);

                    _sprite->runAction(_hurt);

                    auto delay =
DelayTime::create(_hurt->getDuration() -
Animate3D::getTransitionTime());

                    auto seq =
Sequence::create(delay, CallFunc::create(CC_CALLBACK_0(Animate3DTest::renewCallBack,
this)), nullptr);

                    seq->setTag(101);

                    _sprite->runAction(seq);

                }

return;

}

}

}

}

7.动态添加武器

AttachmentTest::AttachmentTest()

: _hasWeapon(false)

, _sprite(nullptr)

{

auto s =
Director::getInstance()->getWinSize();

addNewSpriteWithCoords(
Vec2(s.width/2, s.height/2)
);

auto listener =
EventListenerTouchAllAtOnce::create();

listener->onTouchesEnded =
CC_CALLBACK_2(AttachmentTest::onTouchesEnded,
this);

_eventDispatcher->addEventListenerWithSceneGraphPriority(listener,
this);

}

void AttachmentTest::addNewSpriteWithCoords(Vec2 p)

{

std::string fileName =
"Sprite3DTest/orc.c3b";

auto sprite = Sprite3D::create(fileName);

sprite->setScale(5);

sprite->setRotation3D(Vec3(0,180,0));

addChild(sprite);

sprite->setPosition(
Vec2( p.x, p.y) );

//test attach  亮点在这里,获取某个骨骼,Bip001 R Hand是在3Dmax定义的

  auto sp =
Sprite3D::create("Sprite3DTest/axe.c3b");

    sprite->getAttachNode("Bip001 R Hand")->addChild(sp);

auto animation = Animation3D::create(fileName);

if (animation)

{

auto animate = Animate3D::create(animation);

sprite->runAction(RepeatForever::create(animate));

}

_sprite = sprite;

_hasWeapon = true;

}

void AttachmentTest::onTouchesEnded(const
std::vector<Touch*>& touches,
Event* event)

{

  if (_hasWeapon)

    {

       
_sprite->removeAllAttachNode();   //去掉新骨骼节点

    }

    else

    {

        auto sp = Sprite3D::create("Sprite3DTest/axe.c3b");

        _sprite->getAttachNode("Bip001 R Hand")->addChild(sp);

    }

_hasWeapon = !_hasWeapon;

}

8.动态修改材质Mesh(这个demo好,美女的模型超赞)

Sprite3DReskinTest::Sprite3DReskinTest()

: _sprite(nullptr)

{

auto s = Director::getInstance()->getWinSize();

addNewSpriteWithCoords( Vec2(s.width/2, s.height/2) );

auto listener = EventListenerTouchAllAtOnce::create();

listener->onTouchesEnded = CC_CALLBACK_2(Sprite3DReskinTest::onTouchesEnded,
this);

_eventDispatcher->addEventListenerWithSceneGraphPriority(listener,
this);

TTFConfig ttfConfig("fonts/arial.ttf",
20);

auto label1 = Label::createWithTTF(ttfConfig,"Hair");

auto item1 = MenuItemLabel::create(label1,CC_CALLBACK_1(Sprite3DReskinTest::menuCallback_switchHair,this) );

auto label2 = Label::createWithTTF(ttfConfig,"Glasses");

auto item2 = MenuItemLabel::create(label2,
CC_CALLBACK_1(Sprite3DReskinTest::menuCallback_switchGlasses,this) );

auto label3 = Label::createWithTTF(ttfConfig,"Coat");

auto item3 = MenuItemLabel::create(label3,CC_CALLBACK_1(Sprite3DReskinTest::menuCallback_switchCoat,this) );

auto label4 = Label::createWithTTF(ttfConfig,"Pants");

auto item4 = MenuItemLabel::create(label4,
CC_CALLBACK_1(Sprite3DReskinTest::menuCallback_switchPants,this) );

auto label5 = Label::createWithTTF(ttfConfig,"Shoes");

auto item5 = MenuItemLabel::create(label5,CC_CALLBACK_1(Sprite3DReskinTest::menuCallback_switchShoes,this) );

item1->setPosition( Vec2(VisibleRect::left().x+50, VisibleRect::bottom().y+item1->getContentSize().height*4 ) );

item2->setPosition( Vec2(VisibleRect::left().x+50, VisibleRect::bottom().y+item1->getContentSize().height *5 ) );

item3->setPosition( Vec2(VisibleRect::left().x+50, VisibleRect::bottom().y+item1->getContentSize().height*6 ) );

item4->setPosition( Vec2(VisibleRect::left().x+50, VisibleRect::bottom().y+item1->getContentSize().height *7 ) );

item5->setPosition( Vec2(VisibleRect::left().x+50, VisibleRect::bottom().y+item1->getContentSize().height *8 ) );

auto pMenu1 = CCMenu::create(item1,item2,item3,item4,item5,NULL);

pMenu1->setPosition(Vec2(0,0));

this->addChild(pMenu1,
10);

}

void Sprite3DReskinTest::menuCallback_switchHair(Ref* sender)

{

_useHairId++;

if(_useHairId > 1 )

{

_useHairId = 0;

}

if(_useHairId >= 0  && _sprite)

{

for(int i =
0; i < 2; i++ )

{  //                           获取材质 可见3.3支持了多套材质

auto subMesh = _sprite->getMeshByName(_girlHair[i]);

            if(subMesh)

            {

                if(i == _useHairId )

                {

                    subMesh->setVisible(true);

                }

                else

                {

                    subMesh->setVisible(false);

                }

            }

}

}

}

void Sprite3DReskinTest::menuCallback_switchGlasses(Ref* sender)

{

auto subMesh = _sprite->getMeshByName("Girl_Glasses01");

if(subMesh)

{

if(subMesh->isVisible())

{

subMesh->setVisible(false);

}

else

{

subMesh->setVisible(true);

}

}

}

void Sprite3DReskinTest::menuCallback_switchCoat(Ref* sender)

{

_useUpBodyId++;

if(_useUpBodyId > 1 )

{

_useUpBodyId = 0;

}

if(_useUpBodyId >= 0  && _sprite)

{

for(int i =
0; i < 2; i++ )

{

auto subMesh = _sprite->getMeshByName(_girlUpperBody[i]);

if(subMesh)

{

if(i == _useUpBodyId )

{

subMesh->setVisible(true);

}

else

{

subMesh->setVisible(false);

}

}

}

}

}

void Sprite3DReskinTest::menuCallback_switchPants(Ref* sender)

{

_usePantsId++;

if(_usePantsId > 1 )

{

_usePantsId = 0;

}

if(_usePantsId >= 0  && _sprite)

{

for(int i =
0; i < 2; i++ )

{

auto subMesh = _sprite->getMeshByName(_girlPants[i]);

if(subMesh)

{

if(i == _usePantsId )

{

subMesh->setVisible(true);

}

else

{

subMesh->setVisible(false);

}

}

}

}

}

void Sprite3DReskinTest::menuCallback_switchShoes(Ref* sender)

{

_useShoesId++;

if(_useShoesId > 1 )

{

_useShoesId = 0;

}

if(_useShoesId >=
0  && _sprite)

{

for(int i =
0; i < 2; i++ )

{

auto subMesh = _sprite->getMeshByName(_girlShoes[i]);

if(subMesh)

{

if(i == _useShoesId )

{

subMesh->setVisible(true);

}

else

{

subMesh->setVisible(false);

}

}

}

}

}

std::string Sprite3DReskinTest::title() const

{

return
"Testing Sprite3D Reskin";

}

std::string Sprite3DReskinTest::subtitle() const

{

return "";

}

void Sprite3DReskinTest::addNewSpriteWithCoords(Vec2 p)

{

_girlPants[0]= "Girl_LowerBody01";

_girlPants[1]= "Girl_LowerBody02";

_girlUpperBody[0] = "Girl_UpperBody01";

_girlUpperBody[1] = "Girl_UpperBody02";

_girlShoes[0]  = "Girl_Shoes01";

_girlShoes[1]  = "Girl_Shoes02";

_girlHair[0]= "Girl_Hair01";

_girlHair[1]= "Girl_Hair02";

_usePantsId = 0;

_useUpBodyId = 0;

_useShoesId   =0;

_useHairId = 0;

std::string fileName = "Sprite3DTest/ReskinGirl.c3b";

auto sprite = Sprite3D::create(fileName);

sprite->setScale(4);

sprite->setRotation3D(Vec3(0,0,0));

auto girlPants = sprite->getMeshByName(_girlPants[1]);

if(girlPants)

{

girlPants->setVisible(false);

}

auto girlShoes = sprite->getMeshByName(_girlShoes[1]);

if(girlShoes)

{

girlShoes->setVisible(false);

}

auto girlHair = sprite->getMeshByName(_girlHair[1]);

if(girlHair)

{

girlHair->setVisible(false);

}

auto girlUpBody = sprite->getMeshByName( _girlUpperBody[1]);

if(girlUpBody)

{

girlUpBody->setVisible(false);

}

addChild(sprite);

sprite->setPosition( Vec2( p.x, p.y-60) );

auto animation = Animation3D::create(fileName);

if (animation)

{

auto animate = Animate3D::create(animation);

sprite->runAction(RepeatForever::create(animate));

}

_sprite = sprite;

}

9.包围盒与3D模型碰撞的实现

Sprite3DWithOBBPerfromanceTest::Sprite3DWithOBBPerfromanceTest()

{

auto listener = EventListenerTouchAllAtOnce::create();

listener->onTouchesBegan = CC_CALLBACK_2(Sprite3DWithOBBPerfromanceTest::onTouchesBegan,
this);

listener->onTouchesEnded = CC_CALLBACK_2(Sprite3DWithOBBPerfromanceTest::onTouchesEnded,
this);

listener->onTouchesMoved = CC_CALLBACK_2(Sprite3DWithOBBPerfromanceTest::onTouchesMoved,
this);

_eventDispatcher->addEventListenerWithSceneGraphPriority(listener,
this);

auto s = Director::getInstance()->getWinSize();

initDrawBox();

addNewSpriteWithCoords(Vec2(s.width/2, s.height/2));

MenuItemFont::setFontName("fonts/arial.ttf");

MenuItemFont::setFontSize(65);

auto decrease = MenuItemFont::create(" - ", CC_CALLBACK_1(Sprite3DWithOBBPerfromanceTest::delOBBCallback,
this));

decrease->setColor(Color3B(0,200,20));

auto increase = MenuItemFont::create(" + ", CC_CALLBACK_1(Sprite3DWithOBBPerfromanceTest::addOBBCallback,
this));

increase->setColor(Color3B(0,200,20));

auto menu = Menu::create(decrease, increase,
nullptr);

menu->alignItemsHorizontally();

menu->setPosition(Vec2(s.width/2, s.height-65));

addChild(menu, 1);

TTFConfig ttfCount("fonts/Marker Felt.ttf",
30);

_labelCubeCount = Label::createWithTTF(ttfCount,"0 cubes");

_labelCubeCount->setColor(Color3B(0,200,20));

_labelCubeCount->setPosition(Vec2(s.width/2, s.height-90));

addChild(_labelCubeCount);

_hasCollider = false;

addOBBCallback(nullptr);

scheduleUpdate();

}

std::string Sprite3DWithOBBPerfromanceTest::title()
const

{

return
"OBB Collison Perfromance Test";

}

std::string Sprite3DWithOBBPerfromanceTest::subtitle()
const

{

return "";

}

void Sprite3DWithOBBPerfromanceTest::addNewOBBWithCoords(Vec2 p)

{

Vec3 extents = Vec3(10,
10, 10);

AABB aabb(-extents, extents);

auto obb = OBB(aabb);

obb._center = Vec3(p.x,p.y,0);

_obb.push_back(obb);

}

void Sprite3DWithOBBPerfromanceTest::onTouchesBegan(const std::vector<Touch*>& touches, Event* event)

{

for (auto touch: touches)

{

auto location = touch->getLocationInView();

if(_obb.size() > 0)

{

_intersetList.clear();

Ray ray;

calculateRayByLocationInView(&ray,location);

for(int i =
0; i < _obb.size(); i++)

{

if(ray.intersects(_obb[i]))

{

_intersetList.insert(i);

return;

}

}

}

}

}

void Sprite3DWithOBBPerfromanceTest::onTouchesEnded(const std::vector<Touch*>& touches, Event* event)

{

}

void Sprite3DWithOBBPerfromanceTest::onTouchesMoved(const std::vector<Touch*>& touches, Event* event)

{

for (auto touch: touches)

{

auto location = touch->getLocation();

for(int i =
0; i < _obb.size(); i++)

{

if(_intersetList.find(i) != _intersetList.end())

_obb[i]._center = Vec3(location.x,location.y,0);

}

}

}

void Sprite3DWithOBBPerfromanceTest::update(float dt)

{

char szText[16];

sprintf(szText,"%lu cubes",_obb.size());

_labelCubeCount->setString(szText);

if (_drawDebug)

{

_drawDebug->clear();

Mat4 mat = _sprite->getNodeToWorldTransform();

mat.getRightVector(&_obbt._xAxis);

_obbt._xAxis.normalize();

mat.getUpVector(&_obbt._yAxis);

_obbt._yAxis.normalize();

mat.getForwardVector(&_obbt._zAxis);

_obbt._zAxis.normalize();

_obbt._center = _sprite->getPosition3D();

Vec3 corners[8] = {};

_obbt.getCorners(corners);

_drawDebug->drawCube(corners, Color4F(0,0,1,1));

}

if(_obb.size() > 0)

{

_drawOBB->clear();

for(int i =0; i < _obb.size(); i++)

{

Vec3 corners[8] = {};

_obb[i].getCorners(corners);

_drawOBB->drawCube(corners, _obbt.intersects(_obb[i])?Color4F(1,0,0,1):Color4F(0,1,0,1));

}

}

}

void Sprite3DWithOBBPerfromanceTest::initDrawBox()

{

_drawOBB = DrawNode3D::create();

addChild(_drawOBB);

}

void Sprite3DWithOBBPerfromanceTest::addNewSpriteWithCoords(Vec2 p)

{

std::string fileName = "Sprite3DTest/tortoise.c3b";

auto sprite = Sprite3D::create(fileName);

sprite->setScale(0.1f);

auto s = Director::getInstance()->getWinSize();

sprite->setPosition(Vec2(s.width * 4.f /
5.f, s.height / 2.f));

addChild(sprite);

_sprite = sprite;

auto animation = Animation3D::create(fileName);

if (animation)

{

auto animate = Animate3D::create(animation,
0.f, 1.933f);

sprite->runAction(RepeatForever::create(animate));

}

_moveAction = MoveTo::create(4.f, Vec2(s.width /
5.f, s.height / 2.f));

_moveAction->retain();

auto seq = Sequence::create(_moveAction, CallFunc::create(CC_CALLBACK_0(Sprite3DWithOBBPerfromanceTest::reachEndCallBack,
this)), nullptr);

seq->setTag(100);

sprite->runAction(seq);

AABB aabb = _sprite->getAABB();

    _obbt = OBB(aabb);

_drawDebug = DrawNode3D::create();

addChild(_drawDebug);

}

void Sprite3DWithOBBPerfromanceTest::reachEndCallBack()

{

_sprite->stopActionByTag(100);

auto inverse = (MoveTo*)_moveAction->reverse();

inverse->retain();

_moveAction->release();

_moveAction = inverse;

auto rot = RotateBy::create(1.0f, Vec3(0.f,
180.f, 0.f));

auto seq = Sequence::create(rot, _moveAction, CallFunc::create(CC_CALLBACK_0(Sprite3DWithOBBPerfromanceTest::reachEndCallBack,
this)), nullptr);

seq->setTag(100);

_sprite->runAction(seq);

}

void Sprite3DWithOBBPerfromanceTest::addOBBCallback(Ref* sender)

{

addOBBWithCount(10);

}

void Sprite3DWithOBBPerfromanceTest::addOBBWithCount(float value)

{

for(int i =
0; i < value; i++)

{

Vec2 randompos = Vec2(CCRANDOM_0_1() * Director::getInstance()->getWinSize().width,CCRANDOM_0_1() * Director::getInstance()->getWinSize().height);

Vec3 extents = Vec3(10,
10, 10);

AABB aabb(-extents, extents);

auto obb = OBB(aabb);

obb._center = Vec3(randompos.x,randompos.y,0);

_obb.push_back(obb);

}

}

void Sprite3DWithOBBPerfromanceTest::delOBBCallback(Ref* sender)

{

delOBBWithCount(10);

}

void Sprite3DWithOBBPerfromanceTest::delOBBWithCount(float value)

{

if(_obb.size() >= 10)

{

_obb.erase(_obb.begin(),_obb.begin() + value);

_drawOBB->clear();

}

else

return;

}

void Sprite3DWithOBBPerfromanceTest::unproject(const Mat4& viewProjection,
const Size* viewport, Vec3* src, Vec3* dst)

{

assert(dst);

assert(viewport->width != 0.0f && viewport->height !=
0.0f);

Vec4 screen(src->x / viewport->width, ((viewport->height - src->y)) / viewport->height, src->z,
1.0f);

screen.x = screen.x * 2.0f -
1.0f;

screen.y = screen.y * 2.0f -
1.0f;

screen.z = screen.z * 2.0f -
1.0f;

viewProjection.getInversed().transformVector(screen, &screen);

if (screen.w != 0.0f)

{

screen.x /= screen.w;

screen.y /= screen.w;

screen.z /= screen.w;

}

dst->set(screen.x, screen.y, screen.z);

}

void Sprite3DWithOBBPerfromanceTest::calculateRayByLocationInView(Ray* ray,
const Vec2& location)

{

auto dir = Director::getInstance();

auto view = dir->getWinSize();

Mat4 mat = dir->getMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);

mat = dir->getMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_PROJECTION);

Vec3 src = Vec3(location.x, location.y, -1);

Vec3 nearPoint;

unproject(mat, &view, &src, &nearPoint);

src = Vec3(location.x, location.y, 1);

Vec3 farPoint;

unproject(mat, &view, &src, &farPoint);

Vec3 direction;

Vec3::subtract(farPoint, nearPoint, &direction);

direction.normalize();

ray->_origin = nearPoint;

ray->_direction = direction;

}

10.3D模型的镜像

Sprite3DMirrorTest::Sprite3DMirrorTest()

: _sprite(nullptr)

, _mirrorSprite(nullptr)

{

auto s = Director::getInstance()->getWinSize();

addNewSpriteWithCoords( Vec2(s.width/2, s.height/2) );

}

std::string Sprite3DMirrorTest::title() const

{

return
"Sprite3D Mirror Test";

}

std::string Sprite3DMirrorTest::subtitle() const

{

return "";

}

void Sprite3DMirrorTest::addNewSpriteWithCoords(Vec2 p)

{

std::string fileName = "Sprite3DTest/orc.c3b";

auto sprite = Sprite3D::create(fileName);

sprite->setScale(5);

sprite->setRotation3D(Vec3(0,180,0));

addChild(sprite);

sprite->setPosition( Vec2( p.x - 80, p.y) );

//test attach

auto sp = Sprite3D::create("Sprite3DTest/axe.c3b");

sprite->getAttachNode("Bip001 R Hand")->addChild(sp);

auto animation = Animation3D::create(fileName);

if (animation)

{

auto animate = Animate3D::create(animation);

sprite->runAction(RepeatForever::create(animate));

}

_sprite = sprite;

_hasWeapon = true;

//create mirror Sprite3D 镜像

sprite = Sprite3D::create(fileName);

    sprite->setScale(5);

    sprite->setScaleX(-5);

    sprite->setCullFace(GL_FRONT);

    sprite->setRotation3D(Vec3(0,180,0));

    addChild(sprite);

    sprite->setPosition( Vec2( p.x + 80, p.y) );

//test attach

sp = Sprite3D::create("Sprite3DTest/axe.c3b");

sprite->getAttachNode("Bip001 R Hand")->addChild(sp);

animation = Animation3D::create(fileName);

if (animation)

{

auto animate = Animate3D::create(animation);

sprite->runAction(RepeatForever::create(animate));

}

_mirrorSprite = sprite;

}

时间: 2024-07-31 17:08:24

Cocos2d-x 3.3 的3D开发功能介绍的相关文章

人工智能名片技术开发功能介绍

加推人工智能名片由智能官网.智能名片.分销商城.智能CRM系统.营销裂变系统组成,不仅是一款智能小程序,还是一款人工智能销售系统.他能智能的识别用户行为.智能对客户进行行为画像,并且能够对客户进行成交跟进,极大的解放销售员和客服的客户沟通和维护成本. 加推人工智能名片功能介绍加推智能销售系统是以AI和大数据技术为驱动,以小程序为技术应用入口,构建起的资源高度整合的全流程智能营销系统.管理端分为AI雷达和BOSS雷达系统,使用对象是分别是企业销售人员和企业管理者. 一.AI雷达(销售人员使用).A

【转】使用 WebGL 进行 3D 开发,第 1 部分: WebGL 简介

转自HTML5游戏开发者社区 使用 WebGL 进行 3D 开发,第 1 部分: WebGL 简介使用 WebGL 进行 3D 开发,第 2 部分: 使用 WebGL 库以更少的编码做更多的事情使用 WebGL 进行 3D 开发,第 3 部分: 添加用户交互 WebGL API 让 JavaScript 开发人员能够直接利用如今的 PC 及移动设备硬件中强大的内置 3D 图形加速功能.现代浏览器透明地支持 WebGL,它使人们可以为主流 Web 用户创建高性能的 3D 游戏.应用程序以及 3D

【转】使用 WebGL 进行 3D 开发,第 2 部分: 使用 WebGL 库以更少的编码做更多的事情

转自HTML5开发社区 使用 WebGL 进行 3D 开发,第 1 部分: WebGL 简介使用 WebGL 进行 3D 开发,第 2 部分: 使用 WebGL 库以更少的编码做更多的事情使用 WebGL 进行 3D 开发,第 3 部分: 添加用户交互 WebGL API 让 JavaScript 开发人员能够直接利用如今的 PC 及移动设备硬件中强大的内置 3D 图形加速功能.现代浏览器透明地支持 WebGL,它使人们可以为主流 Web 用户创建高性能的 3D 游戏.应用程序以及 3D 增强的

【转】使用 WebGL 进行 3D 开发,第 3 部分: 添加用户交互

转自HTML5游戏开发者社区 使用 WebGL 进行 3D 开发,第 1 部分: WebGL 简介使用 WebGL 进行 3D 开发,第 2 部分: 使用 WebGL 库以更少的编码做更多的事情使用 WebGL 进行 3D 开发,第 3 部分: 添加用户交互 WebGL API 为 JavaScript 开发人员提供了直接利用当今的 PC 和移动设备硬件中的强大内置 3D 图形加速功能的能力.现代浏览器已经明确支持 WebGL,WebGL 使得人们可以为主流 Web 用户创建高性能的 3D 游戏

iOS 3D Touch功能

新的触摸体验--iOS9的3D Touch 一.引言 在iphone6s问世之后,很多果粉都争先要体验3D Touch给用户带来的额外维度上的交互,这个设计之所以叫做3D Touch,其原理上是增加了一个压力的感触,通过区分轻按和重按来进行不同的用户交互. 二.在模拟器上学习和测试3D Touch 3D Touch 是一个很新颖的设计,可是苹果文档有言: With Xcode 7.0 you must develop on a device that supports 3D Touch. Sim

为自己的应用配置3D Touch 功能

随着 iPhone 6s 以及 iPhone 6s Plus 的发布,开发者们现在就可以为自己的应用配备上 3D Touch 功能了,从而给界面交互方式开启一个新的维度. 正如苹果所言,开发者可以通过非常简单的 API 来使用 3D Touch ,从根本上来说,也就是 UITouch 的一个简单的新属性. override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) { guard let touch

mfc小工具开发之定时闹钟之---功能介绍

使用背景: 之前在xp上用过飞雪日历,感觉挺好用的,还有在音频上的兴趣,促使了我也要自己做一个简单的定时闹钟. 之前开发过图片格式的小工具,没来的急分享,后期整理后,一块奉上,写这篇介绍的时候已近完成定时闹钟的demo部分,时间是凌晨0点30 功能需求: 1.守护进程,进行全天候开启定时闹钟deamon 2.初期暂时定时,然后 准确报时 3.音乐提醒 4.能够后台挂起,无需打扰用户 mfc小工具开发之定时闹钟之---功能介绍,布布扣,bubuko.com

SoundCloud 的开发功能

SoundCloud开发功能:https://developers.soundcloud.com/docs 来自为知笔记(Wiz)

微信成为首批支持iPhone 6s /Plus 上 3D Touch 功能的 App

2015苹果新品发布会上微信成为首批支持iPhone 6s 和 iPhone 6s Plus 上 3D Touch 功能的 App.通过 3D Touch,微信用户将可以通过更精减的操作完成基本任务,包括新建聊天,录制小视频,显示我的二维码或免打扰一小时.用户还可以通过 3D Touch 进行图片缩略图预览:或在聊天页面中启动快速操作菜单,选择标为已读 / 未读,免打扰,置顶或删除. 微信还通过3D Touch推出新功能,让用户可以在发送消息时更改字号或者表情图标的大小.相信微信会给用户带来更多