2019-11-23 20:27:39 +08:00
|
|
|
/****************************************************************************
|
|
|
|
Copyright (c) 2017-2018 Xiamen Yaji Software Co., Ltd.
|
2021-12-31 12:12:40 +08:00
|
|
|
|
2022-10-01 16:24:52 +08:00
|
|
|
https://axmolengine.github.io/
|
2021-12-31 12:12:40 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
|
|
in the Software without restriction, including without limitation the rights
|
|
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
|
|
furnished to do so, subject to the following conditions:
|
2021-12-31 12:12:40 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
The above copyright notice and this permission notice shall be included in
|
|
|
|
all copies or substantial portions of the Software.
|
2021-12-31 12:12:40 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
|
|
THE SOFTWARE.
|
|
|
|
****************************************************************************/
|
|
|
|
|
|
|
|
#include "ShaderTest.h"
|
|
|
|
#include "../testResource.h"
|
2022-10-12 00:15:09 +08:00
|
|
|
#include "axmol.h"
|
2023-06-11 13:08:08 +08:00
|
|
|
#include "renderer/Shaders.h"
|
2019-11-23 20:27:39 +08:00
|
|
|
#include "renderer/backend/Device.h"
|
|
|
|
|
2022-07-11 17:50:21 +08:00
|
|
|
USING_NS_AX;
|
|
|
|
USING_NS_AX_EXT;
|
2019-11-23 20:27:39 +08:00
|
|
|
|
2021-12-31 12:12:40 +08:00
|
|
|
#define SET_UNIFORM(ps, name, value) \
|
|
|
|
do \
|
|
|
|
{ \
|
|
|
|
decltype(value) __v = value; \
|
|
|
|
auto __loc = (ps)->getUniformLocation(name); \
|
|
|
|
(ps)->setUniform(__loc, &__v, sizeof(__v)); \
|
|
|
|
} while (false)
|
|
|
|
|
|
|
|
#define SET_TEXTURE(ps, name, idx, value) \
|
|
|
|
do \
|
|
|
|
{ \
|
|
|
|
auto* __v = value; \
|
|
|
|
auto __loc = (ps)->getUniformLocation(name); \
|
|
|
|
(ps)->setTexture(__loc, idx, __v); \
|
|
|
|
} while (false)
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
ShaderTests::ShaderTests()
|
|
|
|
{
|
|
|
|
ADD_TEST_CASE(ShaderLensFlare);
|
|
|
|
ADD_TEST_CASE(ShaderMandelbrot);
|
|
|
|
ADD_TEST_CASE(ShaderJulia);
|
|
|
|
ADD_TEST_CASE(ShaderHeart);
|
|
|
|
ADD_TEST_CASE(ShaderFlower);
|
|
|
|
ADD_TEST_CASE(ShaderPlasma);
|
|
|
|
ADD_TEST_CASE(ShaderBlur);
|
|
|
|
ADD_TEST_CASE(ShaderRetroEffect);
|
|
|
|
ADD_TEST_CASE(ShaderMonjori);
|
|
|
|
ADD_TEST_CASE(ShaderGlow);
|
|
|
|
ADD_TEST_CASE(ShaderMultiTexture);
|
|
|
|
}
|
|
|
|
|
|
|
|
///---------------------------------------
|
2021-12-31 12:12:40 +08:00
|
|
|
//
|
2019-11-23 20:27:39 +08:00
|
|
|
// ShaderNode
|
2021-12-31 12:12:40 +08:00
|
|
|
//
|
2019-11-23 20:27:39 +08:00
|
|
|
///---------------------------------------
|
2021-12-31 12:12:40 +08:00
|
|
|
enum
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
|
|
|
SIZE_X = 256,
|
|
|
|
SIZE_Y = 256,
|
|
|
|
};
|
|
|
|
|
2021-12-31 12:12:40 +08:00
|
|
|
ShaderNode::ShaderNode() : _center(Vec2(0.0f, 0.0f)), _resolution(Vec2(0.0f, 0.0f)), _time(0.0f) {}
|
2019-11-23 20:27:39 +08:00
|
|
|
|
2021-12-31 12:12:40 +08:00
|
|
|
ShaderNode::~ShaderNode() {}
|
2019-11-23 20:27:39 +08:00
|
|
|
|
2021-12-31 12:12:40 +08:00
|
|
|
ShaderNode* ShaderNode::shaderNodeWithVertex(std::string_view vert, std::string_view frag)
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
2021-12-08 00:11:53 +08:00
|
|
|
auto node = new ShaderNode();
|
2019-11-23 20:27:39 +08:00
|
|
|
node->initWithVertex(vert, frag);
|
|
|
|
node->autorelease();
|
|
|
|
|
|
|
|
return node;
|
|
|
|
}
|
|
|
|
|
2021-12-31 12:12:40 +08:00
|
|
|
bool ShaderNode::initWithVertex(std::string_view vert, std::string_view frag)
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
2023-07-25 09:13:20 +08:00
|
|
|
if (vert.empty())
|
|
|
|
vert = position_vert;
|
2019-11-23 20:27:39 +08:00
|
|
|
_vertFileName = vert;
|
|
|
|
_fragFileName = frag;
|
|
|
|
|
|
|
|
loadShaderVertex(vert, frag);
|
|
|
|
|
2021-12-31 12:12:40 +08:00
|
|
|
_time = 0;
|
2019-11-23 20:27:39 +08:00
|
|
|
_resolution = Vec2(SIZE_X, SIZE_Y);
|
|
|
|
|
|
|
|
scheduleUpdate();
|
|
|
|
|
|
|
|
setContentSize(Size(SIZE_X, SIZE_Y));
|
|
|
|
setAnchorPoint(Vec2(0.5f, 0.5f));
|
2021-12-31 12:12:40 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
// init custom command
|
2020-01-06 09:35:12 +08:00
|
|
|
auto attrPosLoc = _programState->getAttributeLocation("a_position");
|
2023-07-25 09:13:20 +08:00
|
|
|
|
|
|
|
auto vertexLayout = _programState->getMutableVertexLayout();
|
|
|
|
vertexLayout->setAttrib("a_position", attrPosLoc, backend::VertexFormat::FLOAT2, 0, false);
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
float w = SIZE_X, h = SIZE_Y;
|
2021-12-31 12:12:40 +08:00
|
|
|
Vec2 vertices[6] = {Vec2(0.0f, 0.0f), Vec2(w, 0.0f), Vec2(w, h), Vec2(0.0f, 0.0f), Vec2(0.0f, h), Vec2(w, h)};
|
2023-07-25 09:13:20 +08:00
|
|
|
vertexLayout->setStride(sizeof(Vec2));
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
/*
|
|
|
|
* TODO: the Y-coordinate of subclasses are flipped in metal
|
|
|
|
*
|
2022-07-16 10:43:05 +08:00
|
|
|
* keywords: AX_USE_METAL , AX_USE_GL
|
2019-11-23 20:27:39 +08:00
|
|
|
*/
|
2021-12-31 12:12:40 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
_customCommand.createVertexBuffer(sizeof(Vec2), 6, CustomCommand::BufferUsage::STATIC);
|
|
|
|
_customCommand.updateVertexBuffer(vertices, sizeof(vertices));
|
|
|
|
|
|
|
|
_customCommand.setDrawType(CustomCommand::DrawType::ARRAY);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2021-12-31 12:12:40 +08:00
|
|
|
void ShaderNode::loadShaderVertex(std::string_view vert, std::string_view frag)
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
2023-07-25 09:13:20 +08:00
|
|
|
auto program = ProgramManager::getInstance()->loadProgram(vert, frag, VertexLayoutType::Sprite);
|
2019-11-23 20:27:39 +08:00
|
|
|
auto programState = new backend::ProgramState(program);
|
|
|
|
setProgramState(programState);
|
2022-07-16 10:43:05 +08:00
|
|
|
AX_SAFE_RELEASE(programState);
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
void ShaderNode::update(float dt)
|
|
|
|
{
|
|
|
|
_time += dt;
|
|
|
|
}
|
|
|
|
|
2021-12-31 12:12:40 +08:00
|
|
|
void ShaderNode::setPosition(const Vec2& newPosition)
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
|
|
|
Node::setPosition(newPosition);
|
2021-12-31 12:12:40 +08:00
|
|
|
auto position = getPosition();
|
|
|
|
auto frameSize = Director::getInstance()->getOpenGLView()->getFrameSize();
|
|
|
|
auto visibleSize = Director::getInstance()->getVisibleSize();
|
2019-11-23 20:27:39 +08:00
|
|
|
auto retinaFactor = Director::getInstance()->getOpenGLView()->getRetinaFactor();
|
2021-12-31 12:12:40 +08:00
|
|
|
_center = Vec2(position.x * frameSize.width / visibleSize.width * retinaFactor,
|
|
|
|
position.y * frameSize.height / visibleSize.height * retinaFactor);
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
|
|
|
|
2021-12-31 12:12:40 +08:00
|
|
|
void ShaderNode::draw(Renderer* renderer, const Mat4& transform, uint32_t flags)
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
|
|
|
_customCommand.init(_globalZOrder, transform, flags);
|
2021-12-31 12:12:40 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
_programState->setUniform(_locResolution, &_resolution, sizeof(_resolution));
|
|
|
|
_programState->setUniform(_locCenter, &_center, sizeof(_center));
|
|
|
|
|
|
|
|
auto projectionMatrix = Director::getInstance()->getMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_PROJECTION);
|
2021-12-31 12:12:40 +08:00
|
|
|
auto finalMatrix = projectionMatrix * transform;
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
_programState->setUniform(_locMVP, finalMatrix.m, sizeof(finalMatrix.m));
|
|
|
|
|
|
|
|
float time = Director::getInstance()->getTotalFrames() * Director::getInstance()->getAnimationInterval();
|
|
|
|
Vec4 uTime(time / 10.0f, time, time * 2.0f, time * 4.0f);
|
|
|
|
Vec4 sinTime(time / 8.0f, time / 4.0f, time / 2.0f, sinf(time));
|
|
|
|
Vec4 cosTime(time / 8.0f, time / 4.0f, time / 2.0f, cosf(time));
|
|
|
|
|
|
|
|
_programState->setUniform(_locTime, &uTime, sizeof(uTime));
|
|
|
|
_programState->setUniform(_locSinTime, &sinTime, sizeof(sinTime));
|
|
|
|
_programState->setUniform(_locCosTime, &cosTime, sizeof(cosTime));
|
|
|
|
|
|
|
|
renderer->addCommand(&_customCommand);
|
2022-07-16 10:43:05 +08:00
|
|
|
AX_INCREMENT_GL_DRAWN_BATCHES_AND_VERTICES(1, 6);
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
void ShaderNode::updateUniforms()
|
|
|
|
{
|
2021-12-31 12:12:40 +08:00
|
|
|
if (_programState == nullptr)
|
2019-11-23 20:27:39 +08:00
|
|
|
return;
|
|
|
|
|
2021-12-31 12:12:40 +08:00
|
|
|
_locResolution = _programState->getUniformLocation("resolution");
|
|
|
|
_locCenter = _programState->getUniformLocation("center");
|
|
|
|
_locMVP = _programState->getUniformLocation("u_MVPMatrix");
|
|
|
|
_locTime = _programState->getUniformLocation("u_Time");
|
|
|
|
_locSinTime = _programState->getUniformLocation("u_SinTime");
|
|
|
|
_locCosTime = _programState->getUniformLocation("u_CosTime");
|
|
|
|
_locScreenSize = _programState->getUniformLocation("u_screenSize");
|
|
|
|
|
|
|
|
const Vec2& frameSize = Director::getInstance()->getOpenGLView()->getFrameSize();
|
|
|
|
float retinaFactor = Director::getInstance()->getOpenGLView()->getRetinaFactor();
|
2019-11-23 20:27:39 +08:00
|
|
|
auto screenSizeInPixels = frameSize * retinaFactor;
|
|
|
|
_programState->setUniform(_locScreenSize, &screenSizeInPixels, sizeof(screenSizeInPixels));
|
|
|
|
}
|
|
|
|
|
|
|
|
/// ShaderMonjori
|
|
|
|
|
2021-12-31 12:12:40 +08:00
|
|
|
ShaderMonjori::ShaderMonjori() {}
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
bool ShaderMonjori::init()
|
|
|
|
{
|
|
|
|
if (ShaderTestDemo::init())
|
|
|
|
{
|
2023-07-25 09:13:20 +08:00
|
|
|
auto sn = ShaderNode::shaderNodeWithVertex("", "custom/example_Monjori_fs");
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
auto s = Director::getInstance()->getWinSize();
|
2021-12-31 12:12:40 +08:00
|
|
|
sn->setPosition(Vec2(s.width / 2, s.height / 2));
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
addChild(sn);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string ShaderMonjori::title() const
|
|
|
|
{
|
|
|
|
return "Shader: Frag shader";
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string ShaderMonjori::subtitle() const
|
|
|
|
{
|
|
|
|
return "Monjori plane deformations";
|
|
|
|
}
|
|
|
|
|
|
|
|
/// ShaderMandelbrot
|
2021-12-31 12:12:40 +08:00
|
|
|
ShaderMandelbrot::ShaderMandelbrot() {}
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
bool ShaderMandelbrot::init()
|
|
|
|
{
|
|
|
|
if (ShaderTestDemo::init())
|
|
|
|
{
|
2023-07-25 09:13:20 +08:00
|
|
|
auto sn = ShaderNode::shaderNodeWithVertex("", "custom/example_Mandelbrot_fs");
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
auto s = Director::getInstance()->getWinSize();
|
2021-12-31 12:12:40 +08:00
|
|
|
sn->setPosition(Vec2(s.width / 2, s.height / 2));
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
addChild(sn);
|
|
|
|
return true;
|
|
|
|
}
|
2021-12-31 12:12:40 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string ShaderMandelbrot::title() const
|
|
|
|
{
|
|
|
|
return "Shader: Frag shader";
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string ShaderMandelbrot::subtitle() const
|
|
|
|
{
|
|
|
|
return "Mandelbrot shader with Zoom";
|
|
|
|
}
|
|
|
|
|
|
|
|
/// ShaderJulia
|
2021-12-31 12:12:40 +08:00
|
|
|
ShaderJulia::ShaderJulia() {}
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
bool ShaderJulia::init()
|
|
|
|
{
|
|
|
|
if (ShaderTestDemo::init())
|
|
|
|
{
|
2023-07-25 09:13:20 +08:00
|
|
|
auto sn = ShaderNode::shaderNodeWithVertex("", "custom/example_Julia_fs");
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
auto s = Director::getInstance()->getWinSize();
|
2021-12-31 12:12:40 +08:00
|
|
|
sn->setPosition(Vec2(s.width / 2, s.height / 2));
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
addChild(sn);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string ShaderJulia::title() const
|
|
|
|
{
|
|
|
|
return "Shader: Frag shader";
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string ShaderJulia::subtitle() const
|
|
|
|
{
|
|
|
|
return "Julia shader";
|
|
|
|
}
|
|
|
|
|
|
|
|
/// ShaderHeart
|
2021-12-31 12:12:40 +08:00
|
|
|
ShaderHeart::ShaderHeart() {}
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
bool ShaderHeart::init()
|
|
|
|
{
|
|
|
|
if (ShaderTestDemo::init())
|
|
|
|
{
|
2023-07-25 09:13:20 +08:00
|
|
|
auto sn = ShaderNode::shaderNodeWithVertex("", "custom/example_Heart_fs");
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
auto s = Director::getInstance()->getWinSize();
|
2021-12-31 12:12:40 +08:00
|
|
|
sn->setPosition(Vec2(s.width / 2, s.height / 2));
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
addChild(sn);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string ShaderHeart::title() const
|
|
|
|
{
|
|
|
|
return "Shader: Frag shader";
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string ShaderHeart::subtitle() const
|
|
|
|
{
|
|
|
|
return "Heart";
|
|
|
|
}
|
|
|
|
|
|
|
|
/// ShaderFlower
|
2021-12-31 12:12:40 +08:00
|
|
|
ShaderFlower::ShaderFlower() {}
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
bool ShaderFlower::init()
|
|
|
|
{
|
|
|
|
if (ShaderTestDemo::init())
|
|
|
|
{
|
2023-07-25 09:13:20 +08:00
|
|
|
auto sn = ShaderNode::shaderNodeWithVertex("", "custom/example_Flower_fs");
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
auto s = Director::getInstance()->getWinSize();
|
2021-12-31 12:12:40 +08:00
|
|
|
sn->setPosition(Vec2(s.width / 2, s.height / 2));
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
addChild(sn);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string ShaderFlower::title() const
|
|
|
|
{
|
|
|
|
return "Shader: Frag shader";
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string ShaderFlower::subtitle() const
|
|
|
|
{
|
|
|
|
return "Flower";
|
|
|
|
}
|
|
|
|
|
|
|
|
/// ShaderPlasma
|
2021-12-31 12:12:40 +08:00
|
|
|
ShaderPlasma::ShaderPlasma() {}
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
bool ShaderPlasma::init()
|
|
|
|
{
|
|
|
|
if (ShaderTestDemo::init())
|
|
|
|
{
|
2023-07-25 09:13:20 +08:00
|
|
|
auto sn = ShaderNode::shaderNodeWithVertex("", "custom/example_Plasma_fs");
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
auto s = Director::getInstance()->getWinSize();
|
2021-12-31 12:12:40 +08:00
|
|
|
sn->setPosition(Vec2(s.width / 2, s.height / 2));
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
addChild(sn);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string ShaderPlasma::title() const
|
|
|
|
{
|
|
|
|
return "Shader: Frag shader";
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string ShaderPlasma::subtitle() const
|
|
|
|
{
|
|
|
|
return "Plasma";
|
|
|
|
}
|
|
|
|
|
|
|
|
// ShaderBlur
|
|
|
|
|
|
|
|
class SpriteBlur : public Sprite
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
~SpriteBlur();
|
2021-12-31 12:12:40 +08:00
|
|
|
bool initWithTexture(Texture2D* texture, const Rect& rect);
|
2019-11-23 20:27:39 +08:00
|
|
|
void initProgram();
|
|
|
|
|
2021-12-31 12:12:40 +08:00
|
|
|
static SpriteBlur* create(const char* pszFileName);
|
2019-11-23 20:27:39 +08:00
|
|
|
void setBlurRadius(float radius);
|
|
|
|
void setBlurSampleNum(float num);
|
|
|
|
|
|
|
|
protected:
|
|
|
|
float _blurRadius;
|
|
|
|
float _blurSampleNum;
|
|
|
|
};
|
|
|
|
|
2021-12-31 12:12:40 +08:00
|
|
|
SpriteBlur::~SpriteBlur() {}
|
2019-11-23 20:27:39 +08:00
|
|
|
|
2021-12-31 12:12:40 +08:00
|
|
|
SpriteBlur* SpriteBlur::create(const char* pszFileName)
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
2021-12-08 00:11:53 +08:00
|
|
|
SpriteBlur* pRet = new SpriteBlur();
|
2021-12-31 12:12:40 +08:00
|
|
|
bool result = pRet->initWithFile("");
|
2022-10-18 19:17:36 +08:00
|
|
|
ax::log("Test call Sprite::initWithFile with bad file name result is : %s", result ? "true" : "false");
|
2019-11-23 20:27:39 +08:00
|
|
|
|
2021-12-08 00:11:53 +08:00
|
|
|
if (pRet->initWithFile(pszFileName))
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
|
|
|
pRet->autorelease();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2022-07-16 10:43:05 +08:00
|
|
|
AX_SAFE_DELETE(pRet);
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
2021-12-31 12:12:40 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
return pRet;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool SpriteBlur::initWithTexture(Texture2D* texture, const Rect& rect)
|
|
|
|
{
|
|
|
|
_blurRadius = 0;
|
2021-12-31 12:12:40 +08:00
|
|
|
if (Sprite::initWithTexture(texture, rect))
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
2022-07-16 10:43:05 +08:00
|
|
|
#if AX_ENABLE_CACHE_TEXTURE_DATA
|
2021-12-31 12:12:40 +08:00
|
|
|
auto listener =
|
|
|
|
EventListenerCustom::create(EVENT_RENDERER_RECREATED, [this](EventCustom* event) { initProgram(); });
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
|
|
|
|
#endif
|
2021-12-31 12:12:40 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
initProgram();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
void SpriteBlur::initProgram()
|
|
|
|
{
|
2023-07-25 09:13:20 +08:00
|
|
|
auto program = ProgramManager::getInstance()->loadProgram(positionTextureColor_vert, "custom/example_Blur_fs", VertexLayoutType::Sprite);
|
2019-11-23 20:27:39 +08:00
|
|
|
auto programState = new backend::ProgramState(program);
|
|
|
|
setProgramState(programState);
|
2022-07-16 10:43:05 +08:00
|
|
|
AX_SAFE_RELEASE(programState);
|
2021-12-31 12:12:40 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
auto size = getTexture()->getContentSizeInPixels();
|
|
|
|
|
|
|
|
SET_UNIFORM(_programState, "resolution", size);
|
|
|
|
SET_UNIFORM(_programState, "blurRadius", _blurRadius);
|
|
|
|
SET_UNIFORM(_programState, "sampleNum", 7.0f);
|
2021-12-31 12:12:40 +08:00
|
|
|
SET_UNIFORM(_programState, "u_PMatrix",
|
|
|
|
Director::getInstance()->getMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_PROJECTION));
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
void SpriteBlur::setBlurRadius(float radius)
|
|
|
|
{
|
|
|
|
_blurRadius = radius;
|
|
|
|
SET_UNIFORM(_programState, "blurRadius", _blurRadius);
|
|
|
|
}
|
|
|
|
|
|
|
|
void SpriteBlur::setBlurSampleNum(float num)
|
|
|
|
{
|
|
|
|
_blurSampleNum = num;
|
|
|
|
SET_UNIFORM(_programState, "sampleNum", _blurSampleNum);
|
|
|
|
}
|
|
|
|
|
|
|
|
// ShaderBlur
|
|
|
|
|
2021-12-31 12:12:40 +08:00
|
|
|
ShaderBlur::ShaderBlur() {}
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
std::string ShaderBlur::title() const
|
|
|
|
{
|
|
|
|
return "Shader: Frag shader";
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string ShaderBlur::subtitle() const
|
|
|
|
{
|
2021-12-31 12:12:40 +08:00
|
|
|
return "Gaussian blur";
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
void ShaderBlur::createSliderCtls()
|
|
|
|
{
|
|
|
|
auto screenSize = Director::getInstance()->getWinSize();
|
2021-12-31 12:12:40 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
2021-12-31 12:12:40 +08:00
|
|
|
ControlSlider* slider = ControlSlider::create("extensions/sliderTrack.png", "extensions/sliderProgress.png",
|
|
|
|
"extensions/sliderThumb.png");
|
2019-11-23 20:27:39 +08:00
|
|
|
slider->setAnchorPoint(Vec2(0.5f, 1.0f));
|
|
|
|
slider->setMinimumValue(0.0f);
|
|
|
|
slider->setMaximumValue(25.0f);
|
|
|
|
slider->setScale(0.6f);
|
|
|
|
slider->setPosition(Vec2(screenSize.width / 4.0f, screenSize.height / 3.0f + 24.0f));
|
2021-12-31 12:12:40 +08:00
|
|
|
slider->addTargetWithActionForControlEvents(this, cccontrol_selector(ShaderBlur::onRadiusChanged),
|
|
|
|
Control::EventType::VALUE_CHANGED);
|
2019-11-23 20:27:39 +08:00
|
|
|
slider->setValue(2.0f);
|
|
|
|
addChild(slider);
|
|
|
|
_sliderRadiusCtl = slider;
|
2021-12-31 12:12:40 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
auto label = Label::createWithTTF("Blur Radius", "fonts/arial.ttf", 12.0f);
|
|
|
|
addChild(label);
|
|
|
|
label->setPosition(Vec2(screenSize.width / 4.0f, screenSize.height / 3.0f));
|
|
|
|
}
|
2021-12-31 12:12:40 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
2021-12-31 12:12:40 +08:00
|
|
|
ControlSlider* slider = ControlSlider::create("extensions/sliderTrack.png", "extensions/sliderProgress.png",
|
|
|
|
"extensions/sliderThumb.png");
|
2019-11-23 20:27:39 +08:00
|
|
|
slider->setAnchorPoint(Vec2(0.5f, 1.0f));
|
|
|
|
slider->setMinimumValue(0.0f);
|
|
|
|
slider->setMaximumValue(11.0f);
|
|
|
|
slider->setScale(0.6f);
|
|
|
|
slider->setPosition(Vec2(screenSize.width / 4.0f, screenSize.height / 3.0f - 10.0f));
|
2021-12-31 12:12:40 +08:00
|
|
|
slider->addTargetWithActionForControlEvents(this, cccontrol_selector(ShaderBlur::onSampleNumChanged),
|
|
|
|
Control::EventType::VALUE_CHANGED);
|
2019-11-23 20:27:39 +08:00
|
|
|
slider->setValue(7.0f);
|
|
|
|
addChild(slider);
|
|
|
|
_sliderNumCtrl = slider;
|
2021-12-31 12:12:40 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
auto label = Label::createWithTTF("Blur Sample Num", "fonts/arial.ttf", 12.0f);
|
|
|
|
addChild(label);
|
|
|
|
label->setPosition(Vec2(screenSize.width / 4.0f, screenSize.height / 3.0f - 34.0f));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ShaderBlur::init()
|
|
|
|
{
|
2021-12-31 12:12:40 +08:00
|
|
|
if (ShaderTestDemo::init())
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
|
|
|
_blurSprite = SpriteBlur::create("Images/grossini.png");
|
|
|
|
auto sprite = Sprite::create("Images/grossini.png");
|
2021-12-31 12:12:40 +08:00
|
|
|
auto s = Director::getInstance()->getWinSize();
|
|
|
|
_blurSprite->setPosition(Vec2(s.width / 3, s.height / 2 + 30.0f));
|
|
|
|
sprite->setPosition(Vec2(2 * s.width / 3, s.height / 2 + 30.0f));
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
addChild(_blurSprite);
|
|
|
|
addChild(sprite);
|
2021-12-31 12:12:40 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
auto label = Label::createWithTTF("Normal Sprite", "fonts/arial.ttf", 12.0f);
|
|
|
|
addChild(label);
|
2021-12-31 12:12:40 +08:00
|
|
|
label->setPosition(Vec2(2 * s.width / 3, s.height / 3.0f));
|
2019-11-23 20:27:39 +08:00
|
|
|
createSliderCtls();
|
2021-12-31 12:12:40 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ShaderBlur::onRadiusChanged(Ref* sender, Control::EventType)
|
|
|
|
{
|
|
|
|
ControlSlider* slider = (ControlSlider*)sender;
|
|
|
|
_blurSprite->setBlurRadius(slider->getValue());
|
|
|
|
}
|
|
|
|
|
|
|
|
void ShaderBlur::onSampleNumChanged(Ref* sender, Control::EventType)
|
|
|
|
{
|
|
|
|
ControlSlider* slider = (ControlSlider*)sender;
|
|
|
|
_blurSprite->setBlurSampleNum(slider->getValue());
|
|
|
|
}
|
|
|
|
|
|
|
|
// ShaderRetroEffect
|
|
|
|
|
2021-12-31 12:12:40 +08:00
|
|
|
ShaderRetroEffect::ShaderRetroEffect() : _label(nullptr), _accum(0.0f) {}
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
bool ShaderRetroEffect::init()
|
|
|
|
{
|
2021-12-31 12:12:40 +08:00
|
|
|
if (ShaderTestDemo::init())
|
|
|
|
{
|
|
|
|
|
|
|
|
auto fragStr = FileUtils::getInstance()->getStringFromFile(
|
2023-07-25 09:13:20 +08:00
|
|
|
FileUtils::getInstance()->fullPathForFilename("custom/example_HorizontalColor_fs"));
|
2021-12-31 12:12:40 +08:00
|
|
|
char* fragSource = (char*)fragStr.c_str();
|
2019-11-23 20:27:39 +08:00
|
|
|
|
2023-07-25 09:13:20 +08:00
|
|
|
auto program = ProgramManager::getInstance()->loadProgram(positionTextureColor_vert, "custom/example_HorizontalColor_fs", VertexLayoutType::Sprite);
|
2021-12-31 12:12:40 +08:00
|
|
|
auto p = new backend::ProgramState(program);
|
2019-11-23 20:27:39 +08:00
|
|
|
auto director = Director::getInstance();
|
|
|
|
const auto& screenSizeLocation = p->getUniformLocation("u_screenSize");
|
2021-12-31 12:12:40 +08:00
|
|
|
const auto& frameSize = director->getOpenGLView()->getFrameSize();
|
|
|
|
float retinaFactor = director->getOpenGLView()->getRetinaFactor();
|
|
|
|
auto screenSizeInPixels = frameSize * retinaFactor;
|
2019-11-23 20:27:39 +08:00
|
|
|
p->setUniform(screenSizeLocation, &screenSizeInPixels, sizeof(screenSizeInPixels));
|
2021-12-31 12:12:40 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
auto s = director->getWinSize();
|
|
|
|
|
2021-12-31 12:12:40 +08:00
|
|
|
_label = Label::createWithBMFont("fonts/west_england-64.fnt", "RETRO EFFECT");
|
2019-11-23 20:27:39 +08:00
|
|
|
_label->setAnchorPoint(Vec2::ANCHOR_MIDDLE);
|
|
|
|
_label->setProgramState(p);
|
2022-07-16 10:43:05 +08:00
|
|
|
AX_SAFE_RELEASE(p);
|
2019-11-23 20:27:39 +08:00
|
|
|
|
2021-12-31 12:12:40 +08:00
|
|
|
_label->setPosition(Vec2(s.width / 2, s.height / 2));
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
addChild(_label);
|
|
|
|
|
|
|
|
scheduleUpdate();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ShaderRetroEffect::update(float dt)
|
|
|
|
{
|
|
|
|
_accum += dt;
|
|
|
|
int letterCount = _label->getStringLength();
|
|
|
|
for (int i = 0; i < letterCount; ++i)
|
|
|
|
{
|
|
|
|
auto sprite = _label->getLetter(i);
|
|
|
|
if (sprite != nullptr)
|
|
|
|
{
|
|
|
|
auto oldPosition = sprite->getPosition();
|
2021-12-31 12:12:40 +08:00
|
|
|
sprite->setPosition(Vec2(oldPosition.x, sinf(_accum * 2 + i / 2.0) * 20));
|
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
// add fabs() to prevent negative scaling
|
2021-12-31 12:12:40 +08:00
|
|
|
float scaleY = (sinf(_accum * 2 + i / 2.0 + 0.707));
|
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
sprite->setScaleY(scaleY);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string ShaderRetroEffect::title() const
|
|
|
|
{
|
|
|
|
return "Shader: Retro test";
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string ShaderRetroEffect::subtitle() const
|
|
|
|
{
|
|
|
|
return "sin() effect with moving colors";
|
|
|
|
}
|
|
|
|
|
2021-12-31 12:12:40 +08:00
|
|
|
ShaderLensFlare::ShaderLensFlare() {}
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
std::string ShaderLensFlare::title() const
|
|
|
|
{
|
|
|
|
return "ShaderToy Test";
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string ShaderLensFlare::subtitle() const
|
|
|
|
{
|
|
|
|
return "Lens Flare";
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ShaderLensFlare::init()
|
|
|
|
{
|
|
|
|
if (ShaderTestDemo::init())
|
|
|
|
{
|
2023-07-25 09:13:20 +08:00
|
|
|
auto sn = ShaderNode::shaderNodeWithVertex("", "custom/shadertoy_LensFlare_fs");
|
2021-12-31 12:12:40 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
auto s = Director::getInstance()->getWinSize();
|
2021-12-31 12:12:40 +08:00
|
|
|
sn->setPosition(Vec2(s.width / 2, s.height / 2));
|
|
|
|
sn->setContentSize(Size(s.width / 2, s.height / 2));
|
2019-11-23 20:27:39 +08:00
|
|
|
addChild(sn);
|
2021-12-31 12:12:40 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
return true;
|
|
|
|
}
|
2021-12-31 12:12:40 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// ShaderGlow
|
|
|
|
//
|
2021-12-31 12:12:40 +08:00
|
|
|
ShaderGlow::ShaderGlow() {}
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
std::string ShaderGlow::title() const
|
|
|
|
{
|
|
|
|
return "ShaderToy Test";
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string ShaderGlow::subtitle() const
|
|
|
|
{
|
|
|
|
return "Glow";
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ShaderGlow::init()
|
|
|
|
{
|
|
|
|
if (ShaderTestDemo::init())
|
|
|
|
{
|
2023-07-25 09:13:20 +08:00
|
|
|
auto sn = ShaderNode::shaderNodeWithVertex("", "custom/shadertoy_Glow_fs");
|
2021-12-31 12:12:40 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
auto s = Director::getInstance()->getWinSize();
|
2021-12-31 12:12:40 +08:00
|
|
|
sn->setPosition(Vec2(s.width / 2, s.height / 2));
|
|
|
|
sn->setContentSize(Size(s.width / 2, s.height / 2));
|
2019-11-23 20:27:39 +08:00
|
|
|
addChild(sn);
|
2021-12-31 12:12:40 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
return true;
|
|
|
|
}
|
2021-12-31 12:12:40 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// ShaderMultiTexture
|
|
|
|
//
|
2021-12-31 12:12:40 +08:00
|
|
|
ShaderMultiTexture::ShaderMultiTexture() : _changedTextureId(0) {}
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
std::string ShaderMultiTexture::title() const
|
|
|
|
{
|
|
|
|
return "MultiTexture test";
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string ShaderMultiTexture::subtitle() const
|
|
|
|
{
|
|
|
|
return "MultiTexture";
|
|
|
|
}
|
|
|
|
|
|
|
|
ui::Slider* ShaderMultiTexture::createSliderCtl()
|
|
|
|
{
|
|
|
|
auto screenSize = Director::getInstance()->getWinSize();
|
|
|
|
|
|
|
|
ui::Slider* slider = ui::Slider::create();
|
|
|
|
slider->loadBarTexture("cocosui/sliderTrack.png");
|
|
|
|
slider->loadSlidBallTextures("cocosui/sliderThumb.png", "cocosui/sliderThumb.png", "");
|
|
|
|
slider->loadProgressBarTexture("cocosui/sliderProgress.png");
|
|
|
|
slider->setPercent(50);
|
|
|
|
|
|
|
|
slider->setPosition(Vec2(screenSize.width / 2.0f, screenSize.height / 3.0f));
|
|
|
|
addChild(slider);
|
|
|
|
|
|
|
|
slider->addEventListener([&](Ref* sender, ui::Slider::EventType type) {
|
|
|
|
if (type == ui::Slider::EventType::ON_PERCENTAGE_CHANGED)
|
|
|
|
{
|
|
|
|
ui::Slider* slider = dynamic_cast<ui::Slider*>(sender);
|
2021-12-31 12:12:40 +08:00
|
|
|
float p = slider->getPercent() / 100.0f;
|
|
|
|
auto state = _sprite->getProgramState();
|
2019-11-23 20:27:39 +08:00
|
|
|
SET_UNIFORM(state, "u_interpolate", p);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return slider;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ShaderMultiTexture::init()
|
|
|
|
{
|
|
|
|
if (ShaderTestDemo::init())
|
|
|
|
{
|
|
|
|
auto s = Director::getInstance()->getWinSize();
|
|
|
|
|
|
|
|
// Left: normal sprite
|
|
|
|
auto left = Sprite::create("Images/grossinis_sister1.png");
|
|
|
|
addChild(left);
|
2021-12-31 12:12:40 +08:00
|
|
|
left->setPosition(s.width * 1 / 4, s.height / 2);
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
// Right: normal sprite
|
|
|
|
auto right = Sprite::create("Images/grossinis_sister2.png");
|
|
|
|
addChild(right, 0, rightSpriteTag);
|
2021-12-31 12:12:40 +08:00
|
|
|
right->setPosition(s.width * 3 / 4, s.height / 2);
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
// Center: MultiTexture
|
|
|
|
_sprite = Sprite::createWithTexture(left->getTexture());
|
|
|
|
addChild(_sprite);
|
2021-12-31 12:12:40 +08:00
|
|
|
_sprite->setPosition(Vec2(s.width / 2, s.height / 2));
|
2019-11-23 20:27:39 +08:00
|
|
|
|
2023-07-25 09:13:20 +08:00
|
|
|
auto program = ProgramManager::getInstance()->loadProgram("custom/example_MultiTexture_vs", "custom/example_MultiTexture_fs", VertexLayoutType::Sprite);
|
2021-12-31 12:12:40 +08:00
|
|
|
auto programState = new backend::ProgramState(program);
|
2019-11-23 20:27:39 +08:00
|
|
|
_sprite->setProgramState(programState);
|
|
|
|
|
2022-07-05 00:39:20 +08:00
|
|
|
SET_TEXTURE(programState, "u_tex1", 1, right->getTexture()->getBackendTexture());
|
2021-12-31 12:12:40 +08:00
|
|
|
SET_UNIFORM(programState, "u_interpolate", 0.5f);
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
// slider
|
|
|
|
createSliderCtl();
|
2021-12-31 12:12:40 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
// menu
|
|
|
|
auto label = Label::createWithTTF(TTFConfig("fonts/arial.ttf"), "change");
|
2022-07-16 10:43:05 +08:00
|
|
|
auto mi = MenuItemLabel::create(label, AX_CALLBACK_1(ShaderMultiTexture::changeTexture, this));
|
2021-12-31 12:12:40 +08:00
|
|
|
auto menu = Menu::create(mi, nullptr);
|
2019-11-23 20:27:39 +08:00
|
|
|
addChild(menu);
|
|
|
|
menu->setPosition(s.width * 7 / 8, s.height / 2);
|
|
|
|
|
2022-07-16 10:43:05 +08:00
|
|
|
AX_SAFE_RELEASE(programState);
|
2019-11-23 20:27:39 +08:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ShaderMultiTexture::changeTexture(Ref*)
|
|
|
|
{
|
2021-12-31 12:12:40 +08:00
|
|
|
static const int textureFilesCount = 3;
|
|
|
|
static const std::string textureFiles[textureFilesCount] = {"Images/grossini.png", "Images/grossinis_sister1.png",
|
|
|
|
"Images/grossinis_sister2.png"};
|
|
|
|
auto texture =
|
|
|
|
Director::getInstance()->getTextureCache()->addImage(textureFiles[_changedTextureId++ % textureFilesCount]);
|
2019-11-23 20:27:39 +08:00
|
|
|
Sprite* right = dynamic_cast<Sprite*>(getChildByTag(rightSpriteTag));
|
|
|
|
right->setTexture(texture);
|
|
|
|
auto programState = _sprite->getProgramState();
|
2022-07-05 00:39:20 +08:00
|
|
|
SET_TEXTURE(programState, "u_tex1", 1, right->getTexture()->getBackendTexture());
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|