|
|
@@ -3,6 +3,8 @@
|
|
|
#include "BsTextSprite.h"
|
|
|
#include "BsVector2.h"
|
|
|
#include "BsTexture.h"
|
|
|
+#include "BsPlane.h"
|
|
|
+#include "BsRect2.h"
|
|
|
|
|
|
namespace BansheeEngine
|
|
|
{
|
|
|
@@ -91,7 +93,7 @@ namespace BansheeEngine
|
|
|
memcpy(vertDst, &renderElem.vertices[vertIdx + 3], sizeof(Vector2));
|
|
|
memcpy(uvDst, &renderElem.uvs[vertIdx + 3], sizeof(Vector2));
|
|
|
|
|
|
- clipToRect(vecStart, uvStart, 1, vertexStride, clipRect);
|
|
|
+ clipQuadsToRect(vecStart, uvStart, 1, vertexStride, clipRect);
|
|
|
|
|
|
vertDst = vecStart;
|
|
|
Vector2* curVec = (Vector2*)vertDst;
|
|
|
@@ -240,7 +242,7 @@ namespace BansheeEngine
|
|
|
// This will only properly clip an array of quads
|
|
|
// Vertices in the quad must be in a specific order: top left, top right, bottom left, bottom right
|
|
|
// (0, 0) represents top left of the screen
|
|
|
- void Sprite::clipToRect(UINT8* vertices, UINT8* uv, UINT32 numQuads, UINT32 vertStride, const Rect2I& clipRect)
|
|
|
+ void Sprite::clipQuadsToRect(UINT8* vertices, UINT8* uv, UINT32 numQuads, UINT32 vertStride, const Rect2I& clipRect)
|
|
|
{
|
|
|
float left = (float)clipRect.x;
|
|
|
float right = (float)clipRect.x + clipRect.width;
|
|
|
@@ -316,6 +318,483 @@ namespace BansheeEngine
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // Implementation from: http://www.geometrictools.com/Documentation/ClipMesh.pdf
|
|
|
+ class TriangleClipper
|
|
|
+ {
|
|
|
+ private:
|
|
|
+ struct ClipVert
|
|
|
+ {
|
|
|
+ ClipVert() { }
|
|
|
+
|
|
|
+ Vector3 point;
|
|
|
+ Vector2 uv;
|
|
|
+ float distance = 0.0f;
|
|
|
+ UINT32 occurs = 0;
|
|
|
+ bool visible = true;
|
|
|
+ };
|
|
|
+
|
|
|
+ struct ClipEdge
|
|
|
+ {
|
|
|
+ ClipEdge() { }
|
|
|
+
|
|
|
+ UINT32 verts[2];
|
|
|
+ Vector<UINT32> faces;
|
|
|
+ bool visible = true;
|
|
|
+ };
|
|
|
+
|
|
|
+ struct ClipFace
|
|
|
+ {
|
|
|
+ ClipFace() { }
|
|
|
+
|
|
|
+ Vector<UINT32> edges;
|
|
|
+ bool visible = true;
|
|
|
+ Vector3 normal;
|
|
|
+ };
|
|
|
+
|
|
|
+ struct ClipMesh
|
|
|
+ {
|
|
|
+ ClipMesh() { }
|
|
|
+
|
|
|
+ Vector<ClipVert> verts;
|
|
|
+ Vector<ClipEdge> edges;
|
|
|
+ Vector<ClipFace> faces;
|
|
|
+ };
|
|
|
+
|
|
|
+ public:
|
|
|
+ void clip(UINT8* vertices, UINT8* uvs, UINT32 numTris, UINT32 vertexStride, const Rect2& clipRect,
|
|
|
+ Vector<Vector3>& clippedVertices, Vector<Vector2>& clippedUvs); // TODO - Use write callback instead of vectors for output
|
|
|
+
|
|
|
+ private:
|
|
|
+ INT32 clipByPlane(const Plane& plane);
|
|
|
+ INT32 processVertices(const Plane& plane);
|
|
|
+ void processEdges();
|
|
|
+ void processFaces();
|
|
|
+ void convertToMesh(Vector<Vector3>& vertices, Vector<Vector2>& uvs);
|
|
|
+ void getOrderedFaces(FrameVector<FrameVector<UINT32>>& sortedFaces);
|
|
|
+ void getOrderedVertices(ClipFace face, UINT32* vertices);
|
|
|
+ Vector3 getNormal(UINT32* sortedVertices, UINT32 numVertices);
|
|
|
+ bool getOpenPolyline(ClipFace& face, UINT32& start, UINT32& end);
|
|
|
+
|
|
|
+ ClipMesh mesh;
|
|
|
+ };
|
|
|
+
|
|
|
+ void TriangleClipper::clip(UINT8* vertices, UINT8* uvs, UINT32 numTris, UINT32 vertexStride, const Rect2& clipRect,
|
|
|
+ Vector<Vector3>& clippedVertices, Vector<Vector2>& clippedUvs)
|
|
|
+ {
|
|
|
+ // Add vertices
|
|
|
+ UINT32 numVertices = numTris * 3;
|
|
|
+ mesh.verts.resize(numVertices);
|
|
|
+
|
|
|
+ for (UINT32 i = 0; i < numVertices; i++)
|
|
|
+ {
|
|
|
+ ClipVert& clipVert = mesh.verts[i];
|
|
|
+ clipVert.point = *(Vector3*)(vertices + vertexStride * i);
|
|
|
+ clipVert.uv = *(Vector2*)(uvs + vertexStride * i);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Add edges & faces
|
|
|
+ UINT32 numEdges = numTris * 3;
|
|
|
+ mesh.edges.resize(numEdges);
|
|
|
+ mesh.faces.resize(numTris);
|
|
|
+
|
|
|
+ for (UINT32 i = 0; i < numTris; i++)
|
|
|
+ {
|
|
|
+ UINT32 idx0 = i * 3 + 0;
|
|
|
+ UINT32 idx1 = i * 3 + 1;
|
|
|
+ UINT32 idx2 = i * 3 + 2;
|
|
|
+
|
|
|
+ ClipEdge& clipEdge0 = mesh.edges[idx0];
|
|
|
+ clipEdge0.verts[0] = idx0;
|
|
|
+ clipEdge0.verts[1] = idx1;
|
|
|
+
|
|
|
+ ClipEdge& clipEdge1 = mesh.edges[idx1];
|
|
|
+ clipEdge0.verts[0] = idx1;
|
|
|
+ clipEdge0.verts[1] = idx2;
|
|
|
+
|
|
|
+ ClipEdge& clipEdge2 = mesh.edges[idx2];
|
|
|
+ clipEdge0.verts[0] = idx2;
|
|
|
+ clipEdge0.verts[1] = idx0;
|
|
|
+
|
|
|
+ ClipFace& clipFace = mesh.faces[i];
|
|
|
+
|
|
|
+ clipFace.edges.push_back(idx0);
|
|
|
+ clipFace.edges.push_back(idx1);
|
|
|
+ clipFace.edges.push_back(idx2);
|
|
|
+
|
|
|
+ clipEdge0.faces.push_back(i);
|
|
|
+ clipEdge1.faces.push_back(i);
|
|
|
+ clipEdge2.faces.push_back(i);
|
|
|
+
|
|
|
+ UINT32 verts[] = { idx0, idx1, idx2, idx0 };
|
|
|
+ for (UINT32 j = 0; j < 3; j++)
|
|
|
+ clipFace.normal += Vector3::cross(mesh.verts[verts[j]].point, mesh.verts[verts[j + 1]].point);
|
|
|
+
|
|
|
+ clipFace.normal.normalize();
|
|
|
+
|
|
|
+ mesh.faces.push_back(clipFace);
|
|
|
+ }
|
|
|
+
|
|
|
+ Plane clipPlanes[] =
|
|
|
+ {
|
|
|
+ { Vector3(1, 0, 0), clipRect.x },
|
|
|
+ { Vector3(-1, 0, 0), -(clipRect.x + clipRect.width) },
|
|
|
+ { Vector3(0, 1, 0), (clipRect.y + clipRect.height) },
|
|
|
+ { Vector3(0, -1, 0), -clipRect.y }
|
|
|
+ };
|
|
|
+
|
|
|
+ for (int i = 0; i < 4; i++)
|
|
|
+ clipByPlane(clipPlanes[i]);
|
|
|
+
|
|
|
+ convertToMesh(clippedVertices, clippedUvs);
|
|
|
+ }
|
|
|
+
|
|
|
+ INT32 TriangleClipper::clipByPlane(const Plane& plane)
|
|
|
+ {
|
|
|
+ int state = processVertices(plane);
|
|
|
+
|
|
|
+ if (state == 1)
|
|
|
+ return +1; // Nothing is clipped
|
|
|
+ else if (state == -1)
|
|
|
+ return -1; // Everything is clipped
|
|
|
+
|
|
|
+ processEdges();
|
|
|
+ processFaces();
|
|
|
+
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ INT32 TriangleClipper::processVertices(const Plane& plane)
|
|
|
+ {
|
|
|
+ static const float EPSILON = 0.00001f;
|
|
|
+
|
|
|
+ // Compute signed distances from vertices to plane
|
|
|
+ int positive = 0, negative = 0;
|
|
|
+ for (UINT32 i = 0; i < (UINT32)mesh.verts.size(); i++)
|
|
|
+ {
|
|
|
+ ClipVert& vertex = mesh.verts[i];
|
|
|
+
|
|
|
+ if (vertex.visible)
|
|
|
+ {
|
|
|
+ vertex.distance = Vector3::dot(plane.normal, vertex.point) - plane.d;
|
|
|
+ if (vertex.distance >= EPSILON)
|
|
|
+ {
|
|
|
+ positive++;
|
|
|
+ }
|
|
|
+ else if (vertex.distance <= -EPSILON)
|
|
|
+ {
|
|
|
+ negative++;
|
|
|
+ vertex.visible = false;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // Point on the plane within floating point tolerance
|
|
|
+ vertex.distance = 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (negative == 0)
|
|
|
+ {
|
|
|
+ // All vertices on nonnegative side, no clipping
|
|
|
+ return +1;
|
|
|
+ }
|
|
|
+ if (positive == 0)
|
|
|
+ {
|
|
|
+ // All vertices on nonpositive side, everything clipped
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ void TriangleClipper::processEdges()
|
|
|
+ {
|
|
|
+ for (INT32 i = 0; i < (UINT32)mesh.edges.size(); i++)
|
|
|
+ {
|
|
|
+ ClipEdge& edge = mesh.edges[i];
|
|
|
+
|
|
|
+ if (edge.visible)
|
|
|
+ {
|
|
|
+ const ClipVert& v0 = mesh.verts[edge.verts[0]];
|
|
|
+ const ClipVert& v1 = mesh.verts[edge.verts[1]];
|
|
|
+
|
|
|
+ float d0 = v0.distance;
|
|
|
+ float d1 = v1.distance;
|
|
|
+
|
|
|
+ if (d0 <= 0 && d1 <= 0)
|
|
|
+ {
|
|
|
+ // Edge is culled, remove edge from faces sharing it
|
|
|
+ for (UINT32 j = 0; j < (UINT32)edge.faces.size(); j++)
|
|
|
+ {
|
|
|
+ ClipFace& face = mesh.faces[edge.faces[j]];
|
|
|
+
|
|
|
+ auto iterFind = std::find(face.edges.begin(), face.edges.end(), i);
|
|
|
+ if (iterFind != face.edges.end())
|
|
|
+ {
|
|
|
+ face.edges.erase(iterFind);
|
|
|
+
|
|
|
+ if (face.edges.empty())
|
|
|
+ face.visible = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ edge.visible = false;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (d0 >= 0 && d1 >= 0)
|
|
|
+ {
|
|
|
+ // Edge is on nonnegative side, faces retain the edge
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // The edge is split by the plane. Compute the point of intersection.
|
|
|
+ // If the old edge is <V0,V1> and I is the intersection point, the new
|
|
|
+ // edge is <V0,I> when d0 > 0 or <I,V1> when d1 > 0.
|
|
|
+ float t = d0 / (d0 - d1);
|
|
|
+ Vector3 intersectPt = (1 - t)*v0.point + t*v1.point;
|
|
|
+ Vector2 intersectUv = (1 - t)*v0.uv + t*v1.uv;
|
|
|
+
|
|
|
+ UINT32 newVertIdx = (UINT32)mesh.verts.size();
|
|
|
+ mesh.verts.push_back(ClipVert());
|
|
|
+
|
|
|
+ ClipVert& newVert = mesh.verts.back();
|
|
|
+ newVert.point = intersectPt;
|
|
|
+ newVert.uv = intersectUv;
|
|
|
+
|
|
|
+ if (d0 > 0)
|
|
|
+ mesh.edges[i].verts[1] = newVertIdx;
|
|
|
+ else
|
|
|
+ mesh.edges[i].verts[0] = newVertIdx;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ void TriangleClipper::processFaces()
|
|
|
+ {
|
|
|
+ for (UINT32 i = 0; i < (UINT32)mesh.faces.size(); i++)
|
|
|
+ {
|
|
|
+ ClipFace& face = mesh.faces[i];
|
|
|
+
|
|
|
+ if (face.visible)
|
|
|
+ {
|
|
|
+ // The edge is culled. If the edge is exactly on the clip
|
|
|
+ // plane, it is possible that a visible triangle shares it.
|
|
|
+ // The edge will be re-added during the face loop.
|
|
|
+
|
|
|
+ for (UINT32 j = 0; j < (UINT32)face.edges.size(); j++)
|
|
|
+ {
|
|
|
+ ClipEdge& edge = mesh.edges[face.edges[j]];
|
|
|
+ ClipVert& v0 = mesh.verts[edge.verts[0]];
|
|
|
+ ClipVert& v1 = mesh.verts[edge.verts[0]];
|
|
|
+
|
|
|
+ v0.occurs = 0;
|
|
|
+ v1.occurs = 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ UINT32 start, end;
|
|
|
+ if (getOpenPolyline(mesh.faces[i], start, end))
|
|
|
+ {
|
|
|
+ // Polyline is open, close it
|
|
|
+ UINT32 closeEdgeIdx = (UINT32)mesh.edges.size();
|
|
|
+ mesh.edges.push_back(ClipEdge());
|
|
|
+ ClipEdge& closeEdge = mesh.edges.back();
|
|
|
+
|
|
|
+ closeEdge.verts[0] = start;
|
|
|
+ closeEdge.verts[1] = end;
|
|
|
+
|
|
|
+ closeEdge.faces.push_back(i);
|
|
|
+ face.edges.push_back(closeEdgeIdx);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ bool TriangleClipper::getOpenPolyline(ClipFace& face, UINT32& start, UINT32& end)
|
|
|
+ {
|
|
|
+ // Count the number of occurrences of each vertex in the polyline. The
|
|
|
+ // resulting "occurs" values must be 1 or 2.
|
|
|
+ for (UINT32 i = 0; i < (UINT32)face.edges.size(); i++)
|
|
|
+ {
|
|
|
+ ClipEdge& edge = mesh.edges[face.edges[i]];
|
|
|
+
|
|
|
+ if (edge.visible)
|
|
|
+ {
|
|
|
+ ClipVert& v0 = mesh.verts[edge.verts[0]];
|
|
|
+ ClipVert& v1 = mesh.verts[edge.verts[0]];
|
|
|
+
|
|
|
+ v0.occurs++;
|
|
|
+ v1.occurs++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Determine if the polyline is open
|
|
|
+ bool gotStart = false;
|
|
|
+ bool gotEnd = false;
|
|
|
+ for (UINT32 i = 0; i < (UINT32)face.edges.size(); i++)
|
|
|
+ {
|
|
|
+ const ClipEdge& edge = mesh.edges[face.edges[i]];
|
|
|
+
|
|
|
+ const ClipVert& v0 = mesh.verts[edge.verts[0]];
|
|
|
+ const ClipVert& v1 = mesh.verts[edge.verts[1]];
|
|
|
+
|
|
|
+ if (v0.occurs == 1)
|
|
|
+ {
|
|
|
+ if (!gotStart)
|
|
|
+ {
|
|
|
+ start = edge.verts[0];
|
|
|
+ gotStart = true;
|
|
|
+ }
|
|
|
+ else if (!gotEnd)
|
|
|
+ {
|
|
|
+ end = edge.verts[0];
|
|
|
+ gotEnd = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (v1.occurs == 1)
|
|
|
+ {
|
|
|
+ if (!gotStart)
|
|
|
+ {
|
|
|
+ start = edge.verts[1];
|
|
|
+ gotStart = true;
|
|
|
+ }
|
|
|
+ else if (!gotEnd)
|
|
|
+ {
|
|
|
+ end = edge.verts[1];
|
|
|
+ gotEnd = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return gotStart;
|
|
|
+ }
|
|
|
+
|
|
|
+ void TriangleClipper::convertToMesh(Vector<Vector3>& vertices, Vector<Vector2>& uvs)
|
|
|
+ {
|
|
|
+ bs_frame_mark();
|
|
|
+ {
|
|
|
+ FrameVector<FrameVector<UINT32>> allFaces;
|
|
|
+ getOrderedFaces(allFaces);
|
|
|
+
|
|
|
+ // Note: Consider using Delaunay triangulation to avoid skinny triangles
|
|
|
+ for (auto& face : allFaces)
|
|
|
+ {
|
|
|
+ for (int i = 0; i < (UINT32)face.size() - 2; i++)
|
|
|
+ {
|
|
|
+ vertices.push_back(mesh.verts[face[0]].point);
|
|
|
+ vertices.push_back(mesh.verts[face[i + 1]].point);
|
|
|
+ vertices.push_back(mesh.verts[face[i + 2]].point);
|
|
|
+
|
|
|
+ uvs.push_back(mesh.verts[face[0]].uv);
|
|
|
+ uvs.push_back(mesh.verts[face[i + 1]].uv);
|
|
|
+ uvs.push_back(mesh.verts[face[i + 2]].uv);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ bs_frame_clear();
|
|
|
+ }
|
|
|
+
|
|
|
+ void TriangleClipper::getOrderedFaces(FrameVector<FrameVector<UINT32>>& sortedFaces)
|
|
|
+ {
|
|
|
+ for (UINT32 i = 0; i < (UINT32)mesh.faces.size(); i++)
|
|
|
+ {
|
|
|
+ const ClipFace& face = mesh.faces[i];
|
|
|
+
|
|
|
+ if (face.visible)
|
|
|
+ {
|
|
|
+ // Get the ordered vertices of the face. The first and last
|
|
|
+ // element of the array are the same since the polyline is
|
|
|
+ // closed.
|
|
|
+ UINT32 numSortedVerts = (UINT32)face.edges.size() + 1;
|
|
|
+ UINT32* sortedVerts = (UINT32*)bs_stack_alloc(sizeof(UINT32) * numSortedVerts);
|
|
|
+
|
|
|
+ getOrderedVertices(face, sortedVerts);
|
|
|
+
|
|
|
+ FrameVector<UINT32> faceVerts;
|
|
|
+
|
|
|
+ // The convention is that the vertices should be counterclockwise
|
|
|
+ // ordered when viewed from the negative side of the plane of the
|
|
|
+ // face. If you need the opposite convention, switch the
|
|
|
+ // inequality in the if-else statement.
|
|
|
+ Vector3 normal = getNormal(sortedVerts, numSortedVerts);
|
|
|
+ if (Vector3::dot(mesh.faces[i].normal, normal) < 0)
|
|
|
+ {
|
|
|
+ // Clockwise, need to swap
|
|
|
+ for (INT32 j = (INT32)numSortedVerts - 2; j >= 0; j--)
|
|
|
+ faceVerts.push_back(sortedVerts[j]);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // Counterclockwise
|
|
|
+ for (int j = 0; j <= (INT32)numSortedVerts - 2; j++)
|
|
|
+ faceVerts.push_back(sortedVerts[j]);
|
|
|
+ }
|
|
|
+
|
|
|
+ sortedFaces.push_back(faceVerts);
|
|
|
+ bs_stack_free(sortedVerts);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ void TriangleClipper::getOrderedVertices(ClipFace face, UINT32* sortedVerts)
|
|
|
+ {
|
|
|
+ UINT32 numEdges = (UINT32)face.edges.size();
|
|
|
+ UINT32* sortedEdges = (UINT32*)bs_stack_alloc(sizeof(UINT32) * numEdges);
|
|
|
+ for (UINT32 i = 0; i < numEdges; i++)
|
|
|
+ sortedEdges[i] = i;
|
|
|
+
|
|
|
+ // Bubble sort to arrange edges in contiguous order
|
|
|
+ for (UINT32 i0 = 0, i1 = 1, choice = 1; i1 < numEdges - 1; i0 = i1, i1++)
|
|
|
+ {
|
|
|
+ const ClipEdge& edge0 = mesh.edges[sortedEdges[i0]];
|
|
|
+
|
|
|
+ UINT32 current = edge0.verts[choice];
|
|
|
+ for (UINT32 j = i1; j < numEdges; j++)
|
|
|
+ {
|
|
|
+ const ClipEdge& edge1 = mesh.edges[sortedEdges[j]];
|
|
|
+
|
|
|
+ if (edge1.verts[0] == current || edge1.verts[1] == current)
|
|
|
+ {
|
|
|
+ std::swap(sortedEdges[i1], sortedEdges[j]);
|
|
|
+ choice = 1;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Add the first two vertices
|
|
|
+ sortedVerts[0] = mesh.edges[sortedEdges[0]].verts[0];
|
|
|
+ sortedVerts[1] = mesh.edges[sortedEdges[0]].verts[1];
|
|
|
+
|
|
|
+ // Add the remaining vertices
|
|
|
+ for (UINT32 i = 1; i < numEdges; i++)
|
|
|
+ {
|
|
|
+ const ClipEdge& edge = mesh.edges[sortedEdges[i]];
|
|
|
+
|
|
|
+ if (edge.verts[0] == sortedVerts[i])
|
|
|
+ sortedVerts[i + 1] = edge.verts[1];
|
|
|
+ else
|
|
|
+ sortedVerts[i + 1] = edge.verts[0];
|
|
|
+ }
|
|
|
+
|
|
|
+ bs_stack_free(sortedEdges);
|
|
|
+ }
|
|
|
+
|
|
|
+ Vector3 TriangleClipper::getNormal(UINT32* sortedVertices, UINT32 numVertices)
|
|
|
+ {
|
|
|
+ Vector3 normal;
|
|
|
+ for (UINT32 i = 0; i <= numVertices - 2; i++)
|
|
|
+ normal += Vector3::cross(mesh.verts[sortedVertices[i]].point, mesh.verts[sortedVertices[i + 1]].point);
|
|
|
+
|
|
|
+ normal.normalize();
|
|
|
+ return normal;
|
|
|
+ }
|
|
|
+
|
|
|
+ void Sprite::clipTrianglesToRect(UINT8* vertices, UINT8* uv, UINT32 numTris, UINT32 vertStride, const Rect2I& clipRect)
|
|
|
+ {
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
UINT64 SpriteMaterialInfo::generateHash() const
|
|
|
{
|
|
|
UINT64 textureId = 0;
|