2019-11-23 20:27:39 +08:00
|
|
|
/****************************************************************************
|
|
|
|
Copyright (c) 2014-2016 Chukong Technologies Inc.
|
|
|
|
Copyright (c) 2017-2018 Xiamen Yaji Software Co., Ltd.
|
|
|
|
|
2022-10-01 16:24:52 +08:00
|
|
|
https://axmolengine.github.io/
|
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:
|
|
|
|
|
|
|
|
The above copyright notice and this permission notice shall be included in
|
|
|
|
all copies or substantial portions of the Software.
|
|
|
|
|
|
|
|
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.
|
|
|
|
****************************************************************************/
|
|
|
|
|
2023-06-11 13:08:08 +08:00
|
|
|
#include "3d/Mesh.h"
|
|
|
|
#include "3d/MeshSkin.h"
|
|
|
|
#include "3d/Skeleton3D.h"
|
|
|
|
#include "3d/MeshVertexIndexData.h"
|
|
|
|
#include "3d/VertexAttribBinding.h"
|
|
|
|
#include "2d/Light.h"
|
|
|
|
#include "2d/Scene.h"
|
|
|
|
#include "base/EventDispatcher.h"
|
|
|
|
#include "base/Director.h"
|
|
|
|
#include "base/Configuration.h"
|
|
|
|
#include "renderer/TextureCache.h"
|
|
|
|
#include "renderer/Material.h"
|
|
|
|
#include "renderer/Technique.h"
|
|
|
|
#include "renderer/Pass.h"
|
|
|
|
#include "renderer/Renderer.h"
|
2019-11-23 20:27:39 +08:00
|
|
|
#include "renderer/backend/Buffer.h"
|
|
|
|
#include "renderer/backend/Program.h"
|
2023-07-19 23:41:16 +08:00
|
|
|
#include "renderer/RenderConsts.h"
|
2019-11-23 20:27:39 +08:00
|
|
|
#include "math/Mat4.h"
|
|
|
|
|
|
|
|
using namespace std;
|
|
|
|
|
2022-07-11 17:50:21 +08:00
|
|
|
NS_AX_BEGIN
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
// Helpers
|
|
|
|
|
2021-12-25 10:04:45 +08:00
|
|
|
// sampler uniform names, only diffuse and normal texture are supported for now
|
|
|
|
std::string s_uniformSamplerName[] = {
|
|
|
|
"", // NTextureData::Usage::Unknown,
|
|
|
|
"", // NTextureData::Usage::None
|
|
|
|
"", // NTextureData::Usage::Diffuse
|
|
|
|
"", // NTextureData::Usage::Emissive
|
|
|
|
"", // NTextureData::Usage::Ambient
|
|
|
|
"", // NTextureData::Usage::Specular
|
|
|
|
"", // NTextureData::Usage::Shininess
|
|
|
|
"u_normalTex", // NTextureData::Usage::Normal
|
|
|
|
"", // NTextureData::Usage::Bump
|
|
|
|
"", // NTextureData::Usage::Transparency
|
|
|
|
"", // NTextureData::Usage::Reflection
|
2019-11-23 20:27:39 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
// helpers
|
|
|
|
void Mesh::resetLightUniformValues()
|
|
|
|
{
|
2021-12-25 10:04:45 +08:00
|
|
|
const auto& conf = Configuration::getInstance();
|
2023-07-19 23:41:16 +08:00
|
|
|
constexpr int maxDirLight = AX_MAX_DIRECTIONAL_LIGHT;
|
|
|
|
constexpr int maxPointLight = AX_MAX_POINT_LIGHT;
|
|
|
|
constexpr int maxSpotLight = AX_MAX_SPOT_LIGHT;
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
_dirLightUniformColorValues.assign(maxDirLight, Vec3::ZERO);
|
|
|
|
_dirLightUniformDirValues.assign(maxDirLight, Vec3::ZERO);
|
|
|
|
|
|
|
|
_pointLightUniformColorValues.assign(maxPointLight, Vec3::ZERO);
|
|
|
|
_pointLightUniformPositionValues.assign(maxPointLight, Vec3::ZERO);
|
|
|
|
_pointLightUniformRangeInverseValues.assign(maxPointLight, 0.0f);
|
|
|
|
|
|
|
|
_spotLightUniformColorValues.assign(maxSpotLight, Vec3::ZERO);
|
|
|
|
_spotLightUniformPositionValues.assign(maxSpotLight, Vec3::ZERO);
|
2021-12-25 10:04:45 +08:00
|
|
|
|
|
|
|
// TODO It's strange that init _spotLightUniformDirValues to zeros will cause no light effects on iPhone6 and
|
|
|
|
// iPhone6s, but works well on iPhoneX fix no light effects on iPhone6 and iPhone6s
|
2019-11-23 20:27:39 +08:00
|
|
|
_spotLightUniformDirValues.assign(maxSpotLight, Vec3(FLT_EPSILON, 0.0f, 0.0f));
|
|
|
|
_spotLightUniformInnerAngleCosValues.assign(maxSpotLight, 1.0f);
|
|
|
|
_spotLightUniformOuterAngleCosValues.assign(maxSpotLight, 0.0f);
|
|
|
|
_spotLightUniformRangeInverseValues.assign(maxSpotLight, 0.0f);
|
|
|
|
}
|
|
|
|
|
2021-12-25 10:04:45 +08:00
|
|
|
// Generate a dummy texture when the texture file is missing
|
|
|
|
static Texture2D* getDummyTexture()
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
|
|
|
auto texture = Director::getInstance()->getTextureCache()->getTextureForKey("/dummyTexture");
|
2021-12-25 10:04:45 +08:00
|
|
|
if (!texture)
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
|
|
|
#ifdef NDEBUG
|
2021-12-25 10:04:45 +08:00
|
|
|
unsigned char data[] = {0, 0, 0, 0}; // 1*1 transparent picture
|
2019-11-23 20:27:39 +08:00
|
|
|
#else
|
2021-12-25 10:04:45 +08:00
|
|
|
unsigned char data[] = {255, 0, 0, 255}; // 1*1 red picture
|
2019-11-23 20:27:39 +08:00
|
|
|
#endif
|
2021-12-25 10:04:45 +08:00
|
|
|
Image* image = new Image();
|
|
|
|
image->initWithRawData(data, sizeof(data), 1, 1, sizeof(unsigned char));
|
|
|
|
texture = Director::getInstance()->getTextureCache()->addImage(image, "/dummyTexture");
|
2019-11-23 20:27:39 +08:00
|
|
|
image->release();
|
|
|
|
}
|
|
|
|
return texture;
|
|
|
|
}
|
|
|
|
|
|
|
|
Mesh::Mesh()
|
2021-12-25 10:04:45 +08:00
|
|
|
: _skin(nullptr)
|
|
|
|
, _visible(true)
|
2022-07-05 15:42:38 +08:00
|
|
|
, meshIndexFormat(CustomCommand::IndexFormat::U_SHORT)
|
2021-12-25 10:04:45 +08:00
|
|
|
, _meshIndexData(nullptr)
|
|
|
|
, _blend(BlendFunc::ALPHA_NON_PREMULTIPLIED)
|
|
|
|
, _blendDirty(true)
|
|
|
|
, _material(nullptr)
|
|
|
|
, _texFile("")
|
|
|
|
{}
|
2019-11-23 20:27:39 +08:00
|
|
|
Mesh::~Mesh()
|
|
|
|
{
|
2022-07-21 19:19:08 +08:00
|
|
|
for (auto&& tex : _textures)
|
2021-12-25 10:04:45 +08:00
|
|
|
{
|
2022-07-16 10:43:05 +08:00
|
|
|
AX_SAFE_RELEASE(tex.second);
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
2022-07-16 10:43:05 +08:00
|
|
|
AX_SAFE_RELEASE(_skin);
|
|
|
|
AX_SAFE_RELEASE(_meshIndexData);
|
|
|
|
AX_SAFE_RELEASE(_material);
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
backend::Buffer* Mesh::getVertexBuffer() const
|
|
|
|
{
|
|
|
|
return _meshIndexData->getVertexBuffer();
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Mesh::hasVertexAttrib(shaderinfos::VertexKey attrib) const
|
|
|
|
{
|
|
|
|
return _meshIndexData->getMeshVertexData()->hasVertexAttrib(attrib);
|
|
|
|
}
|
|
|
|
|
|
|
|
ssize_t Mesh::getMeshVertexAttribCount() const
|
|
|
|
{
|
|
|
|
return _meshIndexData->getMeshVertexData()->getMeshVertexAttribCount();
|
|
|
|
}
|
|
|
|
|
|
|
|
const MeshVertexAttrib& Mesh::getMeshVertexAttribute(int idx)
|
|
|
|
{
|
|
|
|
return _meshIndexData->getMeshVertexData()->getMeshVertexAttrib(idx);
|
|
|
|
}
|
|
|
|
|
|
|
|
int Mesh::getVertexSizeInBytes() const
|
|
|
|
{
|
2022-11-20 08:58:34 +08:00
|
|
|
return static_cast<int>(_meshIndexData->getMeshVertexData()->getSizePerVertex());
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
|
|
|
|
2021-12-25 10:04:45 +08:00
|
|
|
Mesh* Mesh::create(const std::vector<float>& positions,
|
|
|
|
const std::vector<float>& normals,
|
|
|
|
const std::vector<float>& texs,
|
2022-07-03 21:17:49 +08:00
|
|
|
const IndexArray& indices)
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
|
|
|
int perVertexSizeInFloat = 0;
|
|
|
|
std::vector<float> vertices;
|
|
|
|
std::vector<MeshVertexAttrib> attribs;
|
2022-10-13 21:44:42 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
MeshVertexAttrib att;
|
|
|
|
att.type = backend::VertexFormat::FLOAT3;
|
2021-12-25 10:04:45 +08:00
|
|
|
|
2022-10-13 21:44:42 +08:00
|
|
|
attribs.reserve(3);
|
|
|
|
|
|
|
|
size_t hasNormal = 0;
|
|
|
|
size_t hasTexCoord = 0;
|
|
|
|
if (!positions.empty())
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
|
|
|
perVertexSizeInFloat += 3;
|
|
|
|
att.vertexAttrib = shaderinfos::VertexKey::VERTEX_ATTRIB_POSITION;
|
2022-08-08 13:18:33 +08:00
|
|
|
attribs.emplace_back(att);
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
2022-10-13 21:44:42 +08:00
|
|
|
if (!normals.empty())
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
|
|
|
perVertexSizeInFloat += 3;
|
|
|
|
att.vertexAttrib = shaderinfos::VertexKey::VERTEX_ATTRIB_NORMAL;
|
2022-08-08 13:18:33 +08:00
|
|
|
attribs.emplace_back(att);
|
2022-10-13 21:44:42 +08:00
|
|
|
hasNormal = 1;
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
2022-10-13 21:44:42 +08:00
|
|
|
if (!texs.empty())
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
|
|
|
perVertexSizeInFloat += 2;
|
2021-12-25 10:04:45 +08:00
|
|
|
att.type = backend::VertexFormat::FLOAT2;
|
2019-11-23 20:27:39 +08:00
|
|
|
att.vertexAttrib = shaderinfos::VertexKey::VERTEX_ATTRIB_TEX_COORD;
|
2022-08-08 13:18:33 +08:00
|
|
|
attribs.emplace_back(att);
|
2022-10-13 21:44:42 +08:00
|
|
|
hasTexCoord = 1;
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
2021-12-25 10:04:45 +08:00
|
|
|
|
|
|
|
// position, normal, texCoordinate into _vertexs
|
2019-11-23 20:27:39 +08:00
|
|
|
size_t vertexNum = positions.size() / 3;
|
2022-10-13 21:44:42 +08:00
|
|
|
vertices.reserve(positions.size() + hasNormal * 3 + hasTexCoord * 2);
|
2021-12-25 10:04:45 +08:00
|
|
|
for (size_t i = 0; i < vertexNum; i++)
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
2022-08-08 13:18:33 +08:00
|
|
|
vertices.emplace_back(positions[i * 3]);
|
|
|
|
vertices.emplace_back(positions[i * 3 + 1]);
|
|
|
|
vertices.emplace_back(positions[i * 3 + 2]);
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
if (hasNormal)
|
|
|
|
{
|
2022-08-08 13:18:33 +08:00
|
|
|
vertices.emplace_back(normals[i * 3]);
|
|
|
|
vertices.emplace_back(normals[i * 3 + 1]);
|
|
|
|
vertices.emplace_back(normals[i * 3 + 2]);
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
2021-12-25 10:04:45 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
if (hasTexCoord)
|
|
|
|
{
|
2022-08-08 13:18:33 +08:00
|
|
|
vertices.emplace_back(texs[i * 2]);
|
|
|
|
vertices.emplace_back(texs[i * 2 + 1]);
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
|
|
|
}
|
2022-07-03 21:17:49 +08:00
|
|
|
return create(vertices, perVertexSizeInFloat, indices, attribs);
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
|
|
|
|
2021-12-25 10:04:45 +08:00
|
|
|
Mesh* Mesh::create(const std::vector<float>& vertices,
|
|
|
|
int /*perVertexSizeInFloat*/,
|
|
|
|
const IndexArray& indices,
|
2022-07-03 21:17:49 +08:00
|
|
|
const std::vector<MeshVertexAttrib>& attribs)
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
|
|
|
MeshData meshdata;
|
|
|
|
meshdata.attribs = attribs;
|
2021-12-25 10:04:45 +08:00
|
|
|
meshdata.vertex = vertices;
|
2022-08-08 13:18:33 +08:00
|
|
|
meshdata.subMeshIndices.emplace_back(indices);
|
|
|
|
meshdata.subMeshIds.emplace_back("");
|
2022-07-05 15:42:38 +08:00
|
|
|
auto meshvertexdata = MeshVertexData::create(meshdata, indices.format());
|
2021-12-25 10:04:45 +08:00
|
|
|
auto indexData = meshvertexdata->getMeshIndexDataByIndex(0);
|
|
|
|
|
2022-07-05 15:42:38 +08:00
|
|
|
auto mesh = create("", indexData);
|
|
|
|
mesh->setIndexFormat(indices.format());
|
|
|
|
|
|
|
|
return mesh;
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
|
|
|
|
2021-12-31 12:12:40 +08:00
|
|
|
Mesh* Mesh::create(std::string_view name, MeshIndexData* indexData, MeshSkin* skin)
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
2021-12-08 00:11:53 +08:00
|
|
|
auto state = new Mesh();
|
2019-11-23 20:27:39 +08:00
|
|
|
state->autorelease();
|
|
|
|
state->bindMeshCommand();
|
|
|
|
state->_name = name;
|
|
|
|
state->setMeshIndexData(indexData);
|
|
|
|
state->setSkin(skin);
|
2021-12-25 10:04:45 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Mesh::setVisible(bool visible)
|
|
|
|
{
|
|
|
|
if (_visible != visible)
|
|
|
|
{
|
|
|
|
_visible = visible;
|
|
|
|
if (_visibleChanged)
|
|
|
|
_visibleChanged();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Mesh::isVisible() const
|
|
|
|
{
|
|
|
|
return _visible;
|
|
|
|
}
|
|
|
|
|
2021-12-31 12:12:40 +08:00
|
|
|
void Mesh::setTexture(std::string_view texPath)
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
|
|
|
_texFile = texPath;
|
|
|
|
auto tex = Director::getInstance()->getTextureCache()->addImage(texPath);
|
|
|
|
setTexture(tex, NTextureData::Usage::Diffuse);
|
|
|
|
}
|
|
|
|
|
|
|
|
void Mesh::setTexture(Texture2D* tex)
|
|
|
|
{
|
|
|
|
setTexture(tex, NTextureData::Usage::Diffuse);
|
|
|
|
}
|
|
|
|
|
|
|
|
void Mesh::setTexture(Texture2D* tex, NTextureData::Usage usage, bool cacheFileName)
|
|
|
|
{
|
|
|
|
// Texture must be saved for future use
|
|
|
|
// it doesn't matter if the material is already set or not
|
|
|
|
// This functionality is added for compatibility issues
|
|
|
|
if (tex == nullptr)
|
|
|
|
tex = getDummyTexture();
|
2021-12-25 10:04:45 +08:00
|
|
|
|
2022-07-16 10:43:05 +08:00
|
|
|
AX_SAFE_RETAIN(tex);
|
|
|
|
AX_SAFE_RELEASE(_textures[usage]);
|
2021-12-25 10:04:45 +08:00
|
|
|
_textures[usage] = tex;
|
|
|
|
|
|
|
|
if (usage == NTextureData::Usage::Diffuse)
|
|
|
|
{
|
|
|
|
if (_material)
|
|
|
|
{
|
2019-11-23 20:27:39 +08:00
|
|
|
auto technique = _material->_currentTechnique;
|
2022-07-21 19:19:08 +08:00
|
|
|
for (auto&& pass : technique->_passes)
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
|
|
|
pass->setUniformTexture(0, tex->getBackendTexture());
|
|
|
|
}
|
|
|
|
}
|
2021-12-25 10:04:45 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
bindMeshCommand();
|
|
|
|
if (cacheFileName)
|
|
|
|
_texFile = tex->getPath();
|
|
|
|
}
|
2021-12-25 10:04:45 +08:00
|
|
|
else if (usage == NTextureData::Usage::Normal) // currently only diffuse and normal are supported
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
2021-12-25 10:04:45 +08:00
|
|
|
if (_material)
|
|
|
|
{
|
2019-11-23 20:27:39 +08:00
|
|
|
auto technique = _material->_currentTechnique;
|
2022-07-21 19:19:08 +08:00
|
|
|
for (auto&& pass : technique->_passes)
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
|
|
|
pass->setUniformNormTexture(1, tex->getBackendTexture());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-31 12:12:40 +08:00
|
|
|
void Mesh::setTexture(std::string_view texPath, NTextureData::Usage usage)
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
|
|
|
auto tex = Director::getInstance()->getTextureCache()->addImage(texPath);
|
|
|
|
setTexture(tex, usage);
|
|
|
|
}
|
|
|
|
|
|
|
|
Texture2D* Mesh::getTexture() const
|
|
|
|
{
|
|
|
|
return _textures.at(NTextureData::Usage::Diffuse);
|
|
|
|
}
|
|
|
|
|
|
|
|
Texture2D* Mesh::getTexture(NTextureData::Usage usage)
|
|
|
|
{
|
|
|
|
return _textures[usage];
|
|
|
|
}
|
|
|
|
|
|
|
|
void Mesh::setMaterial(Material* material)
|
|
|
|
{
|
2021-12-25 10:04:45 +08:00
|
|
|
if (_material != material)
|
|
|
|
{
|
2022-07-16 10:43:05 +08:00
|
|
|
AX_SAFE_RELEASE(_material);
|
2019-11-23 20:27:39 +08:00
|
|
|
_material = material;
|
2022-07-16 10:43:05 +08:00
|
|
|
AX_SAFE_RETAIN(_material);
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
|
|
|
_meshCommands.clear();
|
|
|
|
|
|
|
|
if (_material)
|
|
|
|
{
|
2022-07-21 19:19:08 +08:00
|
|
|
for (auto&& technique : _material->getTechniques())
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
2021-12-25 10:04:45 +08:00
|
|
|
// allocate MeshCommand vector for technique
|
|
|
|
// allocate MeshCommand for each pass
|
|
|
|
auto& list = _meshCommands[technique->getName()];
|
2022-10-13 21:44:42 +08:00
|
|
|
list.resize(technique->getPasses().size());
|
2021-12-25 10:04:45 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
int i = 0;
|
2022-07-21 19:19:08 +08:00
|
|
|
for (auto&& pass : technique->getPasses())
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
2022-08-08 18:02:17 +08:00
|
|
|
#ifdef _AX_DEBUG
|
2021-12-25 10:04:45 +08:00
|
|
|
// make it crashed when missing attribute data
|
|
|
|
if (_material->getTechnique()->getName().compare(technique->getName()) == 0)
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
2021-12-25 10:04:45 +08:00
|
|
|
auto program = pass->getProgramState()->getProgram();
|
2022-09-24 11:38:41 +08:00
|
|
|
auto& attributes = program->getActiveAttributes();
|
2019-11-23 20:27:39 +08:00
|
|
|
auto meshVertexData = _meshIndexData->getMeshVertexData();
|
|
|
|
auto attributeCount = meshVertexData->getMeshVertexAttribCount();
|
2022-07-16 10:43:05 +08:00
|
|
|
AXASSERT(attributes.size() <= attributeCount, "missing attribute data");
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
|
|
|
#endif
|
2021-12-25 10:04:45 +08:00
|
|
|
// TODO
|
2019-11-23 20:27:39 +08:00
|
|
|
auto vertexAttribBinding = VertexAttribBinding::create(_meshIndexData, pass, &list[i]);
|
|
|
|
pass->setVertexAttribBinding(vertexAttribBinding);
|
|
|
|
i += 1;
|
|
|
|
}
|
|
|
|
}
|
2022-07-20 07:33:45 +08:00
|
|
|
|
|
|
|
_meshIndexData->setPrimitiveType(material->getPrimitiveType());
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
|
|
|
// Was the texture set before the GLProgramState ? Set it
|
2022-07-21 19:19:08 +08:00
|
|
|
for (auto&& tex : _textures)
|
2019-11-23 20:27:39 +08:00
|
|
|
setTexture(tex.second, tex.first);
|
2021-12-25 10:04:45 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
if (_blendDirty)
|
|
|
|
setBlendFunc(_blend);
|
2021-12-25 10:04:45 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
bindMeshCommand();
|
|
|
|
}
|
|
|
|
|
|
|
|
Material* Mesh::getMaterial() const
|
|
|
|
{
|
|
|
|
return _material;
|
|
|
|
}
|
|
|
|
|
2021-12-25 10:04:45 +08:00
|
|
|
void Mesh::draw(Renderer* renderer,
|
|
|
|
float globalZOrder,
|
|
|
|
const Mat4& transform,
|
|
|
|
uint32_t flags,
|
|
|
|
unsigned int lightMask,
|
|
|
|
const Vec4& color,
|
2022-08-06 16:17:55 +08:00
|
|
|
bool forceDepthWrite,
|
|
|
|
bool wireframe)
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
2021-12-25 10:04:45 +08:00
|
|
|
if (!isVisible())
|
2019-11-23 20:27:39 +08:00
|
|
|
return;
|
|
|
|
|
2022-08-06 16:17:55 +08:00
|
|
|
bool isTransparent = (_material->isTransparent() || color.w < 1.f);
|
2021-12-25 10:04:45 +08:00
|
|
|
float globalZ = isTransparent ? 0 : globalZOrder;
|
2019-11-23 20:27:39 +08:00
|
|
|
if (isTransparent)
|
|
|
|
flags |= Node::FLAGS_RENDER_AS_3D;
|
|
|
|
|
2021-12-25 10:04:45 +08:00
|
|
|
// TODO
|
|
|
|
// _meshCommand.init(globalZ,
|
|
|
|
// _material,
|
|
|
|
// getVertexBuffer(),
|
|
|
|
// getIndexBuffer(),
|
|
|
|
// getPrimitiveType(),
|
|
|
|
// getIndexFormat(),
|
|
|
|
// getIndexCount(),
|
|
|
|
// transform,
|
|
|
|
// flags);
|
2019-11-23 20:27:39 +08:00
|
|
|
|
|
|
|
if (isTransparent && !forceDepthWrite)
|
|
|
|
_material->getStateBlock().setDepthWrite(false);
|
|
|
|
else
|
|
|
|
_material->getStateBlock().setDepthWrite(true);
|
|
|
|
|
|
|
|
// set default uniforms for Mesh
|
|
|
|
// 'u_color' and others
|
|
|
|
const auto scene = Director::getInstance()->getRunningScene();
|
2021-12-25 10:04:45 +08:00
|
|
|
auto technique = _material->_currentTechnique;
|
|
|
|
for (const auto pass : technique->_passes)
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
|
|
|
pass->setUniformColor(&color, sizeof(color));
|
|
|
|
|
|
|
|
if (_skin)
|
|
|
|
pass->setUniformMatrixPalette(_skin->getMatrixPalette(), _skin->getMatrixPaletteSizeInBytes());
|
|
|
|
|
2020-08-18 14:29:09 +08:00
|
|
|
if (scene && !scene->getLights().empty())
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
|
|
|
setLightUniforms(pass, scene, color, lightMask);
|
|
|
|
}
|
|
|
|
}
|
2021-12-25 10:04:45 +08:00
|
|
|
auto& commands = _meshCommands[technique->getName()];
|
2019-11-23 20:27:39 +08:00
|
|
|
|
2022-07-21 19:19:08 +08:00
|
|
|
for (auto&& command : commands)
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
|
|
|
command.init(globalZ, transform);
|
|
|
|
command.setSkipBatching(isTransparent);
|
|
|
|
command.setTransparent(isTransparent);
|
2022-08-06 16:17:55 +08:00
|
|
|
command.set3D(!_material->isForce2DQueue());
|
|
|
|
command.setWireframe(wireframe);
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
|
|
|
|
2022-07-20 07:33:45 +08:00
|
|
|
_meshIndexData->setPrimitiveType(_material->_drawPrimitive);
|
2021-12-25 10:04:45 +08:00
|
|
|
_material->draw(commands.data(), globalZ, getVertexBuffer(), getIndexBuffer(), getPrimitiveType(), getIndexFormat(),
|
2022-11-20 08:58:34 +08:00
|
|
|
static_cast<unsigned int>(getIndexCount()), transform);
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
void Mesh::setSkin(MeshSkin* skin)
|
|
|
|
{
|
|
|
|
if (_skin != skin)
|
|
|
|
{
|
2022-07-16 10:43:05 +08:00
|
|
|
AX_SAFE_RETAIN(skin);
|
|
|
|
AX_SAFE_RELEASE(_skin);
|
2019-11-23 20:27:39 +08:00
|
|
|
_skin = skin;
|
|
|
|
calculateAABB();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Mesh::setMeshIndexData(MeshIndexData* subMesh)
|
|
|
|
{
|
|
|
|
if (_meshIndexData != subMesh)
|
|
|
|
{
|
2022-07-16 10:43:05 +08:00
|
|
|
AX_SAFE_RETAIN(subMesh);
|
|
|
|
AX_SAFE_RELEASE(_meshIndexData);
|
2019-11-23 20:27:39 +08:00
|
|
|
_meshIndexData = subMesh;
|
|
|
|
calculateAABB();
|
|
|
|
bindMeshCommand();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Mesh::setProgramState(backend::ProgramState* programState)
|
|
|
|
{
|
|
|
|
auto material = Material::createWithProgramState(programState);
|
|
|
|
if (_material)
|
|
|
|
{
|
|
|
|
material->setStateBlock(_material->getStateBlock());
|
|
|
|
}
|
|
|
|
setMaterial(material);
|
|
|
|
}
|
|
|
|
|
|
|
|
backend::ProgramState* Mesh::getProgramState() const
|
|
|
|
{
|
|
|
|
return _material ? _material->_currentTechnique->_passes.at(0)->getProgramState() : nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Mesh::calculateAABB()
|
|
|
|
{
|
|
|
|
if (_meshIndexData)
|
|
|
|
{
|
|
|
|
_aabb = _meshIndexData->getAABB();
|
|
|
|
if (_skin)
|
|
|
|
{
|
2021-12-25 10:04:45 +08:00
|
|
|
// get skin root
|
2019-11-23 20:27:39 +08:00
|
|
|
Bone3D* root = nullptr;
|
|
|
|
Mat4 invBindPose;
|
|
|
|
if (_skin->_skinBones.size())
|
|
|
|
{
|
|
|
|
root = _skin->_skinBones.at(0);
|
2021-12-25 10:04:45 +08:00
|
|
|
while (root)
|
|
|
|
{
|
|
|
|
auto parent = root->getParentBone();
|
2019-11-23 20:27:39 +08:00
|
|
|
bool parentInSkinBone = false;
|
2021-12-25 10:04:45 +08:00
|
|
|
for (const auto& bone : _skin->_skinBones)
|
|
|
|
{
|
2019-11-23 20:27:39 +08:00
|
|
|
if (bone == parent)
|
|
|
|
{
|
|
|
|
parentInSkinBone = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!parentInSkinBone)
|
|
|
|
break;
|
|
|
|
root = parent;
|
|
|
|
}
|
|
|
|
}
|
2021-12-25 10:04:45 +08:00
|
|
|
|
2019-11-23 20:27:39 +08:00
|
|
|
if (root)
|
|
|
|
{
|
|
|
|
_aabb.transform(root->getWorldMat() * _skin->getInvBindPose(root));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Mesh::bindMeshCommand()
|
|
|
|
{
|
|
|
|
if (_material && _meshIndexData)
|
|
|
|
{
|
2022-10-04 10:42:21 +08:00
|
|
|
auto& stateBlock = _material->getStateBlock();
|
|
|
|
stateBlock.setCullFace(true);
|
|
|
|
stateBlock.setDepthTest(true);
|
|
|
|
if (_blend.src != backend::BlendFactor::ONE && _blend.dst != backend::BlendFactor::ONE)
|
|
|
|
stateBlock.setBlend(true);
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Mesh::setLightUniforms(Pass* pass, Scene* scene, const Vec4& color, unsigned int lightmask)
|
|
|
|
{
|
2022-07-16 10:43:05 +08:00
|
|
|
AXASSERT(pass, "Invalid Pass");
|
|
|
|
AXASSERT(scene, "Invalid scene");
|
2019-11-23 20:27:39 +08:00
|
|
|
|
2021-12-25 10:04:45 +08:00
|
|
|
const auto& conf = Configuration::getInstance();
|
2023-07-19 23:41:16 +08:00
|
|
|
constexpr int maxDirLight = AX_MAX_DIRECTIONAL_LIGHT;
|
|
|
|
constexpr int maxPointLight = AX_MAX_POINT_LIGHT;
|
|
|
|
constexpr int maxSpotLight = AX_MAX_SPOT_LIGHT;
|
2021-12-25 10:04:45 +08:00
|
|
|
auto& lights = scene->getLights();
|
2019-11-23 20:27:39 +08:00
|
|
|
|
2021-12-25 10:04:45 +08:00
|
|
|
auto bindings = pass->getVertexAttributeBinding();
|
2019-11-23 20:27:39 +08:00
|
|
|
|
2021-12-25 10:04:45 +08:00
|
|
|
if (bindings && bindings->hasAttribute(shaderinfos::VertexKey::VERTEX_ATTRIB_NORMAL))
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
|
|
|
resetLightUniformValues();
|
|
|
|
|
2021-12-25 10:04:45 +08:00
|
|
|
int enabledDirLightNum = 0;
|
2019-11-23 20:27:39 +08:00
|
|
|
int enabledPointLightNum = 0;
|
2021-12-25 10:04:45 +08:00
|
|
|
int enabledSpotLightNum = 0;
|
2019-11-23 20:27:39 +08:00
|
|
|
Vec3 ambientColor;
|
|
|
|
for (const auto& light : lights)
|
|
|
|
{
|
|
|
|
bool useLight = light->isEnabled() && ((unsigned int)light->getLightFlag() & lightmask);
|
|
|
|
if (useLight)
|
|
|
|
{
|
|
|
|
float intensity = light->getIntensity();
|
|
|
|
switch (light->getLightType())
|
|
|
|
{
|
2021-12-25 10:04:45 +08:00
|
|
|
case LightType::DIRECTIONAL:
|
|
|
|
{
|
|
|
|
if (enabledDirLightNum < maxDirLight)
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
2021-12-25 10:04:45 +08:00
|
|
|
auto dirLight = static_cast<DirectionLight*>(light);
|
|
|
|
Vec3 dir = dirLight->getDirectionInWorld();
|
|
|
|
dir.normalize();
|
|
|
|
const Color3B& col = dirLight->getDisplayedColor();
|
|
|
|
_dirLightUniformColorValues[enabledDirLightNum].set(
|
|
|
|
col.r / 255.0f * intensity, col.g / 255.0f * intensity, col.b / 255.0f * intensity);
|
|
|
|
_dirLightUniformDirValues[enabledDirLightNum] = dir;
|
|
|
|
++enabledDirLightNum;
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
2021-12-25 10:04:45 +08:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case LightType::POINT:
|
|
|
|
{
|
|
|
|
if (enabledPointLightNum < maxPointLight)
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
2021-12-25 10:04:45 +08:00
|
|
|
auto pointLight = static_cast<PointLight*>(light);
|
|
|
|
Mat4 mat = pointLight->getNodeToWorldTransform();
|
|
|
|
const Color3B& col = pointLight->getDisplayedColor();
|
|
|
|
_pointLightUniformColorValues[enabledPointLightNum].set(
|
|
|
|
col.r / 255.0f * intensity, col.g / 255.0f * intensity, col.b / 255.0f * intensity);
|
|
|
|
_pointLightUniformPositionValues[enabledPointLightNum].set(mat.m[12], mat.m[13], mat.m[14]);
|
|
|
|
_pointLightUniformRangeInverseValues[enabledPointLightNum] = 1.0f / pointLight->getRange();
|
|
|
|
++enabledPointLightNum;
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
2021-12-25 10:04:45 +08:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case LightType::SPOT:
|
|
|
|
{
|
|
|
|
if (enabledSpotLightNum < maxSpotLight)
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
2021-12-25 10:04:45 +08:00
|
|
|
auto spotLight = static_cast<SpotLight*>(light);
|
|
|
|
Vec3 dir = spotLight->getDirectionInWorld();
|
|
|
|
dir.normalize();
|
|
|
|
Mat4 mat = light->getNodeToWorldTransform();
|
|
|
|
const Color3B& col = spotLight->getDisplayedColor();
|
|
|
|
_spotLightUniformColorValues[enabledSpotLightNum].set(
|
|
|
|
col.r / 255.0f * intensity, col.g / 255.0f * intensity, col.b / 255.0f * intensity);
|
|
|
|
_spotLightUniformPositionValues[enabledSpotLightNum].set(mat.m[12], mat.m[13], mat.m[14]);
|
|
|
|
_spotLightUniformDirValues[enabledSpotLightNum] = dir;
|
|
|
|
_spotLightUniformInnerAngleCosValues[enabledSpotLightNum] = spotLight->getCosInnerAngle();
|
|
|
|
_spotLightUniformOuterAngleCosValues[enabledSpotLightNum] = spotLight->getCosOuterAngle();
|
|
|
|
_spotLightUniformRangeInverseValues[enabledSpotLightNum] = 1.0f / spotLight->getRange();
|
|
|
|
++enabledSpotLightNum;
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
2021-12-25 10:04:45 +08:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case LightType::AMBIENT:
|
|
|
|
{
|
|
|
|
auto ambLight = static_cast<AmbientLight*>(light);
|
|
|
|
const Color3B& col = ambLight->getDisplayedColor();
|
|
|
|
ambientColor.add(col.r / 255.0f * intensity, col.g / 255.0f * intensity,
|
|
|
|
col.b / 255.0f * intensity);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (0 < maxDirLight)
|
|
|
|
{
|
2021-12-25 10:04:45 +08:00
|
|
|
pass->setUniformDirLightColor(&_dirLightUniformColorValues[0],
|
|
|
|
_dirLightUniformColorValues.size() * sizeof(_dirLightUniformColorValues[0]));
|
|
|
|
pass->setUniformDirLightDir(&_dirLightUniformDirValues[0],
|
|
|
|
_dirLightUniformDirValues.size() * sizeof(_dirLightUniformDirValues[0]));
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if (0 < maxPointLight)
|
|
|
|
{
|
2021-12-25 10:04:45 +08:00
|
|
|
pass->setUniformPointLightColor(
|
|
|
|
&_pointLightUniformColorValues[0],
|
|
|
|
_pointLightUniformColorValues.size() * sizeof(_pointLightUniformColorValues[0]));
|
|
|
|
pass->setUniformPointLightPosition(
|
|
|
|
&_pointLightUniformPositionValues[0],
|
|
|
|
_pointLightUniformPositionValues.size() * sizeof(_pointLightUniformPositionValues[0]));
|
|
|
|
pass->setUniformPointLightRangeInverse(
|
|
|
|
&_pointLightUniformRangeInverseValues[0],
|
|
|
|
_pointLightUniformRangeInverseValues.size() * sizeof(_pointLightUniformRangeInverseValues[0]));
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if (0 < maxSpotLight)
|
|
|
|
{
|
2021-12-25 10:04:45 +08:00
|
|
|
pass->setUniformSpotLightColor(
|
|
|
|
&_spotLightUniformColorValues[0],
|
|
|
|
_spotLightUniformColorValues.size() * sizeof(_spotLightUniformColorValues[0]));
|
|
|
|
pass->setUniformSpotLightPosition(
|
|
|
|
&_spotLightUniformPositionValues[0],
|
|
|
|
_spotLightUniformPositionValues.size() * sizeof(_spotLightUniformPositionValues[0]));
|
|
|
|
pass->setUniformSpotLightDir(&_spotLightUniformDirValues[0],
|
|
|
|
_spotLightUniformDirValues.size() * sizeof(_spotLightUniformDirValues[0]));
|
|
|
|
pass->setUniformSpotLightInnerAngleCos(
|
|
|
|
&_spotLightUniformInnerAngleCosValues[0],
|
|
|
|
_spotLightUniformInnerAngleCosValues.size() * sizeof(_spotLightUniformInnerAngleCosValues[0]));
|
|
|
|
pass->setUniformSpotLightOuterAngleCos(
|
|
|
|
&_spotLightUniformOuterAngleCosValues[0],
|
|
|
|
_spotLightUniformOuterAngleCosValues.size() * sizeof(_spotLightUniformOuterAngleCosValues[0]));
|
|
|
|
pass->setUniformSpotLightRangeInverse(
|
|
|
|
&_spotLightUniformRangeInverseValues[0],
|
|
|
|
_spotLightUniformRangeInverseValues.size() * sizeof(_spotLightUniformRangeInverseValues[0]));
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
auto ambientLightColor = Vec3(ambientColor.x, ambientColor.y, ambientColor.z);
|
|
|
|
pass->setUniformAmbientLigthColor(&ambientLightColor, sizeof(ambientLightColor));
|
|
|
|
}
|
2021-12-25 10:04:45 +08:00
|
|
|
else // normal does not exist
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
|
|
|
Vec3 ambient(0.0f, 0.0f, 0.0f);
|
|
|
|
bool hasAmbient = false;
|
|
|
|
for (const auto& light : lights)
|
|
|
|
{
|
|
|
|
if (light->getLightType() == LightType::AMBIENT)
|
|
|
|
{
|
|
|
|
bool useLight = light->isEnabled() && ((unsigned int)light->getLightFlag() & lightmask);
|
|
|
|
if (useLight)
|
|
|
|
{
|
2021-12-25 10:04:45 +08:00
|
|
|
hasAmbient = true;
|
|
|
|
const Color3B& col = light->getDisplayedColor();
|
2019-11-23 20:27:39 +08:00
|
|
|
ambient.x += col.r * light->getIntensity();
|
|
|
|
ambient.y += col.g * light->getIntensity();
|
|
|
|
ambient.z += col.b * light->getIntensity();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (hasAmbient)
|
|
|
|
{
|
2021-12-25 10:04:45 +08:00
|
|
|
ambient.x /= 255.f;
|
|
|
|
ambient.y /= 255.f;
|
|
|
|
ambient.z /= 255.f;
|
|
|
|
// override the uniform value of u_color using the calculated color
|
2019-11-23 20:27:39 +08:00
|
|
|
auto fcolor = Vec4(color.x * ambient.x, color.y * ambient.y, color.z * ambient.z, color.w);
|
|
|
|
pass->setUniformColor(&fcolor, sizeof(fcolor));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-25 10:04:45 +08:00
|
|
|
void Mesh::setBlendFunc(const BlendFunc& blendFunc)
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
|
|
|
// Blend must be saved for future use
|
|
|
|
// it doesn't matter if the material is already set or not
|
|
|
|
// This functionality is added for compatibility issues
|
2021-12-25 10:04:45 +08:00
|
|
|
if (_blend != blendFunc)
|
2019-11-23 20:27:39 +08:00
|
|
|
{
|
|
|
|
_blendDirty = true;
|
2021-12-25 10:04:45 +08:00
|
|
|
_blend = blendFunc;
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
|
|
|
|
2021-12-25 10:04:45 +08:00
|
|
|
if (_material)
|
|
|
|
{
|
|
|
|
// TODO set blend to Pass
|
2019-11-23 20:27:39 +08:00
|
|
|
_material->getStateBlock().setBlendFunc(blendFunc);
|
|
|
|
bindMeshCommand();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const BlendFunc& Mesh::getBlendFunc() const
|
|
|
|
{
|
2021-12-25 10:04:45 +08:00
|
|
|
// return _material->_currentTechnique->_passes.at(0)->getBlendFunc();
|
2019-11-23 20:27:39 +08:00
|
|
|
return _blend;
|
|
|
|
}
|
|
|
|
|
|
|
|
CustomCommand::PrimitiveType Mesh::getPrimitiveType() const
|
|
|
|
{
|
|
|
|
return _meshIndexData->getPrimitiveType();
|
|
|
|
}
|
|
|
|
|
|
|
|
ssize_t Mesh::getIndexCount() const
|
|
|
|
{
|
2022-07-05 15:42:38 +08:00
|
|
|
return _meshIndexData->getIndexBuffer()->getSize() / IndexArray::formatToStride(meshIndexFormat);
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
CustomCommand::IndexFormat Mesh::getIndexFormat() const
|
|
|
|
{
|
2022-07-05 15:42:38 +08:00
|
|
|
return meshIndexFormat;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Mesh::setIndexFormat(CustomCommand::IndexFormat indexFormat)
|
|
|
|
{
|
|
|
|
meshIndexFormat = indexFormat;
|
2019-11-23 20:27:39 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
backend::Buffer* Mesh::getIndexBuffer() const
|
|
|
|
{
|
|
|
|
return _meshIndexData->getIndexBuffer();
|
|
|
|
}
|
2022-07-11 17:50:21 +08:00
|
|
|
NS_AX_END
|