前言
由于帧内编码基础网格时,采用EdgeBreaker编码器对基础网格顶点几何位置进行编码,会导致解码端解码出的顶点顺序与编码端不一致,不做处理的话会导致解码端解码出的MV以及位移添加到错误的位置上。
因此,在编码端编码I_BASEMESH时,会计算一个mapping,用于调整顶点顺序,使得解码端获取的MV和位移能够叠加在正确位置上。
代码
默认情况下,I_BASEMESH会调用computeDracoMapping函数计算编码前与编码后顶点索引之间的对应关系,其中会对base mesh进行无损编码,为的就是获得解码后的顶点索引顺序。为了构建编码前顶点索引与解码顶点索引之间的映射关系,会使用顶点的3维几何坐标和2维纹理坐标进行点与点之间的匹配。
bool
VMCEncoder::computeDracoMapping(TriangleMesh<MeshType> base,
std::vector<int32_t>& mapping,
const int32_t frameIndex,
const VMCEncoderParameters& params) const {
// Save intermediate files
if (params.keepBaseMesh) {
base.save(_keepFilesPathPrefix + "fr_" + std::to_string(frameIndex)
+ "_mapping_src.ply");
}
// Scale
const auto scalePosition = 1 << (18 - params.bitDepthPosition);
const auto scaleTexCoord = 1 << 18;
for (int32_t v = 0, vcount = base.pointCount(); v < vcount; ++v) {
base.setPoint(v, (1 << 18) + Round(base.point(v) * scalePosition));
}//量化几何坐标
for (int32_t tc = 0, tccount = base.texCoordCount(); tc < tccount; ++tc) {
base.setTexCoord(tc, Round(base.texCoord(tc) * scaleTexCoord));
}//量化纹理坐标
// Save intermediate files
if (params.keepBaseMesh) {
base.save(_keepFilesPathPrefix + "fr_" + std::to_string(frameIndex)
+ "_mapping_scale.ply");
}
// Encode
GeometryEncoderParameters encoderParams;
// remove this one ??
encoderParams.cl_ = 10; // this is the default for draco
// were not specified before, using scale, extend to 20bits as base values may extend beyond the original scale
encoderParams.qp_ = 20;
// in practice 19 bits are used for texcoords as 1.0 is scaled as 0x4000
// this only has incidence when qp-bits packing is used in the encoder
encoderParams.qt_ = 19;
// end remove those
encoderParams.dracoUsePosition_ = params.dracoUsePosition;
encoderParams.dracoUseUV_ = params.dracoUseUV;
encoderParams.dracoMeshLossless_ = params.dracoMeshLossless;
encoderParams.predCoder_ = params.predCoder;
encoderParams.topoCoder_ = params.topoCoder;
encoderParams.traversal_ = params.baseMeshVertexTraversal;
encoderParams.reindexOutput_ = params.motionVertexTraversal;
encoderParams.baseMeshDeduplicatePositions_ =
params.baseMeshDeduplicatePositions;
encoderParams.verbose = false;
TriangleMesh<MeshType> rec;//重建
std::vector<uint8_t> geometryBitstream;
auto encoder = GeometryEncoder<MeshType>::create(params.meshCodecId);
printf("DracoMapping: use_position = %d use_uv = %d mesh_lossless = %d \n",
encoderParams.dracoUsePosition_,
encoderParams.dracoUseUV_,
encoderParams.dracoMeshLossless_);
encoder->encode(base, encoderParams, geometryBitstream, rec);//模拟进行编解码
// Save intermediate files
if (params.keepBaseMesh) {
auto prefix =
_keepFilesPathPrefix + "fr_" + std::to_string(frameIndex) + "_mapping";
base.save(prefix + "_enc.ply");
rec.save(prefix + "_rec.ply");
save(prefix + ".drc", geometryBitstream);
}
// Geometry parametrisation base
auto fsubdiv0 = base;
fsubdiv0.subdivideMidPoint(params.liftingSubdivisionIterationCount);//先对base进行细分(包括纹理坐标)
if (params.keepBaseMesh) {
fsubdiv0.save(_keepFilesPathPrefix + "fr_" + std::to_string(frameIndex)
+ "_mapping_fsubdiv0.ply");
}
std::vector<int32_t> texCoordToPoint0;
if (fsubdiv0.texCoordCount() > 0)
fsubdiv0.computeTexCoordToPointMapping(texCoordToPoint0);//计算纹理坐标索引与顶点索引之间的对应关系 textCoordToPoint0[纹理索引] = 顶点几何索引
struct ArrayHasher {
std::size_t operator()(const std::array<double, 5>& a) const {
std::size_t h = 0;
for (auto e : a) {
h ^= std::hash<double>{}(e) + 0x9e3779b9 + (h << 6) + (h >> 2);
}
return h;
}
};
std::map<std::array<double, 5>, int32_t> map0;
const auto texCoordCount0 = fsubdiv0.texCoordCount();//纹理坐标个数
const auto coordCount0 = fsubdiv0.pointCount();//细分后顶点个数
int32_t vCount = texCoordCount0 > 0 ? texCoordCount0 : coordCount0;
for (int32_t v = 0; v < vCount; ++v) {//遍历纹理坐标个数
const auto& point0 = texCoordCount0 > 0
? fsubdiv0.point(std::max(0, texCoordToPoint0[v]))
: fsubdiv0.point(v);//顶点几何坐标(三维)
const auto& texCoord0 =
texCoordCount0 > 0 ? fsubdiv0.texCoord(v) : Vec2<MeshType>(0.0);//顶点纹理坐标(二维)
const std::array<double, 5> vertex0{
point0[0], point0[1], point0[2], texCoord0[0], texCoord0[1]};//将几何坐标和UV坐标作为索引信息
map0[vertex0] = texCoordCount0 > 0 ? texCoordToPoint0[v] : v;//mapp0中存储的是编码前对应的((顶点位置,UV坐标),顶点索引)
}
// Geometry parametrisation rec (after compression)
auto fsubdiv1 = rec;
fsubdiv1.subdivideMidPoint(params.liftingSubdivisionIterationCount);//对解码重建的base进行细分
if (params.keepBaseMesh) {
fsubdiv1.save(_keepFilesPathPrefix + "fr_" + std::to_string(frameIndex)
+ "_mapping_fsubdiv1.ply");
}
const auto pointCount1 = fsubdiv1.pointCount();//重建细分顶点个数
mapping.resize(pointCount1, -1);//mapping的size是解码后细分的顶点个数
std::vector<bool> tags(pointCount1, false);//标记是否处理过该顶点
for (int32_t t = 0, tcount = fsubdiv1.triangleCount(); t < tcount; ++t) {//遍历细分后的三角
const auto& tri = fsubdiv1.triangle(t);//三角面片
const auto& triUV =
fsubdiv1.texCoordCount() > 0 ? fsubdiv1.texCoordTriangle(t) : tri;//三个纹理坐标索引
for (int32_t k = 0; k < 3; ++k) {
const auto indexPos = tri[k];//解码后顶点索引
if (tags[indexPos]) { continue; }
tags[indexPos] = true;
const auto indexTexCoord = triUV[k];//获取顶点对应的纹理索引
const auto& point1 = fsubdiv1.point(indexPos);//获取顶点几何坐标
const auto& texCoord1 = fsubdiv1.texCoordCount() > 0
? fsubdiv1.texCoord(indexTexCoord)
: Vec2<MeshType>(0.0);//获取顶点纹理坐标
const std::array<double, 5> vertex1 = {
point1[0], point1[1], point1[2], texCoord1[0], texCoord1[1]};
const auto it = map0.find(vertex1);//在mapp0里面查找当前顶点,依据是顶点几何坐标和纹理坐标
if (it != map0.end()) {
mapping[indexPos] = map0[vertex1];//构建解码顶点索引与编码前顶点索引的映射关系
} else {
assert(0);
}
}
}
return true;
}
第一个受影响的base mesh编码,帧内由于其直接编码顶点坐标,所以其顶点索引顺序编解码端不一致不会有什么影响,但是帧间base mesh编码时,需要计算当前帧base mesh(编码前)相对于参考帧base mesh(解码后)的MV,因此涉及到顶点索引的不一致问题,需要在计算MV前,将base的顶点顺序调整成和解码端一致,这样MV计算才能对的上。
其对应的在compressBaseMesh中的代码如下:
bmsh.setSmhType(P_BASEMESH);
auto& bmsdu = bmsl.getSubmeshDataunitInter();
//TODO: update gof to reference list
//frame.referenceFrameIndex : 0~31
const auto& refFrame = gof[frame.referenceFrameIndex];
auto& mapping = frame.mapping;
auto& qpositions = frame.qpositions;
mapping = refFrame.mapping;//参考帧mapping
const auto pointCountRecBaseMesh = refFrame.base.pointCount();//参考帧重建base(解码后的顶点索引顺序)
qpositions.resize(pointCountRecBaseMesh);
for (int32_t v = 0; v < pointCountRecBaseMesh; ++v) {//遍历解码顶点索引
assert(mapping[v] >= 0 && mapping[v] < base.pointCount());
qpositions[v] = (base.point(mapping[v]) * scalePosition).round();//mapping[v]是编码端的顶点位置,所以qposition存储的顺序是按解码顺序存储的
}
base = refFrame.base;
for (int32_t v = 0, vcount = base.pointCount(); v < vcount; ++v) {
base.point(v) = qpositions[v];//将编码端顶点位置调整成解码端一致的
}
// decide to use P_BASEMESH or SKIP_BASEMESH
auto frameInfoInterOrSkipType =
chooseSkipOrInter(qpositions, refFrame.qpositions);
if (frameInfoInterOrSkipType == P_BASEMESH) {
std::cout << "\n(compressBaseMesh) FrameIndex: " << frame.frameIndex
<< " submeshIndex: " << submeshIndex
<< " submeshType: P_SUBMESH\t";
std::cout << "referenceFrameIndex : " << frame.referenceFrameIndex
<< " \n";
fflush(stdout);
compressMotion(refFrame, qpositions, bmsl, params, frame.frameIndex);
for (int32_t v = 0, vcount = base.pointCount(); v < vcount; ++v) {
base.point(v) = qpositions[v];
}
除了MV编码之外,第二个受影响的就是位移编码,因为位移本质上就是重建的细分结果(rcsubdiv)与编码端的frame.subdiv顶点之间的位置差异。由于重建细分的顶点索引会受到重建的base mesh索引顺序的影响,所以无论是I_BASEMESH还是P_BASEMESH,其重建的细分顶点顺序与编码端的frame.subdiv顶点顺序一定不一致,需要进行调整。
其对应的在compress函数中的代码如下:
if (!params.dracoMeshLossless) {
auto rsubdiv = rec;//重建的细分结果
std::swap(rsubdiv, frame.subdiv);//交换重建结果与frame.subdiv
const auto& mapping = frame.mapping;
for (int32_t v = 0, vcount = frame.subdiv.pointCount(); v < vcount;
++v) {
const auto vindex = mapping[v];//v是解码顺序,vindex是编码顺序
assert(vindex >= 0 && vindex < vcount);
frame.subdiv.setPoint(v, rsubdiv.point(vindex));
}
}