diff options
Diffstat (limited to 'Runtime/Graphics')
125 files changed, 30275 insertions, 0 deletions
diff --git a/Runtime/Graphics/CubemapProcessor.cpp b/Runtime/Graphics/CubemapProcessor.cpp new file mode 100644 index 0000000..2cf535e --- /dev/null +++ b/Runtime/Graphics/CubemapProcessor.cpp @@ -0,0 +1,515 @@ +#include "UnityPrefix.h" +#include "CubemapProcessor.h" + +//-------------------------------------------------------------------------------------- +//Based on CCubeMapProcessor from CubeMapGen v1.4 +// Class for filtering and processing cubemaps +// +// +//-------------------------------------------------------------------------------------- +// (C) 2005 ATI Research, Inc., All rights reserved. +//-------------------------------------------------------------------------------------- + +//used to index cube faces +#define CP_FACE_X_POS 0 +#define CP_FACE_X_NEG 1 +#define CP_FACE_Y_POS 2 +#define CP_FACE_Y_NEG 3 +#define CP_FACE_Z_POS 4 +#define CP_FACE_Z_NEG 5 + +//used to index image edges +// NOTE.. the actual number corresponding to the edge is important +// do not change these, or the code will break +// +// CP_EDGE_LEFT is u = 0 +// CP_EDGE_RIGHT is u = width-1 +// CP_EDGE_TOP is v = 0 +// CP_EDGE_BOTTOM is v = height-1 +#define CP_EDGE_LEFT 0 +#define CP_EDGE_RIGHT 1 +#define CP_EDGE_TOP 2 +#define CP_EDGE_BOTTOM 3 + +//corners of CUBE map (P or N specifys if it corresponds to the +// positive or negative direction each of X, Y, and Z +#define CP_CORNER_NNN 0 +#define CP_CORNER_NNP 1 +#define CP_CORNER_NPN 2 +#define CP_CORNER_NPP 3 +#define CP_CORNER_PNN 4 +#define CP_CORNER_PNP 5 +#define CP_CORNER_PPN 6 +#define CP_CORNER_PPP 7 + + +//information about cube maps neighboring face after traversing +// across an edge +struct CPCubeMapNeighbor +{ + UInt8 m_Face; //index of neighboring face + UInt8 m_Edge; //edge in neighboring face that abuts this face +}; + +//------------------------------------------------------------------------------ +// D3D cube map face specification +// mapping from 3D x,y,z cube map lookup coordinates +// to 2D within face u,v coordinates +// +// --------------------> U direction +// | (within-face texture space) +// | _____ +// | | | +// | | +Y | +// | _____|_____|_____ _____ +// | | | | | | +// | | -X | +Z | +X | -Z | +// | |_____|_____|_____|_____| +// | | | +// | | -Y | +// | |_____| +// | +// v V direction +// (within-face texture space) +//------------------------------------------------------------------------------ + +//Information about neighbors and how texture coorrdinates change across faces +// in ORDER of left, right, top, bottom (e.g. edges corresponding to u=0, +// u=1, v=0, v=1 in the 2D coordinate system of the particular face. +//Note this currently assumes the D3D cube face ordering and orientation +CPCubeMapNeighbor sg_CubeNgh[6][4] = +{ + //XPOS face + {{CP_FACE_Z_POS, CP_EDGE_RIGHT }, + {CP_FACE_Z_NEG, CP_EDGE_LEFT }, + {CP_FACE_Y_POS, CP_EDGE_RIGHT }, + {CP_FACE_Y_NEG, CP_EDGE_RIGHT }}, + //XNEG face + {{CP_FACE_Z_NEG, CP_EDGE_RIGHT }, + {CP_FACE_Z_POS, CP_EDGE_LEFT }, + {CP_FACE_Y_POS, CP_EDGE_LEFT }, + {CP_FACE_Y_NEG, CP_EDGE_LEFT }}, + //YPOS face + {{CP_FACE_X_NEG, CP_EDGE_TOP }, + {CP_FACE_X_POS, CP_EDGE_TOP }, + {CP_FACE_Z_NEG, CP_EDGE_TOP }, + {CP_FACE_Z_POS, CP_EDGE_TOP }}, + //YNEG face + {{CP_FACE_X_NEG, CP_EDGE_BOTTOM}, + {CP_FACE_X_POS, CP_EDGE_BOTTOM}, + {CP_FACE_Z_POS, CP_EDGE_BOTTOM}, + {CP_FACE_Z_NEG, CP_EDGE_BOTTOM}}, + //ZPOS face + {{CP_FACE_X_NEG, CP_EDGE_RIGHT }, + {CP_FACE_X_POS, CP_EDGE_LEFT }, + {CP_FACE_Y_POS, CP_EDGE_BOTTOM }, + {CP_FACE_Y_NEG, CP_EDGE_TOP }}, + //ZNEG face + {{CP_FACE_X_POS, CP_EDGE_RIGHT }, + {CP_FACE_X_NEG, CP_EDGE_LEFT }, + {CP_FACE_Y_POS, CP_EDGE_TOP }, + {CP_FACE_Y_NEG, CP_EDGE_BOTTOM }} +}; + + +//The 12 edges of the cubemap, (entries are used to index into the neighbor table) +// this table is used to average over the edges. +int sg_CubeEdgeList[12][2] = { + {CP_FACE_X_POS, CP_EDGE_LEFT}, + {CP_FACE_X_POS, CP_EDGE_RIGHT}, + {CP_FACE_X_POS, CP_EDGE_TOP}, + {CP_FACE_X_POS, CP_EDGE_BOTTOM}, + + {CP_FACE_X_NEG, CP_EDGE_LEFT}, + {CP_FACE_X_NEG, CP_EDGE_RIGHT}, + {CP_FACE_X_NEG, CP_EDGE_TOP}, + {CP_FACE_X_NEG, CP_EDGE_BOTTOM}, + + {CP_FACE_Z_POS, CP_EDGE_TOP}, + {CP_FACE_Z_POS, CP_EDGE_BOTTOM}, + {CP_FACE_Z_NEG, CP_EDGE_TOP}, + {CP_FACE_Z_NEG, CP_EDGE_BOTTOM} +}; + + +//Information about which of the 8 cube corners are correspond to the +// the 4 corners in each cube face +// the order is upper left, upper right, lower left, lower right +int sg_CubeCornerList[6][4] = { + { CP_CORNER_PPP, CP_CORNER_PPN, CP_CORNER_PNP, CP_CORNER_PNN }, // XPOS face + { CP_CORNER_NPN, CP_CORNER_NPP, CP_CORNER_NNN, CP_CORNER_NNP }, // XNEG face + { CP_CORNER_NPN, CP_CORNER_PPN, CP_CORNER_NPP, CP_CORNER_PPP }, // YPOS face + { CP_CORNER_NNP, CP_CORNER_PNP, CP_CORNER_NNN, CP_CORNER_PNN }, // YNEG face + { CP_CORNER_NPP, CP_CORNER_PPP, CP_CORNER_NNP, CP_CORNER_PNP }, // ZPOS face + { CP_CORNER_PPN, CP_CORNER_NPN, CP_CORNER_PNN, CP_CORNER_NNN } // ZNEG face +}; + + + +//========================================================================================================== +//void FixupCubeEdges(CImageSurface *a_CubeMap, int a_FixupType, int a_FixupWidth); +// +//Apply edge fixup to a cubemap mip level. +// +//a_CubeMap [in/out] Array of 6 images comprising cubemap miplevel to apply edge fixup to. +//a_FixupType [in] Specifies the technique used for edge fixup. Choose one of the following, +// CP_FIXUP_NONE, CP_FIXUP_PULL_LINEAR, CP_FIXUP_PULL_HERMITE, CP_FIXUP_AVERAGE_LINEAR, +// CP_FIXUP_AVERAGE_HERMITE +//a_FixupWidth [in] Fixup width in texels +// +//========================================================================================================== + + +//-------------------------------------------------------------------------------------- +// Fixup cube edges +// +// average texels on cube map faces across the edges +//-------------------------------------------------------------------------------------- + +void FixupCubeEdges(CImageSurface *a_CubeMap, int a_FixupType, int a_FixupWidth) +{ + int i, j, k; + int face; + int edge; + int neighborFace; + int neighborEdge; + + int nChannels = a_CubeMap[0].m_NumChannels; + int size = a_CubeMap[0].m_Width; + + CPCubeMapNeighbor neighborInfo; + + CP_ITYPE* edgeStartPtr; + CP_ITYPE* neighborEdgeStartPtr; + + int edgeWalk; + int neighborEdgeWalk; + + //pointer walk to walk one texel away from edge in perpendicular direction + int edgePerpWalk; + int neighborEdgePerpWalk; + + //number of texels inward towards cubeface center to apply fixup to + int fixupDist; + int iFixup; + + // note that if functionality to filter across the three texels for each corner, then + CP_ITYPE *cornerPtr[8][3]; //indexed by corner and face idx + CP_ITYPE *faceCornerPtrs[4]; //corner pointers for face + int cornerNumPtrs[8]; //indexed by corner and face idx + int iCorner; //corner iterator + int iFace; //iterator for faces + int corner; + + //if there is no fixup, or fixup width = 0, do nothing + if((a_FixupType == CP_FIXUP_NONE) || + (a_FixupWidth == 0) ) + { + return; + } + + //special case 1x1 cubemap, average face colors + if( a_CubeMap[0].m_Width == 1 ) + { + //iterate over channels + for(k=0; k<nChannels; k++) + { + CP_ITYPE accum = 0.0f; + + //iterate over faces to accumulate face colors + for(iFace=0; iFace<6; iFace++) + { + accum += *(a_CubeMap[iFace].m_ImgData + k); + } + + //compute average over 6 face colors + accum /= 6.0f; + + //iterate over faces to distribute face colors + for(iFace=0; iFace<6; iFace++) + { + *(a_CubeMap[iFace].m_ImgData + k) = accum; + } + } + + return; + } + + + //iterate over corners + for(iCorner = 0; iCorner < 8; iCorner++ ) + { + cornerNumPtrs[iCorner] = 0; + } + + //iterate over faces to collect list of corner texel pointers + for(iFace=0; iFace<6; iFace++ ) + { + //the 4 corner pointers for this face + faceCornerPtrs[0] = a_CubeMap[iFace].m_ImgData; + faceCornerPtrs[1] = a_CubeMap[iFace].m_ImgData + ( (size - 1) * nChannels ); + faceCornerPtrs[2] = a_CubeMap[iFace].m_ImgData + ( (size) * (size - 1) * nChannels ); + faceCornerPtrs[3] = a_CubeMap[iFace].m_ImgData + ( (((size) * (size - 1)) + (size - 1)) * nChannels ); + + //iterate over face corners to collect cube corner pointers + for(i=0; i<4; i++ ) + { + corner = sg_CubeCornerList[iFace][i]; + cornerPtr[corner][ cornerNumPtrs[corner] ] = faceCornerPtrs[i]; + cornerNumPtrs[corner]++; + } + } + + + //iterate over corners to average across corner tap values + for(iCorner = 0; iCorner < 8; iCorner++ ) + { + for(k=0; k<nChannels; k++) + { + CP_ITYPE cornerTapAccum; + + cornerTapAccum = 0.0f; + + //iterate over corner texels and average results + for(i=0; i<3; i++ ) + { + cornerTapAccum += *(cornerPtr[iCorner][i] + k); + } + + //divide by 3 to compute average of corner tap values + cornerTapAccum *= (1.0f / 3.0f); + + //iterate over corner texels and average results + for(i=0; i<3; i++ ) + { + *(cornerPtr[iCorner][i] + k) = cornerTapAccum; + } + } + } + + + //maximum width of fixup region is one half of the cube face size + fixupDist = std::min( a_FixupWidth, size / 2); + + //iterate over the twelve edges of the cube to average across edges + for(i=0; i<12; i++) + { + face = sg_CubeEdgeList[i][0]; + edge = sg_CubeEdgeList[i][1]; + + neighborInfo = sg_CubeNgh[face][edge]; + neighborFace = neighborInfo.m_Face; + neighborEdge = neighborInfo.m_Edge; + + edgeStartPtr = a_CubeMap[face].m_ImgData; + neighborEdgeStartPtr = a_CubeMap[neighborFace].m_ImgData; + edgeWalk = 0; + neighborEdgeWalk = 0; + + //amount to pointer to sample taps away from cube face + edgePerpWalk = 0; + neighborEdgePerpWalk = 0; + + //Determine walking pointers based on edge type + // e.g. CP_EDGE_LEFT, CP_EDGE_RIGHT, CP_EDGE_TOP, CP_EDGE_BOTTOM + switch(edge) + { + case CP_EDGE_LEFT: + // no change to faceEdgeStartPtr + edgeWalk = nChannels * size; + edgePerpWalk = nChannels; + break; + case CP_EDGE_RIGHT: + edgeStartPtr += (size - 1) * nChannels; + edgeWalk = nChannels * size; + edgePerpWalk = -nChannels; + break; + case CP_EDGE_TOP: + // no change to faceEdgeStartPtr + edgeWalk = nChannels; + edgePerpWalk = nChannels * size; + break; + case CP_EDGE_BOTTOM: + edgeStartPtr += (size) * (size - 1) * nChannels; + edgeWalk = nChannels; + edgePerpWalk = -(nChannels * size); + break; + } + + //For certain types of edge abutments, the neighbor edge walk needs to + // be flipped: the cases are + // if a left edge mates with a left or bottom edge on the neighbor + // if a top edge mates with a top or right edge on the neighbor + // if a right edge mates with a right or top edge on the neighbor + // if a bottom edge mates with a bottom or left edge on the neighbor + //Seeing as the edges are enumerated as follows + // left =0 + // right =1 + // top =2 + // bottom =3 + // + //If the edge enums are the same, or the sum of the enums == 3, + // the neighbor edge walk needs to be flipped + if( (edge == neighborEdge) || ((edge + neighborEdge) == 3) ) + { //swapped direction neighbor edge walk + switch(neighborEdge) + { + case CP_EDGE_LEFT: //start at lower left and walk up + neighborEdgeStartPtr += (size - 1) * (size) * nChannels; + neighborEdgeWalk = -(nChannels * size); + neighborEdgePerpWalk = nChannels; + break; + case CP_EDGE_RIGHT: //start at lower right and walk up + neighborEdgeStartPtr += ((size - 1)*(size) + (size - 1)) * nChannels; + neighborEdgeWalk = -(nChannels * size); + neighborEdgePerpWalk = -nChannels; + break; + case CP_EDGE_TOP: //start at upper right and walk left + neighborEdgeStartPtr += (size - 1) * nChannels; + neighborEdgeWalk = -nChannels; + neighborEdgePerpWalk = (nChannels * size); + break; + case CP_EDGE_BOTTOM: //start at lower right and walk left + neighborEdgeStartPtr += ((size - 1)*(size) + (size - 1)) * nChannels; + neighborEdgeWalk = -nChannels; + neighborEdgePerpWalk = -(nChannels * size); + break; + } + } + else + { //swapped direction neighbor edge walk + switch(neighborEdge) + { + case CP_EDGE_LEFT: //start at upper left and walk down + //no change to neighborEdgeStartPtr for this case since it points + // to the upper left corner already + neighborEdgeWalk = nChannels * size; + neighborEdgePerpWalk = nChannels; + break; + case CP_EDGE_RIGHT: //start at upper right and walk down + neighborEdgeStartPtr += (size - 1) * nChannels; + neighborEdgeWalk = nChannels * size; + neighborEdgePerpWalk = -nChannels; + break; + case CP_EDGE_TOP: //start at upper left and walk left + //no change to neighborEdgeStartPtr for this case since it points + // to the upper left corner already + neighborEdgeWalk = nChannels; + neighborEdgePerpWalk = (nChannels * size); + break; + case CP_EDGE_BOTTOM: //start at lower left and walk left + neighborEdgeStartPtr += (size) * (size - 1) * nChannels; + neighborEdgeWalk = nChannels; + neighborEdgePerpWalk = -(nChannels * size); + break; + } + } + + + //Perform edge walk, to average across the 12 edges and smoothly propagate change to + //nearby neighborhood + + //step ahead one texel on edge + edgeStartPtr += edgeWalk; + neighborEdgeStartPtr += neighborEdgeWalk; + + // note that this loop does not process the corner texels, since they have already been + // averaged across faces across earlier + for(j=1; j<(size - 1); j++) + { + //for each set of taps along edge, average them + // and rewrite the results into the edges + for(k = 0; k<nChannels; k++) + { + CP_ITYPE edgeTap, neighborEdgeTap, avgTap; //edge tap, neighborEdgeTap and the average of the two + CP_ITYPE edgeTapDev, neighborEdgeTapDev; + + edgeTap = *(edgeStartPtr + k); + neighborEdgeTap = *(neighborEdgeStartPtr + k); + + //compute average of tap intensity values + avgTap = 0.5f * (edgeTap + neighborEdgeTap); + + //propagate average of taps to edge taps + (*(edgeStartPtr + k)) = avgTap; + (*(neighborEdgeStartPtr + k)) = avgTap; + + edgeTapDev = edgeTap - avgTap; + neighborEdgeTapDev = neighborEdgeTap - avgTap; + + //iterate over taps in direction perpendicular to edge, and + // adjust intensity values gradualy to obscure change in intensity values of + // edge averaging. + for(iFixup = 1; iFixup < fixupDist; iFixup++) + { + //fractional amount to apply change in tap intensity along edge to taps + // in a perpendicular direction to edge + CP_ITYPE fixupFrac = (CP_ITYPE)(fixupDist - iFixup) / (CP_ITYPE)(fixupDist); + CP_ITYPE fixupWeight; + + switch(a_FixupType ) + { + case CP_FIXUP_PULL_LINEAR: + { + fixupWeight = fixupFrac; + } + break; + case CP_FIXUP_PULL_HERMITE: + { + //hermite spline interpolation between 1 and 0 with both pts derivatives = 0 + // e.g. smooth step + // the full formula for hermite interpolation is: + // + // [ 2 -2 1 1 ][ p0 ] + // [t^3 t^2 t 1 ][ -3 3 -2 -1 ][ p1 ] + // [ 0 0 1 0 ][ d0 ] + // [ 1 0 0 0 ][ d1 ] + // + // Where p0 and p1 are the point locations and d0, and d1 are their respective derivatives + // t is the parameteric coordinate used to specify an interpoltion point on the spline + // and ranges from 0 to 1. + // if p0 = 0 and p1 = 1, and d0 and d1 = 0, the interpolation reduces to + // + // p(t) = - 2t^3 + 3t^2 + fixupWeight = ((-2.0 * fixupFrac + 3.0) * fixupFrac * fixupFrac); + } + break; + case CP_FIXUP_AVERAGE_LINEAR: + { + fixupWeight = fixupFrac; + + //perform weighted average of edge tap value and current tap + // fade off weight linearly as a function of distance from edge + edgeTapDev = + (*(edgeStartPtr + (iFixup * edgePerpWalk) + k)) - avgTap; + neighborEdgeTapDev = + (*(neighborEdgeStartPtr + (iFixup * neighborEdgePerpWalk) + k)) - avgTap; + } + break; + case CP_FIXUP_AVERAGE_HERMITE: + { + fixupWeight = ((-2.0 * fixupFrac + 3.0) * fixupFrac * fixupFrac); + + //perform weighted average of edge tap value and current tap + // fade off weight using hermite spline with distance from edge + // as parametric coordinate + edgeTapDev = + (*(edgeStartPtr + (iFixup * edgePerpWalk) + k)) - avgTap; + neighborEdgeTapDev = + (*(neighborEdgeStartPtr + (iFixup * neighborEdgePerpWalk) + k)) - avgTap; + } + break; + } + + // vary intensity of taps within fixup region toward edge values to hide changes made to edge taps + *(edgeStartPtr + (iFixup * edgePerpWalk) + k) -= (fixupWeight * edgeTapDev); + *(neighborEdgeStartPtr + (iFixup * neighborEdgePerpWalk) + k) -= (fixupWeight * neighborEdgeTapDev); + } + + } + + edgeStartPtr += edgeWalk; + neighborEdgeStartPtr += neighborEdgeWalk; + } + } +} diff --git a/Runtime/Graphics/CubemapProcessor.h b/Runtime/Graphics/CubemapProcessor.h new file mode 100644 index 0000000..b824e8d --- /dev/null +++ b/Runtime/Graphics/CubemapProcessor.h @@ -0,0 +1,35 @@ +#ifndef CUBEMAPPROCESSOR_H +#define CUBEMAPPROCESSOR_H + +//-------------------------------------------------------------------------------------- +// Based on CCubeMapProcessor from CubeMapGen v1.4 +// http://developer.amd.com/archive/gpu/cubemapgen/Pages/default.aspx#download +// Class for filtering and processing cubemaps +// +// +//-------------------------------------------------------------------------------------- +// (C) 2005 ATI Research, Inc., All rights reserved. +//-------------------------------------------------------------------------------------- + + +#define CP_ITYPE float + +struct CImageSurface +{ + int m_Width; //image width + int m_Height; //image height + int m_NumChannels; //number of channels + CP_ITYPE *m_ImgData; //cubemap image data +}; + +// Edge fixup type (how to perform smoothing near edge region) +#define CP_FIXUP_NONE 0 +#define CP_FIXUP_PULL_LINEAR 1 +#define CP_FIXUP_PULL_HERMITE 2 +#define CP_FIXUP_AVERAGE_LINEAR 3 +#define CP_FIXUP_AVERAGE_HERMITE 4 + + +void FixupCubeEdges (CImageSurface *a_CubeMap, int a_FixupType, int a_FixupWidth); + +#endif diff --git a/Runtime/Graphics/CubemapTexture.cpp b/Runtime/Graphics/CubemapTexture.cpp new file mode 100644 index 0000000..4fe2d7e --- /dev/null +++ b/Runtime/Graphics/CubemapTexture.cpp @@ -0,0 +1,186 @@ +#include "UnityPrefix.h" +#include "CubemapTexture.h" +#include "Runtime/Graphics/CubemapProcessor.h" +#include "Runtime/Graphics/Image.h" +#include "Runtime/Shaders/GraphicsCaps.h" +#include "Runtime/Utilities/BitUtility.h" +#include "Runtime/GfxDevice/GfxDevice.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" + +using namespace std; + +Cubemap::Cubemap (MemLabelId label, ObjectCreationMode mode) +: Super (label, mode) +{ + m_SourceTextures.resize (6); +} + +Cubemap::~Cubemap () +{ +} + +bool Cubemap::InitTexture (int width, int height, TextureFormat format, int flags, int imageCount) +{ + Assert (imageCount == 6); + Assert (width == height); + if( !IsPowerOfTwo (width) || !IsPowerOfTwo (height) ) + { + ErrorStringObject ("Texture has non-power of two size", this); + return false; + } + if( width != height ) + { + ErrorStringObject ("Cubemap faces must be square", this); + return false; + } + SetDirty(); + return Super::InitTexture (width, width, format, flags, 6); +} + + +void Cubemap::UploadTexture (bool dontUseSubImage) +{ + Assert (GetRawImageData()); + + ErrorIf (GetGLWidth() != GetGLHeight() || m_ImageCount != 6); + int faceDataSize = GetRawImageData(1)-GetRawImageData(0); + int dataSize = faceDataSize * 6; + UInt32 uploadFlags = (dontUseSubImage || !m_TextureUploaded) ? GfxDevice::kUploadTextureDontUseSubImage : GfxDevice::kUploadTextureDefault; + GetGfxDevice().UploadTextureCube( GetTextureID(), GetRawImageData(), dataSize, faceDataSize, GetGLWidth(), GetTextureFormat(), CountMipmaps(), uploadFlags, GetActiveTextureColorSpace() ); + Texture::s_TextureIDMap.insert (std::make_pair(GetTextureID(),this)); + + m_TextureSettings.m_WrapMode = kTexWrapClamp; + ApplySettings(); + m_TextureUploaded = true; + +#if !UNITY_EDITOR + if(!m_IsReadable) + { + DeallocateTextureData (m_TexData.data); + m_TexData.data = 0; + m_TexData.imageSize = 0; + } +#endif +} + +static void ConvertImageToFloatArray (float* dst, ImageReference const& src) +{ + ColorRGBAf* dstRGBA = (ColorRGBAf*)dst; + const int width = src.GetWidth (); + const int height = src.GetHeight (); + for (int y = 0; y < width; ++y) + for (int x = 0; x < height; ++x) + { + *dstRGBA++ = GetImagePixel (src.GetImageData (), width, height, src.GetFormat (), kTexWrapClamp, x, y); + } +} + +static void ConvertFloatArrayToImage (ImageReference& dst, float const* src) +{ + ColorRGBAf const* srcRGBA = (ColorRGBAf const*)src; + const int width = dst.GetWidth (); + const int height = dst.GetHeight (); + for (int y = 0; y < width; ++y) + for (int x = 0; x < height; ++x) + { + SetImagePixel (dst, x, y, kTexWrapClamp, *srcRGBA++); + } +} + +void Cubemap::RebuildMipMap () +{ + TextureRepresentation& rep = m_TexData; + + if (rep.data == NULL || !m_MipMap) + return; + + if (IsAnyCompressedTextureFormat(rep.format)) + { + ErrorStringObject ("Rebuilding mipmaps of compressed textures is not supported", this); + return; + } + if (m_ImageCount != 6) + { + ErrorStringObject ("Cubemap must have 6 faces", this); + return; + } + + Assert (rep.width == rep.height); + const int edge = rep.width; + for (int i = 0; i < 6; i++) + CreateMipMap (rep.data + rep.imageSize * i, edge, edge, 1, rep.format); +} + + +void Cubemap::FixupEdges (int fixupWidthInPixels) +{ + TextureRepresentation& rep = m_TexData; + + if (rep.data == NULL || !m_MipMap) + return; + + const int edge = rep.width; + const int channels = 4; CImageSurface cubemapSurfaces[6]; + for (int q = 0; q < 6; ++q) + { + cubemapSurfaces[q].m_ImgData = (float*)UNITY_MALLOC (kMemDefault, edge * edge * channels * sizeof(CP_ITYPE)); + } + + int mipEdge = edge; + for (int mip = 0; mip < CountMipmaps (); ++mip) + { + ImageReference mipImages[6]; + + for (int face = 0; face < 6; ++face) + { + if (!GetWriteImageReference (&mipImages[face], face, mip)) + { + ErrorStringObject ("Can't draw into cubemap", this); + return; + } + ConvertImageToFloatArray (cubemapSurfaces[face].m_ImgData, mipImages[face]); + cubemapSurfaces[face].m_Width = cubemapSurfaces[face].m_Height = mipEdge; + cubemapSurfaces[face].m_NumChannels = channels; + } + + FixupCubeEdges (cubemapSurfaces, CP_FIXUP_AVERAGE_HERMITE, fixupWidthInPixels); + + for (int face = 0; face < 6; ++face) + { + ConvertFloatArrayToImage (mipImages[face], cubemapSurfaces[face].m_ImgData); + } + + mipEdge = max(mipEdge/2, 1); + } + + for (int q = 0; q < 6; ++q) + { + UNITY_FREE (kMemDefault, cubemapSurfaces[q].m_ImgData); + } +} + +void Cubemap::SetSourceTexture (CubemapFace face, PPtr<Texture2D> tex) +{ + Assert (face < m_SourceTextures.size ()); + + m_SourceTextures[face] = tex; +} + +PPtr<Texture2D> Cubemap::GetSourceTexture (CubemapFace face) const +{ + Assert (face < m_SourceTextures.size ()); + + return m_SourceTextures[face]; +} + +template<class TransferFunction> +void Cubemap::Transfer (TransferFunction& transfer) +{ + Super::Transfer (transfer); + transfer.Transfer (m_SourceTextures, "m_SourceTextures"); + transfer.Align (); +} + +IMPLEMENT_CLASS (Cubemap) +IMPLEMENT_OBJECT_SERIALIZE (Cubemap) + diff --git a/Runtime/Graphics/CubemapTexture.h b/Runtime/Graphics/CubemapTexture.h new file mode 100644 index 0000000..6934926 --- /dev/null +++ b/Runtime/Graphics/CubemapTexture.h @@ -0,0 +1,30 @@ +#ifndef CUBEMAPTEXTURE_H +#define CUBEMAPTEXTURE_H + +#include "Texture2D.h" + + +class Cubemap : public Texture2D +{ +public: + REGISTER_DERIVED_CLASS (Cubemap, Texture2D) + DECLARE_OBJECT_SERIALIZE (Cubemap) + + Cubemap (MemLabelId label, ObjectCreationMode mode); + + virtual bool InitTexture (int width, int height, TextureFormat format, int flags = kMipmapMask, int imageCount = 1); + virtual TextureDimension GetDimension () const { return kTexDimCUBE; } + virtual void UploadTexture (bool dontUseSubImage); + + virtual void RebuildMipMap (); + + void FixupEdges (int fixupWidthInPixels = 1); + + void SetSourceTexture (CubemapFace face, PPtr<Texture2D> tex); + PPtr<Texture2D> GetSourceTexture (CubemapFace face) const; + +private: + std::vector<PPtr<Texture2D> > m_SourceTextures; +}; + +#endif diff --git a/Runtime/Graphics/DXTCompression.cpp b/Runtime/Graphics/DXTCompression.cpp new file mode 100644 index 0000000..b580a32 --- /dev/null +++ b/Runtime/Graphics/DXTCompression.cpp @@ -0,0 +1,595 @@ +// Fast DXT compression code adapted from +// stb_dxt by Sean Barrett: http://nothings.org/stb/stb_dxt.h +// which is in turn adapted from code by +// Fabian "ryg" Giesen: http://www.farbrausch.de/~fg/code/dxt/ + +#include "UnityPrefix.h" +#include "DXTCompression.h" + +// stb_dxt.h - v1.01 - DXT1/DXT5 compressor - public domain +// original by fabian "ryg" giesen - ported to C by stb +// +// version history: +// v1.xx - fix for big endian archs (Aras) +// v1.02 - fix alpha encoding bug +// v1.01 - fix endianness bug that messed up quality, thanks ryg & cbloom +// v1.00 - first release + +#if UNITY_BIG_ENDIAN +#define STB_BYTESWAP_16(i) ((i<<8)|(i>>8)) +#define STB_BYTESWAP_32(i) ((i>>24) | (i>>8)&0x0000ff00 | (i<<8)&0x00ff0000 | (i<<24)) +#else +#define STB_BYTESWAP_16(i) (i) +#define STB_BYTESWAP_32(i) (i) +#endif + + +static unsigned char stb__Expand5[32]; +static unsigned char stb__Expand6[64]; +static unsigned char stb__OMatch5[256][2]; +static unsigned char stb__OMatch6[256][2]; +static unsigned char stb__QuantRBTab[256+16]; +static unsigned char stb__QuantGTab[256+16]; + +static int stb__Mul8Bit(int a,int b) +{ + int t = a*b + 128; + return (t + (t >> 8)) >> 8; +} + +static void stb__From16Bit(unsigned char *out, unsigned short v) +{ + int rv = (v & 0xf800) >> 11; + int gv = (v & 0x07e0) >> 5; + int bv = (v & 0x001f) >> 0; + + out[0] = stb__Expand5[rv]; + out[1] = stb__Expand6[gv]; + out[2] = stb__Expand5[bv]; + out[3] = 0; +} + +static unsigned short stb__As16Bit(int r, int g, int b) +{ + return (stb__Mul8Bit(r,31) << 11) + (stb__Mul8Bit(g,63) << 5) + stb__Mul8Bit(b,31); +} + +static void stb__LerpRGB(unsigned char *out, unsigned char *p1, unsigned char *p2, int f) +{ + out[0] = p1[0] + stb__Mul8Bit(p2[0] - p1[0],f); + out[1] = p1[1] + stb__Mul8Bit(p2[1] - p1[1],f); + out[2] = p1[2] + stb__Mul8Bit(p2[2] - p1[2],f); +} + +/****************************************************************************/ + +// compute table to reproduce constant colors as accurately as possible +static void stb__PrepareOptTable(unsigned char *Table,const unsigned char *expand,int size) +{ + int i,mn,mx; + for (i=0;i<256;i++) { + int bestErr = 256; + for (mn=0;mn<size;mn++) { + for (mx=0;mx<size;mx++) { + int mine = expand[mn]; + int maxe = expand[mx]; + int err = abs(maxe + stb__Mul8Bit(mine-maxe,0x55) - i); + + if(err < bestErr) + { + Table[i*2+0] = mx; + Table[i*2+1] = mn; + bestErr = err; + } + } + } + } +} + +static void stb__EvalColors(unsigned char *color,unsigned short c0,unsigned short c1) +{ + stb__From16Bit(color+ 0, c0); + stb__From16Bit(color+ 4, c1); + stb__LerpRGB (color+ 8, color+0,color+4, 0x55); + stb__LerpRGB (color+12, color+0,color+4, 0xaa); +} + +// Block dithering function. Simply dithers a block to 565 RGB. +// (Floyd-Steinberg) +static void stb__DitherBlock(unsigned char *dest, unsigned char *block) +{ + int err[8],*ep1 = err,*ep2 = err+4, *et; + int ch,y; + + // process channels seperately + for (ch=0; ch<3; ++ch) { + unsigned char *bp = block+ch, *dp = dest+ch; + unsigned char *quant = (ch == 1) ? stb__QuantGTab+8 : stb__QuantRBTab+8; + memset(err, 0, sizeof(err)); + for(y=0; y<4; ++y) { + dp[ 0] = quant[bp[ 0] + ((3*ep2[1] + 5*ep2[0]) >> 4)]; + ep1[0] = bp[ 0] - dp[ 0]; + dp[ 4] = quant[bp[ 4] + ((7*ep1[0] + 3*ep2[2] + 5*ep2[1] + ep2[0]) >> 4)]; + ep1[1] = bp[ 4] - dp[ 4]; + dp[ 8] = quant[bp[ 8] + ((7*ep1[1] + 3*ep2[3] + 5*ep2[2] + ep2[1]) >> 4)]; + ep1[2] = bp[ 8] - dp[ 8]; + dp[12] = quant[bp[12] + ((7*ep1[2] + 5*ep2[3] + ep2[2]) >> 4)]; + ep1[3] = bp[12] - dp[12]; + bp += 16; + dp += 16; + et = ep1, ep1 = ep2, ep2 = et; // swap + } + } +} + +// The color matching function +static unsigned int stb__MatchColorsBlock(unsigned char *block, unsigned char *color,int dither) +{ + unsigned int mask = 0; + int dirr = color[0*4+0] - color[1*4+0]; + int dirg = color[0*4+1] - color[1*4+1]; + int dirb = color[0*4+2] - color[1*4+2]; + int dots[16]; + int stops[4]; + int i; + int c0Point, halfPoint, c3Point; + + for(i=0;i<16;i++) + dots[i] = block[i*4+0]*dirr + block[i*4+1]*dirg + block[i*4+2]*dirb; + + for(i=0;i<4;i++) + stops[i] = color[i*4+0]*dirr + color[i*4+1]*dirg + color[i*4+2]*dirb; + + c0Point = (stops[1] + stops[3]) >> 1; + halfPoint = (stops[3] + stops[2]) >> 1; + c3Point = (stops[2] + stops[0]) >> 1; + + if(!dither) { + // the version without dithering is straightforward + for (i=15;i>=0;i--) { + int dot = dots[i]; + mask <<= 2; + + if(dot < halfPoint) + mask |= (dot < c0Point) ? 1 : 3; + else + mask |= (dot < c3Point) ? 2 : 0; + } + } else { + // with floyd-steinberg dithering + int err[8],*ep1 = err,*ep2 = err+4; + int *dp = dots, y; + + c0Point <<= 4; + halfPoint <<= 4; + c3Point <<= 4; + for(i=0;i<8;i++) + err[i] = 0; + + for(y=0;y<4;y++) + { + int dot,lmask,step; + + dot = (dp[0] << 4) + (3*ep2[1] + 5*ep2[0]); + if(dot < halfPoint) + step = (dot < c0Point) ? 1 : 3; + else + step = (dot < c3Point) ? 2 : 0; + ep1[0] = dp[0] - stops[step]; + lmask = step; + + dot = (dp[1] << 4) + (7*ep1[0] + 3*ep2[2] + 5*ep2[1] + ep2[0]); + if(dot < halfPoint) + step = (dot < c0Point) ? 1 : 3; + else + step = (dot < c3Point) ? 2 : 0; + ep1[1] = dp[1] - stops[step]; + lmask |= step<<2; + + dot = (dp[2] << 4) + (7*ep1[1] + 3*ep2[3] + 5*ep2[2] + ep2[1]); + if(dot < halfPoint) + step = (dot < c0Point) ? 1 : 3; + else + step = (dot < c3Point) ? 2 : 0; + ep1[2] = dp[2] - stops[step]; + lmask |= step<<4; + + dot = (dp[3] << 4) + (7*ep1[2] + 5*ep2[3] + ep2[2]); + if(dot < halfPoint) + step = (dot < c0Point) ? 1 : 3; + else + step = (dot < c3Point) ? 2 : 0; + ep1[3] = dp[3] - stops[step]; + lmask |= step<<6; + + dp += 4; + mask |= lmask << (y*8); + { int *et = ep1; ep1 = ep2; ep2 = et; } // swap + } + } + + return mask; +} + +// The color optimization function. (Clever code, part 1) +static void stb__OptimizeColorsBlock(unsigned char *block, unsigned short *pmax16, unsigned short *pmin16) +{ + int mind = 0x7fffffff,maxd = -0x7fffffff; + unsigned char *minp = NULL, *maxp = NULL; + double magn; + int v_r,v_g,v_b; + static const int nIterPower = 4; + float covf[6],vfr,vfg,vfb; + + // determine color distribution + int cov[6]; + int mu[3],min[3],max[3]; + int ch,i,iter; + + for(ch=0;ch<3;ch++) + { + const unsigned char *bp = ((const unsigned char *) block) + ch; + int muv,minv,maxv; + + muv = minv = maxv = bp[0]; + for(i=4;i<64;i+=4) + { + muv += bp[i]; + if (bp[i] < minv) minv = bp[i]; + else if (bp[i] > maxv) maxv = bp[i]; + } + + mu[ch] = (muv + 8) >> 4; + min[ch] = minv; + max[ch] = maxv; + } + + // determine covariance matrix + for (i=0;i<6;i++) + cov[i] = 0; + + for (i=0;i<16;i++) + { + int r = block[i*4+0] - mu[0]; + int g = block[i*4+1] - mu[1]; + int b = block[i*4+2] - mu[2]; + + cov[0] += r*r; + cov[1] += r*g; + cov[2] += r*b; + cov[3] += g*g; + cov[4] += g*b; + cov[5] += b*b; + } + + // convert covariance matrix to float, find principal axis via power iter + for(i=0;i<6;i++) + covf[i] = cov[i] / 255.0f; + + vfr = (float) (max[0] - min[0]); + vfg = (float) (max[1] - min[1]); + vfb = (float) (max[2] - min[2]); + + for(iter=0;iter<nIterPower;iter++) + { + float r = vfr*covf[0] + vfg*covf[1] + vfb*covf[2]; + float g = vfr*covf[1] + vfg*covf[3] + vfb*covf[4]; + float b = vfr*covf[2] + vfg*covf[4] + vfb*covf[5]; + + vfr = r; + vfg = g; + vfb = b; + } + + magn = fabs(vfr); + if (fabs(vfg) > magn) magn = fabs(vfg); + if (fabs(vfb) > magn) magn = fabs(vfb); + + if(magn < 4.0f) { // too small, default to luminance + v_r = 148; + v_g = 300; + v_b = 58; + } else { + magn = 512.0 / magn; + v_r = (int) (vfr * magn); + v_g = (int) (vfg * magn); + v_b = (int) (vfb * magn); + } + + // Pick colors at extreme points + for(i=0;i<16;i++) + { + int dot = block[i*4+0]*v_r + block[i*4+1]*v_g + block[i*4+2]*v_b; + + if (dot < mind) { + mind = dot; + minp = block+i*4; + } + + if (dot > maxd) { + maxd = dot; + maxp = block+i*4; + } + } + + *pmax16 = stb__As16Bit(maxp[0],maxp[1],maxp[2]); + *pmin16 = stb__As16Bit(minp[0],minp[1],minp[2]); +} + +static int stb__sclamp(float y, int p0, int p1) +{ + int x = (int) y; + if (x < p0) return p0; + if (x > p1) return p1; + return x; +} + +// The refinement function. (Clever code, part 2) +// Tries to optimize colors to suit block contents better. +// (By solving a least squares system via normal equations+Cramer's rule) +static int stb__RefineBlock(unsigned char *block, unsigned short *pmax16, unsigned short *pmin16, unsigned int mask) +{ + static const int w1Tab[4] = { 3,0,2,1 }; + static const int prods[4] = { 0x090000,0x000900,0x040102,0x010402 }; + // ^some magic to save a lot of multiplies in the accumulating loop... + + float frb,fg; + unsigned short oldMin, oldMax, min16, max16; + int i, akku = 0, xx,xy,yy; + int At1_r,At1_g,At1_b; + int At2_r,At2_g,At2_b; + unsigned int cm = mask; + + oldMin = *pmin16; + oldMax = *pmax16; + + if((mask ^ (mask<<2)) < 4) // all pixels have the same index? + { + // yes, linear system would be singular; solve using optimal + // single-color match on average color + int r = 8, g = 8, b = 8; + for (i=0;i<16;++i) { + r += block[i*4+0]; + g += block[i*4+1]; + b += block[i*4+2]; + } + + r >>= 4; g >>= 4; b >>= 4; + + max16 = (stb__OMatch5[r][0]<<11) | (stb__OMatch6[g][0]<<5) | stb__OMatch5[b][0]; + min16 = (stb__OMatch5[r][1]<<11) | (stb__OMatch6[g][1]<<5) | stb__OMatch5[b][1]; + } else { + At1_r = At1_g = At1_b = 0; + At2_r = At2_g = At2_b = 0; + for (i=0;i<16;++i,cm>>=2) { + int step = cm&3; + int w1 = w1Tab[step]; + int r = block[i*4+0]; + int g = block[i*4+1]; + int b = block[i*4+2]; + + akku += prods[step]; + At1_r += w1*r; + At1_g += w1*g; + At1_b += w1*b; + At2_r += r; + At2_g += g; + At2_b += b; + } + + At2_r = 3*At2_r - At1_r; + At2_g = 3*At2_g - At1_g; + At2_b = 3*At2_b - At1_b; + + // extract solutions and decide solvability + xx = akku >> 16; + yy = (akku >> 8) & 0xff; + xy = (akku >> 0) & 0xff; + + frb = 3.0f * 31.0f / 255.0f / (xx*yy - xy*xy); + fg = frb * 63.0f / 31.0f; + + // solve. + max16 = stb__sclamp((At1_r*yy - At2_r*xy)*frb+0.5f,0,31) << 11; + max16 |= stb__sclamp((At1_g*yy - At2_g*xy)*fg +0.5f,0,63) << 5; + max16 |= stb__sclamp((At1_b*yy - At2_b*xy)*frb+0.5f,0,31) << 0; + + min16 = stb__sclamp((At2_r*xx - At1_r*xy)*frb+0.5f,0,31) << 11; + min16 |= stb__sclamp((At2_g*xx - At1_g*xy)*fg +0.5f,0,63) << 5; + min16 |= stb__sclamp((At2_b*xx - At1_b*xy)*frb+0.5f,0,31) << 0; + } + + *pmin16 = min16; + *pmax16 = max16; + return oldMin != min16 || oldMax != max16; +} + +// Color block compression +static void stb__CompressColorBlock(unsigned char *dest, unsigned char *block,int quality) +{ + unsigned int mask; + int i; + unsigned short max16, min16; + unsigned char dblock[16*4],color[4*4]; + + // check if block is constant + for (i=1;i<16;i++) + if (((unsigned int *) block)[i] != ((unsigned int *) block)[0]) + break; + + if(i == 16) { // constant color + int r = block[0], g = block[1], b = block[2]; + mask = 0xaaaaaaaa; + max16 = (stb__OMatch5[r][0]<<11) | (stb__OMatch6[g][0]<<5) | stb__OMatch5[b][0]; + min16 = (stb__OMatch5[r][1]<<11) | (stb__OMatch6[g][1]<<5) | stb__OMatch5[b][1]; + } else { + // first step: compute dithered version for PCA if desired + if(quality) + stb__DitherBlock(dblock,block); + + // second step: pca+map along principal axis + stb__OptimizeColorsBlock(quality ? dblock : block,&max16,&min16); + if (max16 != min16) { + stb__EvalColors(color,max16,min16); + mask = stb__MatchColorsBlock(block,color,quality != 0); + } else + mask = 0; + + // third step: refine + if (stb__RefineBlock(quality ? dblock : block,&max16,&min16,mask)) { + if (max16 != min16) { + stb__EvalColors(color,max16,min16); + mask = stb__MatchColorsBlock(block,color,quality != 0); + } else + mask = 0; + } + } + + // write the color block + if(max16 < min16) + { + unsigned short t = min16; + min16 = max16; + max16 = t; + mask ^= 0x55555555; + } + + ((unsigned short *) dest)[0] = STB_BYTESWAP_16(max16); + ((unsigned short *) dest)[1] = STB_BYTESWAP_16(min16); + ((unsigned int *) dest)[1] = STB_BYTESWAP_32(mask); +} + +// Alpha block compression (this is easy for a change) +static void stb__CompressAlphaBlock(unsigned char *dest,unsigned char *src,int quality) +{ + int i,dist,bias,dist4,dist2,bits,mask; + + // find min/max color + int mn,mx; + mn = mx = src[3]; + + for (i=1;i<16;i++) + { + if (src[i*4+3] < mn) mn = src[i*4+3]; + else if (src[i*4+3] > mx) mx = src[i*4+3]; + } + + // encode them + ((unsigned char *)dest)[0] = mx; + ((unsigned char *)dest)[1] = mn; + dest += 2; + + // determine bias and emit color indices + dist = mx-mn; + bias = mn*7 - (dist >> 1); + dist4 = dist*4; + dist2 = dist*2; + bits = 0,mask=0; + + for (i=0;i<16;i++) { + int a = src[i*4+3]*7 - bias; + int ind,t; + + // select index (hooray for bit magic) + t = (dist4 - a) >> 31; ind = t & 4; a -= dist4 & t; + t = (dist2 - a) >> 31; ind += t & 2; a -= dist2 & t; + t = (dist - a) >> 31; ind += t & 1; + + ind = -ind & 7; + ind ^= (2 > ind); + + // write index + mask |= ind << bits; + if((bits += 3) >= 8) { + *dest++ = mask; + mask >>= 8; + bits -= 8; + } + } +} + +static void stb__InitDXT() +{ + int i; + for(i=0;i<32;i++) + stb__Expand5[i] = (i<<3)|(i>>2); + + for(i=0;i<64;i++) + stb__Expand6[i] = (i<<2)|(i>>4); + + for(i=0;i<256+16;i++) + { + int v = i-8 < 0 ? 0 : i-8 > 255 ? 255 : i-8; + stb__QuantRBTab[i] = stb__Expand5[stb__Mul8Bit(v,31)]; + stb__QuantGTab[i] = stb__Expand6[stb__Mul8Bit(v,63)]; + } + + stb__PrepareOptTable(&stb__OMatch5[0][0],stb__Expand5,32); + stb__PrepareOptTable(&stb__OMatch6[0][0],stb__Expand6,64); +} + +// input: a 4x4 pixel block, r,g,b,a bytes per pixel +// alpha: true = DXT5, false = DXT1 +// quality: true = slower, with dither; false = faster, no dither +static void FastCompressDXTBlock( UInt8* dest, const UInt8* src, bool alpha, bool quality) +{ + static int init=0; + if (!init) { + stb__InitDXT(); + init=1; + } + + if (alpha) { + stb__CompressAlphaBlock(dest,(unsigned char*) src,quality); + dest += 8; + } + + stb__CompressColorBlock(dest,(unsigned char*) src,quality); +} + +void FastCompressImage (int width, int height, const UInt8* src, UInt8* dest, bool dxt5, bool dither ) +{ + // adapted from stbgl__compress: http://nothings.org/stb/stb_gl.h + + const UInt8* destEnd = dest + ((width+3)/4)*((height+3)/4)*(dxt5?16:8); + + for (int y=0; y < height; y += 4) { + for (int x=0; x < width; x += 4) { + UInt8 block[16*4]; + int xleft=4; + if (x+3 >= width) xleft = width-x; + int yleft; + for (yleft=0; yleft < 4; ++yleft) { + if (y+yleft >= height) break; + memcpy(block+yleft*16, src + width*4*(y+yleft) + x*4, xleft*4); + } + if (xleft < 4) { + switch (xleft) { + case 0: AssertString("should not happen"); + case 1: + for (int yy=0; yy < yleft; ++yy) { + memcpy(block+yy*16+1*4, block+yy*16+0*4, 4); + memcpy(block+yy*16+2*4, block+yy*16+0*4, 8); + } + break; + case 2: + for (int yy=0; yy < yleft; ++yy) + memcpy(block+yy*16+2*4, block+yy*16+0*4, 8); + break; + case 3: + for (int yy=0; yy < yleft; ++yy) + memcpy(block+yy*16+3*4, block+yy*16+1*4, 4); + break; + } + } + int yy = 0; + for(; yleft<4; ++yleft,++yy) + memcpy(block+yleft*16, block+yy*16, 4*4); + FastCompressDXTBlock( dest, block, dxt5, dither ); + dest += dxt5 ? 16 : 8; + } + } + + AssertIf( dest > destEnd ); + UNUSED(destEnd); +} diff --git a/Runtime/Graphics/DXTCompression.h b/Runtime/Graphics/DXTCompression.h new file mode 100644 index 0000000..414c23e --- /dev/null +++ b/Runtime/Graphics/DXTCompression.h @@ -0,0 +1,13 @@ +// Fast DXT compression code adapted from +// stb_dxt by Sean Barrett: http://nothings.org/stb/stb_dxt.h +// which is in turn adapted from code by +// Fabian "ryg" Giesen: http://www.farbrausch.de/~fg/code/dxt/ + +#ifndef __DXT_COMPRESSION__ +#define __DXT_COMPRESSION__ + +// src: 32 bit/pixel width*height image, R,G,B,A bytes in pixel. +// dest must be (width+3)/4*(height+3)/4 bytes if dxt5=true, and half of that if dxt5=false (dxt1 is used then). +void FastCompressImage (int width, int height, const UInt8* src, UInt8* dest, bool dxt5, bool dither ); + +#endif diff --git a/Runtime/Graphics/DisplayManager.h b/Runtime/Graphics/DisplayManager.h new file mode 100644 index 0000000..6de5f08 --- /dev/null +++ b/Runtime/Graphics/DisplayManager.h @@ -0,0 +1,54 @@ +#pragma once +#include "UnityPrefix.h" + +// we do c-style interface, because display management is way too platfrom specific +// these function will be used from script +// UnityDisplayManager prefix is used because some platforms implements this in trampoline + +#define SUPPORT_MULTIPLE_DISPLAYS UNITY_IPHONE + +#if !SUPPORT_MULTIPLE_DISPLAYS + #include "ScreenManager.h" +#endif + +struct RenderSurfaceBase; + +#if SUPPORT_MULTIPLE_DISPLAYS + + extern "C" int UnityDisplayManager_DisplayCount(); + extern "C" bool UnityDisplayManager_DisplayAvailable(void* nativeDisplay); + extern "C" void UnityDisplayManager_DisplaySystemResolution(void* nativeDisplay, int* w, int* h); + extern "C" void UnityDisplayManager_DisplayRenderingResolution(void* nativeDisplay, int* w, int* h); + extern "C" void UnityDisplayManager_DisplayRenderingBuffers(void* nativeDisplay, RenderSurfaceBase** colorBuffer, RenderSurfaceBase** depthBuffer); + extern "C" void UnityDisplayManager_SetRenderingResolution(void* nativeDisplay, int w, int h); + +#else + + inline int UnityDisplayManager_DisplayCount() + { + return 1; + } + inline bool UnityDisplayManager_DisplayAvailable(void*) + { + return true; + } + inline void UnityDisplayManager_DisplaySystemResolution(void*, int* w, int* h) + { + *w = GetScreenManager().GetWidth(); + *h = GetScreenManager().GetHeight(); + } + inline void UnityDisplayManager_DisplayRenderingResolution(void*, int* w, int* h) + { + *w = GetScreenManager().GetWidth(); + *h = GetScreenManager().GetHeight(); + } + inline void UnityDisplayManager_SetRenderingResolution(void*, int w, int h) + { + GetScreenManager().RequestResolution(w, h, GetScreenManager ().IsFullScreen (), 0); + } + inline void UnityDisplayManager_DisplayRenderingBuffers(void*, RenderSurfaceBase** color, RenderSurfaceBase** depth) + { + *color = *depth = 0; + } + +#endif diff --git a/Runtime/Graphics/DrawSplashScreenAndWatermarks.cpp b/Runtime/Graphics/DrawSplashScreenAndWatermarks.cpp new file mode 100644 index 0000000..6735e4a --- /dev/null +++ b/Runtime/Graphics/DrawSplashScreenAndWatermarks.cpp @@ -0,0 +1,449 @@ +#include "UnityPrefix.h" +#include "DrawSplashScreenAndWatermarks.h" +#include "Runtime/Graphics/GraphicsHelper.h" +#include "Runtime/Misc/ReproductionLog.h" +#include "Runtime/Misc/PlayerSettings.h" +#include "Runtime/Misc/BuildSettings.h" +#include "Runtime/Misc/ResourceManager.h" +#include "Runtime/Graphics/Texture2D.h" +#include "Runtime/Graphics/ScreenManager.h" +#include "Runtime/Input/TimeManager.h" +#include "Runtime/Camera/RenderManager.h" +#include "Runtime/Camera/CameraUtil.h" +#include "Runtime/Camera/RenderLayers/GUITexture.h" +#include "Runtime/Misc/SystemInfo.h" + +#if WEBPLUG +double gDisplayFullscreenEscapeTimeout = -1.0F; +#endif + +static bool ConsumeVersionNumber(const char *&version, int &number) +{ + number = 0; + + for (int i = 0; i < 8; ++i) // limit to 8 digits + { + char c = *version; + + if ((c < '0') || (c > '9')) + { + return (i > 0); + } + + ++version; + + number *= 10; + number += (c - '0'); + } + + return false; +} + +static bool ConsumeVersionSeparator(const char *&version) +{ + if ('.' == *version) + { + ++version; + return true; + } + + return false; +} + +bool IsContentBuiltWithBetaVersion() +{ + if (GetBuildSettingsPtr() == NULL) + return false; + + const char* version = GetBuildSettings().GetVersion().c_str(); + + int temp; + + if (ConsumeVersionNumber(version, temp) && ConsumeVersionSeparator(version)) // major + { + if (ConsumeVersionNumber(version, temp) && ConsumeVersionSeparator(version)) // minor + { + if (ConsumeVersionNumber(version, temp)) // fix + { + char c = *version; + + if (('d' == c) || ('D' == c) || // development + ('a' == c) || ('A' == c) || // alpha + ('b' == c) || ('B' == c)) // beta + { + return true; + } + } + } + } + + return false; +} + +void DrawWaterMark( bool alwaysDisplay ) +{ + Texture2D* watermark = GetBuiltinResource<Texture2D>("UnityWaterMark-small.png"); + if( !watermark ) + return; + +#if !UNITY_EDITOR + + const float kSlideInDelay = 1.5f; + const float kSlideInTime = 1.0f; + const float kStayTime = 5.0f; + const float kSlideOutTime = 1.5f; + + float t = GetTimeManager().GetRealtime(); + if( !alwaysDisplay && t > kSlideInDelay+kSlideInTime+kStayTime+kSlideOutTime ) + return; + + float a; + if( t < kSlideInDelay+kSlideInTime ) + a = (t-kSlideInDelay) / kSlideInTime; + else if( !alwaysDisplay ) + a = 1.0f - (t-(kSlideInDelay+kSlideInTime+kStayTime)) / kSlideOutTime; + else + a = 1.0f; + +#else + float a = 1.0f; +#endif + + float pos = SmoothStep( 0.0f, 128.0f, a ); + + const Rectf& windowRect = GetRenderManager().GetWindowRect(); + DeviceMVPMatricesState preserveMVP; + SetupPixelCorrectCoordinates(); + + DrawGUITexture (Rectf (windowRect.width-pos, 62, 128, -58), watermark, ColorRGBAf(0.5f, 0.5f, 0.5f, 0.5f)); +} + +// (0, 0) is at the lower left corner. x increments to the left, y - upwards. +// specify -(x + 1) to align to the right and -(y + 1) to align to the top +static int DrawSimpleWatermark(const string &name, float x, float y, const ColorRGBAf& color) +{ + Texture2D* texture = GetBuiltinResource<Texture2D>(name); + if (!texture) + return 0; + + const Rectf& windowRect = GetRenderManager().GetWindowRect(); + DeviceMVPMatricesState preserveMVP; + SetupPixelCorrectCoordinates(); + + float width = texture->GetDataWidth(); + float height = -texture->GetDataHeight(); + + if (x < 0) + { + x = -(x + 1); + x = (windowRect.width - width - x); + } + + if (y < 0) + { + y = -(y + 1); + y = (windowRect.height + height - y); + } + + y -= height; + + DrawGUITexture (Rectf (x, y, width, height), texture, color); + + return texture->GetDataHeight(); +} + +static int DrawSimpleWatermark(const string &name, float x, float y) +{ + return DrawSimpleWatermark(name, x, y, ColorRGBAf(0.5f, 0.5f, 0.5f, 0.5f)); +} + +static void DrawTrialWatermark (int& watermarkoffset) +{ + int height = DrawSimpleWatermark("UnityWaterMark-trial.png", -(1 + 1), watermarkoffset); + watermarkoffset += height + 3; +} + +static void DrawEducationalWatermark (int& watermarkoffset) +{ + int height = DrawSimpleWatermark("UnityWaterMark-edu.png", -(1 + 1), watermarkoffset); + watermarkoffset += height + 3; +} + +static void DrawPrototypingWatermark (int& watermarkoffset) +{ + int height = DrawSimpleWatermark("UnityWaterMark-proto.png", -(1 + 1), watermarkoffset); + watermarkoffset += height + 3; +} + +static void DrawDeveloperWatermark (int& watermarkoffset) +{ + int height = DrawSimpleWatermark("UnityWaterMark-dev.png", -(1 + 1), watermarkoffset); + watermarkoffset += height + 3; +} + +#if UNITY_FLASH +static void DrawDebugFlashPlayerWatermark (int& watermarkoffset) +{ + int height = DrawSimpleWatermark("UnityWatermark-DebugFlashPlayer.png", -(1 + 1), watermarkoffset); + watermarkoffset += height + 3; +} +#endif + +#if WEBPLUG + + +static void DrawContentBetaWatermark (int& watermarkoffset) +{ + int height = DrawSimpleWatermark("UnityWaterMark-beta.png", -(1 + 1), watermarkoffset); + watermarkoffset += height + 3; +} + +static void DrawPluginBetaWatermark (int& watermarkoffset) +{ + int height = DrawSimpleWatermark("UnityWaterMarkPlugin-beta.png", -(1 + 1), watermarkoffset); + watermarkoffset += height + 3; +} + +void ShowFullscreenEscapeWarning () +{ + gDisplayFullscreenEscapeTimeout = GetTimeSinceStartup(); +} + +void RenderFullscreenEscapeWarning () +{ + const float kTimeout = 6.0F; + if (GetTimeSinceStartup() < gDisplayFullscreenEscapeTimeout + kTimeout && GetScreenManager().IsFullScreen()) + { + float time = GetTimeSinceStartup() - gDisplayFullscreenEscapeTimeout; + time = std::max(time, 0.0F); + + Texture2D* background = GetBuiltinResource<Texture2D>("EscToExit_back.png"); + Texture2D* text = GetBuiltinResource<Texture2D>("EscToExit_text.png"); + if( background && text ) + { + float a = SmoothStep( 0.5f, 0, (time - 3.5f) * 0.5f ); + ColorRGBA32 color = ColorRGBAf(0.5f,0.5f,0.5f,a); + const float w = 350; + const float h = 71; + const float yFac = 1.0f - 0.38197f; + + const Rectf& windowRect = GetRenderManager().GetWindowRect(); + DeviceMVPMatricesState preserveMVP; + SetupPixelCorrectCoordinates(); + DrawGUITexture( Rectf((windowRect.width-w)*0.5f, (windowRect.height-h)*yFac, w, -h), background, 36, 36, 0, 0, color ); + const float textw = 267; + DrawGUITexture( Rectf((windowRect.width-textw)*0.5f, (windowRect.height-h)*yFac-25, textw, -20), text, color ); + } + } + else if (!GetScreenManager().HasFullscreenRequested()) + { + gDisplayFullscreenEscapeTimeout = -1000.0; + } +} +#endif + + +#if UNITY_CAN_SHOW_SPLASH_SCREEN + +static double gSplashScreenStartTime; +static const float kSplashBeforeGameDuration = 4.5f; + +void BeginSplashScreenFade() +{ + gSplashScreenStartTime = GetTimeSinceStartup(); +} + +bool IsSplashScreenFadeComplete() +{ + return (GetTimeSinceStartup() >= gSplashScreenStartTime + kSplashBeforeGameDuration); +} + +bool GetShouldShowSplashScreen() +{ + const BuildSettings& settings = GetBuildSettings(); + bool disableWatermark = GetBuildSettings().isNoWatermarkBuild; + if ( disableWatermark ) + return false; + bool isTrial = !settings.hasPublishingRights; + bool isIndie = !settings.hasPROVersion; + return isTrial || isIndie; +} + +#if DEBUGMODE + #define DEBUGMSG_ONCE(Message) \ + {\ + static bool once = false; \ + if (!once) { printf_console(Message); once = true; }\ + } +#else + #define DEBUGMSG_ONCE(Message) {} +#endif + +void DrawSplashScreen (bool fullDraw) +{ + AssertIf (!GetShouldShowSplashScreen()); + + DEBUGMSG_ONCE("Begin showing splash screen.\n"); + + const float kSplashFadeStart = 3.0f; + const float kSplashXFadeDuration = 0.4f; + + float timeBegin = fullDraw ? 0.0f : kSplashBeforeGameDuration; + float t = GetTimeSinceStartup() - gSplashScreenStartTime - timeBegin; + if (!fullDraw && t > kSplashXFadeDuration) + { + DEBUGMSG_ONCE("End showing splash screen.\n"); + return; + } + + Texture2D* logo = GetBuiltinResource<Texture2D>("UnitySplash.png"); + if (!logo) + return; + Texture2D* text = GetBuiltinResource<Texture2D>("UnitySplash2.png"); + if (!text) + return; + Texture2D* background = GetBuiltinResource<Texture2D>("UnitySplash3.png"); + if (!background) + return; + Texture2D* black = GetBuiltinResource<Texture2D>("UnitySplashBack.png"); + if (!black) + return; + + const Rectf& windowRect = GetRenderManager().GetWindowRect(); + Vector2f basePos (windowRect.width * .5f - 119, windowRect.height *.5f - 219); + + Rectf (basePos.x, basePos.y, 276,432); + + GfxDevice& device = GetGfxDevice(); + if (fullDraw) + { + device.BeginFrame(); + if (!device.IsValidState()) + { + device.HandleInvalidState(); + return; + } + const float kBlack[4] = {0,0,0,0}; + GraphicsHelper::Clear (kGfxClearAll, kBlack, 1.0f, 0); + } + + DeviceMVPMatricesState preserveMVP; + SetupPixelCorrectCoordinates(); + + if (fullDraw) + { + float a = SmoothStep( 0.5f, 0.0f, (t - kSplashFadeStart) / (kSplashBeforeGameDuration-kSplashFadeStart)); + DrawGUITexture( Rectf (basePos.x, basePos.y + 432, 276, -432), background, ColorRGBAf(a,a,a, 0.5f) ); + DrawGUITexture( Rectf (basePos.x, basePos.y + 432, 276, -432), background, ColorRGBAf(a,a,a, 0.5f) ); + DrawGUITexture( Rectf (basePos.x + 43, basePos.y + 83, 146, -19), text, ColorRGBAf(a,a,a, 0.5f) ); + DrawGUITexture( Rectf (basePos.x, basePos.y + 330, 206, -214), logo, ColorRGBAf(a,a,a, 0.5f) ); + } + else + { + float a = SmoothStep( 0.5f, 0.0f, t / kSplashXFadeDuration); + DrawGUITexture(Rectf (0,0,windowRect.width+10, windowRect.height+10), black, ColorRGBAf(0.5f, 0.5f, 0.5f, a)); + } + + if (fullDraw) + { + device.EndFrame(); +#if ((UNITY_LINUX && SUPPORT_X11) || UNITY_OSX) && !UNITY_EDITOR && !WEBPLUG + GetScreenManager().SetBlitMaterial(); +#endif + device.PresentFrame(); + } +} + +#endif // UNITY_CAN_SHOW_SPLASH_SCREEN + + +#if UNITY_FLASH +static bool isDebugFlashPlayer; +static bool haveSetDebugFlashPlayer=false; + +static bool IsRunningInDebugFlashPlayer() +{ + if (!haveSetDebugFlashPlayer) + { + __asm("%0 = flash.system.Capabilities.isDebugger ? 1 : 0;" : "=r"(isDebugFlashPlayer)); + //__asm("trace('Debugger!?!?!');"); + //__asm("trace(flash.system.Capabilities.isDebugger ? 1 : 0);"); + haveSetDebugFlashPlayer = true; + } + return isDebugFlashPlayer; +} +#endif + +void DrawSplashAndWatermarks() +{ +#if SUPPORT_REPRODUCE_LOG + if (GetReproduceMode() != kNormalPlayback) + return; +#endif + // draw splash / watermark + RuntimePlatform platform = systeminfo::GetRuntimePlatform(); + bool disableWatermark = GetBuildSettings().isNoWatermarkBuild; + bool isEducational = GetBuildSettings().isEducationalBuild && !UNITY_EDITOR; + bool isPrototyping = GetBuildSettings().isPrototypingBuild && !UNITY_EDITOR; + bool isTrial = !GetBuildSettings().hasPublishingRights && !UNITY_EDITOR; + bool isIndie = !GetBuildSettings().hasPROVersion; + bool isIndieAndWebPlayer = isIndie && systeminfo::IsPlatformWebPlayer(platform); + bool isDeveloperPlayerBuild = !UNITY_EDITOR && GetBuildSettings().isDebugBuild; + + if (isIndieAndWebPlayer) + DrawWaterMark (false); + + int waterMarkOffset = 3; + +#if UNITY_FLASH + if (IsRunningInDebugFlashPlayer()) + DrawDebugFlashPlayerWatermark(waterMarkOffset); +#endif + + const bool isDeveloperBuild = IS_CONTENT_NEWER_OR_SAME(kUnityVersion3_2_a1) && isDeveloperPlayerBuild || (UNITY_DEVELOPER_BUILD && !UNITY_FLASH); + + if (isDeveloperBuild) + DrawDeveloperWatermark(waterMarkOffset); + +#if !UNITY_FLASH && !UNITY_WP8 + // Time based licenses will still look like "trial" licenses, so check for educational and prototyping flags first + if ( !disableWatermark ) + { + if (isEducational) + DrawEducationalWatermark (waterMarkOffset); + else if (isPrototyping) + DrawPrototypingWatermark (waterMarkOffset); + else if (isTrial) + DrawTrialWatermark (waterMarkOffset); + } +#endif + +#if UNITY_ANDROID || UNITY_BB10 + // it will be drawn completely transparent, so no need to worry ;-) + if( gGraphicsCaps.gles20.needToRenderWatermarkDueToDrawFromMemoryBuggy + && GetGfxDevice().GetRenderer() == kGfxRendererOpenGLES20Mobile + && !isDeveloperBuild + ) + { + DrawSimpleWatermark("UnityWaterMark-dev.png", -(1 + 1), waterMarkOffset, ColorRGBAf(0,0,0,0)); + } +#endif + +#if WEBPLUG && !UNITY_PEPPER && !UNITY_FLASH + + // Jonas Echterhoff suggested [2012.07.23 18:21:35] to show only one of the messages, + // i.e., either plugin beta or content beta, because if you have some unity beta installed + // you will always have three watermarks: Made with beta, Running on beta and Development player + #if UNITY_IS_BETA + DrawPluginBetaWatermark (waterMarkOffset); + #else + if( IsContentBuiltWithBetaVersion ()) + DrawContentBetaWatermark (waterMarkOffset); + #endif // UNITY_IS_BETA +#endif + +#if UNITY_CAN_SHOW_SPLASH_SCREEN + if (GetShouldShowSplashScreen()) + DrawSplashScreen (false); +#endif +} diff --git a/Runtime/Graphics/DrawSplashScreenAndWatermarks.h b/Runtime/Graphics/DrawSplashScreenAndWatermarks.h new file mode 100644 index 0000000..b323456 --- /dev/null +++ b/Runtime/Graphics/DrawSplashScreenAndWatermarks.h @@ -0,0 +1,18 @@ +#pragma once + +void DrawSplashAndWatermarks(); +void DrawWaterMark( bool alwaysDisplay ); + +#if UNITY_CAN_SHOW_SPLASH_SCREEN + +void BeginSplashScreenFade(); +bool IsSplashScreenFadeComplete(); +bool GetShouldShowSplashScreen(); +void DrawSplashScreen (bool fullDraw); +#endif + +#if WEBPLUG +void ShowFullscreenEscapeWarning (); +void RenderFullscreenEscapeWarning (); +extern double gDisplayFullscreenEscapeTimeout; +#endif diff --git a/Runtime/Graphics/DrawUtil.cpp b/Runtime/Graphics/DrawUtil.cpp new file mode 100644 index 0000000..4c17fe8 --- /dev/null +++ b/Runtime/Graphics/DrawUtil.cpp @@ -0,0 +1,173 @@ +#include "UnityPrefix.h" +#include "DrawUtil.h" +#include "Runtime/Math/Vector3.h" +#include "Runtime/Math/Quaternion.h" +#include "Runtime/Math/Matrix4x4.h" +#include "Runtime/Shaders/VBO.h" +#include "Runtime/GfxDevice/GfxDevice.h" +#include "Runtime/Filters/Mesh/LodMesh.h" +#include "Runtime/GfxDevice/ChannelAssigns.h" +#include "Runtime/Camera/Camera.h" +#include "Runtime/Camera/RenderManager.h" +#include "Transform.h" +#include "Runtime/GfxDevice/GfxDevice.h" +#include "Runtime/GfxDevice/GfxDeviceStats.h" +#include "Runtime/GfxDevice/BatchRendering.h" +#include "Runtime/Camera/Renderqueue.h" +#include "Runtime/Profiler/Profiler.h" +#include "Runtime/Shaders/ComputeShader.h" +#include "Runtime/Graphics/SpriteFrame.h" +#if UNITY_PS3 +# include "Runtime/GfxDevice/ps3/GfxGCMVBO.h" +#endif + +PROFILER_INFORMATION(gDrawMeshVBOProfile, "Mesh.DrawVBO", kProfilerRender) +PROFILER_INFORMATION(gDrawMeshNullProfile, "Graphics.DrawProcedural", kProfilerRender) + +static void DrawMeshInternal (const ChannelAssigns& channels, Mesh &mesh, const Matrix4x4f &matrix, int subsetIndex, TransformType transformType); + +void DrawUtil::DrawVBOMeshRaw (VBO& vbo, Mesh& mesh, const ChannelAssigns& channels, int subsetIndex, UInt32 channelsInVBO) +{ + PROFILER_AUTO(gDrawMeshVBOProfile, &mesh) + + subsetIndex = std::min<unsigned int>(subsetIndex, mesh.GetSubMeshCount()? mesh.GetSubMeshCount()-1:0); + SubMesh& submesh = mesh.GetSubMeshFast(subsetIndex); + + int firstVertex, vertexCount; + + firstVertex = submesh.firstVertex; + vertexCount = submesh.vertexCount; +#if UNITY_PS3 + GfxGCMVBO& gcmVBO = (GfxGCMVBO&)vbo; + gcmVBO.DrawSubmesh(channels, subsetIndex, &submesh); +#else + vbo.DrawVBO (channels, submesh.firstByte, submesh.indexCount, submesh.topology, firstVertex, vertexCount); +#endif + GPU_TIMESTAMP(); +} + +void DrawUtil::DrawMeshRaw (const ChannelAssigns& channels, Mesh& mesh, int subsetIndex) +{ + VBO* vbo = mesh.GetSharedVBO (channels.GetSourceMap()); + if( vbo != NULL ) + { + DrawVBOMeshRaw (*vbo, mesh, channels, subsetIndex); + } +} + +void DrawUtil::DrawMesh (const ChannelAssigns& channels, Mesh &mesh, const Vector3f &position, const Quaternionf &rotation, int subsetIndex) +{ + Matrix4x4f matrix; + QuaternionToMatrix (rotation, matrix); + matrix.SetPosition( position ); + + TransformType transformType = kNoScaleTransform; + DrawMeshInternal (channels, mesh, matrix, subsetIndex, transformType); +} + +void DrawUtil::DrawMesh (const ChannelAssigns& channels, Mesh &mesh, const Matrix4x4f &matrix, int subsetIndex) + { + // just assume it is a scaled transform, but don't handle negative scales... oh well! (says @aras_p) + TransformType transformType = kUniformScaleTransform; + DrawMeshInternal (channels, mesh, matrix, subsetIndex, transformType); +} + +static void DrawMeshInternal (const ChannelAssigns& channels, Mesh &mesh, const Matrix4x4f &matrix, int subsetIndex, TransformType transformType) +{ + const Camera* camera = GetCurrentCameraPtr(); + + GfxDevice& device = GetGfxDevice(); + float matWorld[16], matView[16]; + + CopyMatrix(device.GetViewMatrix(), matView); + CopyMatrix(device.GetWorldMatrix(), matWorld); + if (camera) + device.SetViewMatrix( camera->GetWorldToCameraMatrix().GetPtr() ); + + SetupObjectMatrix (matrix, transformType); + + if (subsetIndex != -1) + { + DrawUtil::DrawMeshRaw (channels, mesh, subsetIndex); + } + else + { + int submeshCount = mesh.GetSubMeshCount(); + for (int i=0;i<submeshCount;i++) + DrawUtil::DrawMeshRaw (channels, mesh, i); + } + + device.SetViewMatrix(matView); + device.SetWorldMatrix(matWorld); +} + +void DrawUtil::DrawProcedural (GfxPrimitiveType topology, int vertexCount, int instanceCount) +{ + if (instanceCount > 1 && !gGraphicsCaps.hasInstancing) + { + ErrorString ("Can't do instanced Graphics.DrawProcedural"); + return; + } + + PROFILER_AUTO(gDrawMeshNullProfile, NULL) + + GfxDevice& device = GetGfxDevice(); + device.DrawNullGeometry (topology, vertexCount, instanceCount); + device.GetFrameStats().AddDrawCall (vertexCount*instanceCount, vertexCount*instanceCount); + + GPU_TIMESTAMP(); +} + +void DrawUtil::DrawProceduralIndirect (GfxPrimitiveType topology, ComputeBuffer* bufferWithArgs, UInt32 argsOffset) +{ + if (!gGraphicsCaps.hasInstancing || !gGraphicsCaps.hasComputeShader) + { + ErrorString ("Can't do indirect Graphics.DrawProcedural"); + return; + } + if (!bufferWithArgs) + { + ErrorString ("Graphics.DrawProcedural with invalid buffer"); + return; + } + ComputeBufferID bufferHandle = bufferWithArgs->GetBufferHandle(); + if (!bufferHandle.IsValid()) + { + ErrorString ("Graphics.DrawProcedural with invalid buffer"); + return; + } + + PROFILER_AUTO(gDrawMeshNullProfile, NULL) + + GfxDevice& device = GetGfxDevice(); + device.DrawNullGeometryIndirect (topology, bufferHandle, argsOffset); + device.GetFrameStats().AddDrawCall (1,1); // unknown primitive count + + GPU_TIMESTAMP(); +} + +void DrawUtil::DrawSpriteRaw (const ChannelAssigns& channels, Sprite& sprite, const ColorRGBAf& color) +{ + GfxDevice& device = GetGfxDevice(); + + const SpriteRenderData& rd = sprite.GetRenderData(false); // Use non-atlased RenderData as input. + Assert(rd.texture.IsValid()); + + // Get VBO chunk for a rectangle or mesh + UInt32 numIndices = rd.indices.size(); + UInt32 numVertices = rd.vertices.size(); + if (!numIndices) + return; + + const UInt32 channelMask = (1<<kShaderChannelVertex) | (1<<kShaderChannelTexCoord0) | (1<<kShaderChannelColor); + + DynamicVBO& vbo = device.GetDynamicVBO(); + UInt8* __restrict vbPtr; + UInt16* __restrict ibPtr; + if ( !vbo.GetChunk(channelMask, numVertices, numIndices, DynamicVBO::kDrawIndexedTriangles, (void**)&vbPtr, (void**)&ibPtr) ) + return; + + TransformSprite (vbPtr, ibPtr, NULL, &rd, device.ConvertToDeviceVertexColor(color), 0); + vbo.ReleaseChunk(numVertices, numIndices); + vbo.DrawChunk(channels); +} diff --git a/Runtime/Graphics/DrawUtil.h b/Runtime/Graphics/DrawUtil.h new file mode 100644 index 0000000..52bf156 --- /dev/null +++ b/Runtime/Graphics/DrawUtil.h @@ -0,0 +1,51 @@ +#ifndef DRAWUTIL_H +#define DRAWUTIL_H + +#include "Runtime/GfxDevice/GfxDeviceTypes.h" +#include "Runtime/Modules/ExportModules.h" + +namespace Unity { class Material; } +class ChannelAssigns; +using namespace Unity; +class Vector2f; +class Vector3f; +class ColorRGBAf; +class Quaternionf; +class Mesh; +class VBO; +class Matrix4x4f; +class ComputeBuffer; +class Sprite; + + +enum MeshPrimitiveType +{ + kTriangles = 0, + kLines = 1, +}; + +// Utilities for drawing +struct EXPORT_COREMODULE DrawUtil +{ + static void DrawVBOMeshRaw (VBO& vbo, Mesh& mesh, const ChannelAssigns& channels, int materialIndex, UInt32 channelsInVBO = ~0UL); + + /// Draw a mesh with the current matrix + static void DrawMeshRaw (const ChannelAssigns& channels, Mesh &mesh, int materialIndex); + + static void DrawMesh (const ChannelAssigns& channels, Mesh &mesh, const Vector3f &position, const Quaternionf &rotation, int materialIndex = -1); + static void DrawMesh (const ChannelAssigns& channels, Mesh &mesh, const Matrix4x4f &matrix, int materialIndex); + + static void DrawProcedural (GfxPrimitiveType topology, int vertexCount, int instanceCount); + + // args in ComputeBuffer at given offset: + // uint32 vertexCountPerInstance + // uint32 instanceCount + // uint32 startVertexLocation + // uint32 startInstanceLocation + static void DrawProceduralIndirect (GfxPrimitiveType topology, ComputeBuffer* bufferWithArgs, UInt32 argsOffset); + + + static void DrawSpriteRaw (const ChannelAssigns& channels, Sprite& sprite, const ColorRGBAf& color); +}; + +#endif diff --git a/Runtime/Graphics/ETC2Decompression.cpp b/Runtime/Graphics/ETC2Decompression.cpp new file mode 100644 index 0000000..d2f8169 --- /dev/null +++ b/Runtime/Graphics/ETC2Decompression.cpp @@ -0,0 +1,487 @@ +#include "ETC2Decompression.h" + +// Utilities + +template<typename T> +static inline T InRange (T v, T a, T b) +{ + return a <= v && v <= b; +} + +template<typename T> +static inline T Clamp (T v, T a, T b) +{ + if (v < a) return a; + else if (v > b) return b; + else return v; +} + +static inline int DivRoundUp (int a, int b) +{ + return a/b + ((a%b) ? 1 : 0); +} + +static inline UInt32 GetSingleBit (UInt64 src, int bit) +{ + return (src >> bit) & 1; +} + +static inline UInt32 GetBitRange (UInt64 src, int low, int high) +{ + int numBits = (high-low) + 1; + return (src >> low) & ((1<<numBits)-1); +} + +static inline UInt8 ExtendTo8 (UInt8 src, UInt32 fromBits) +{ + return (src << (8-fromBits)) | (src >> (2*fromBits - 8)); +} + +static inline SInt8 Extend3BitSignedDelta (UInt8 src) +{ + bool isNeg = (src & (1<<2)) != 0; + return (SInt8)((isNeg ? ~((1<<3)-1) : 0) | src); +} + +static inline UInt16 Extend11To16 (UInt16 src) +{ + return (src << 5) | (src >> 6); +} + +static inline SInt16 Extend11To16WithSign (SInt16 src) +{ + if (src < 0) + return -(SInt16)Extend11To16(-src); + else + return (SInt16)Extend11To16(src); +} + +// Block access utilities + +static inline UInt64 Get64BitBlock (const UInt8* src, int blockNdx) +{ + UInt64 block = 0; + for (int i = 0; i < 8; i++) + block = (block << 8ull) | (UInt64)(src[blockNdx*8+i]); + return block; +} + +static inline UInt64 Get128BitBlockStart (const UInt8* src, int blockNdx) +{ + return Get64BitBlock(src, 2*blockNdx); +} + +static inline UInt64 Get128BitBlockEnd (const UInt8* src, int blockNdx) +{ + return Get64BitBlock(src, 2*blockNdx + 1); +} + +// Block decompression routines + +static void DecompressETC2Block (UInt64 src, UInt8 dst[kETC2UncompressedBlockSizeRGB8], UInt8 alphaDst[kETC2UncompressedBlockSizeA8], bool alphaMode) +{ + typedef enum BlockMode_e + { + kBlockModeIndividual = 0, + kBlockModeDifferential, + kBlockModeT, + kBlockModeH, + kBlockModePlanar, + kBlockModeCount + } BlockMode; + + int diffOpaqueBit = (int)GetSingleBit(src, 33); + SInt8 selBR = (SInt8)GetBitRange(src, 59, 63); // 5 bits. + SInt8 selBG = (SInt8)GetBitRange(src, 51, 55); + SInt8 selBB = (SInt8)GetBitRange(src, 43, 47); + SInt8 selDR = Extend3BitSignedDelta((UInt8)GetBitRange(src, 56, 58)); // 3 bits. + SInt8 selDG = Extend3BitSignedDelta((UInt8)GetBitRange(src, 48, 50)); + SInt8 selDB = Extend3BitSignedDelta((UInt8)GetBitRange(src, 40, 42)); + BlockMode mode; + + if (!alphaMode && diffOpaqueBit == 0) + mode = kBlockModeIndividual; + else if (!InRange(selBR + selDR, 0, 31)) + mode = kBlockModeT; + else if (!InRange(selBG + selDG, 0, 31)) + mode = kBlockModeH; + else if (!InRange(selBB + selDB, 0, 31)) + mode = kBlockModePlanar; + else + mode = kBlockModeDifferential; + + if (mode == kBlockModeIndividual || mode == kBlockModeDifferential) + { + // A lot of logic is shared between individual and differential mode + static const int modifierLUT[8][4] = + { + // 00 01 10 11 + { 2, 8, -2, -8 }, + { 5, 17, -5, -17 }, + { 9, 29, -9, -29 }, + { 13, 42, -13, -42 }, + { 18, 60, -18, -60 }, + { 24, 80, -24, -80 }, + { 33, 106, -33, -106 }, + { 47, 183, -47, -183 } + }; + + int flipBit = (int)GetSingleBit(src, 32); + UInt32 table[2] = { GetBitRange(src, 37, 39), GetBitRange(src, 34, 36) }; + UInt8 baseR[2]; + UInt8 baseG[2]; + UInt8 baseB[2]; + + // Base values per mode + if (mode == kBlockModeIndividual) + { + baseR[0] = ExtendTo8((UInt8)GetBitRange(src, 60, 63), 4); + baseR[1] = ExtendTo8((UInt8)GetBitRange(src, 56, 59), 4); + baseG[0] = ExtendTo8((UInt8)GetBitRange(src, 52, 55), 4); + baseG[1] = ExtendTo8((UInt8)GetBitRange(src, 48, 51), 4); + baseB[0] = ExtendTo8((UInt8)GetBitRange(src, 44, 47), 4); + baseB[1] = ExtendTo8((UInt8)GetBitRange(src, 40, 43), 4); + } + else + { + baseR[0] = ExtendTo8(selBR, 5); + baseG[0] = ExtendTo8(selBG, 5); + baseB[0] = ExtendTo8(selBB, 5); + + baseR[1] = ExtendTo8((UInt8)(selBR + selDR), 5); + baseG[1] = ExtendTo8((UInt8)(selBG + selDG), 5); + baseB[1] = ExtendTo8((UInt8)(selBB + selDB), 5); + } + + for (int pixelNdx = 0; pixelNdx < kETC2BlockHeight*kETC2BlockWidth; pixelNdx++) + { + int x = pixelNdx / kETC2BlockHeight; + int y = pixelNdx % kETC2BlockHeight; + int dstOffset = (y*kETC2BlockWidth + x)*kETC2UncompressedPixelSizeRGB8; + int subBlock = ((flipBit ? y : x) >= 2) ? 1 : 0; + UInt32 tableNdx = table[subBlock]; + UInt32 modifierNdx = (GetSingleBit(src, 16+pixelNdx) << 1) | GetSingleBit(src, pixelNdx); + int alphaDstOffset = (y*kETC2BlockWidth + x)*kETC2UncompressedPixelSizeA8; // Only needed for PUNCHTHROUGH version. + + if (alphaMode && diffOpaqueBit == 0 && modifierNdx == 2) + { + // If doing PUNCHTHROUGH version (alphaMode), opaque bit may affect colors. + dst[dstOffset+0] = 0; + dst[dstOffset+1] = 0; + dst[dstOffset+2] = 0; + alphaDst[alphaDstOffset] = 0; + } + else + { + int modifier; + + // PUNCHTHROUGH version and opaque bit may also affect modifiers. + if (alphaMode && diffOpaqueBit == 0 && (modifierNdx == 0 || modifierNdx == 2)) + modifier = 0; + else + modifier = modifierLUT[tableNdx][modifierNdx]; + + dst[dstOffset+0] = (UInt8)Clamp<int>((int)baseR[subBlock] + modifier, 0, 255); + dst[dstOffset+1] = (UInt8)Clamp<int>((int)baseG[subBlock] + modifier, 0, 255); + dst[dstOffset+2] = (UInt8)Clamp<int>((int)baseB[subBlock] + modifier, 0, 255); + + if (alphaMode) + alphaDst[alphaDstOffset] = 255; + } + } + } + else if (mode == kBlockModeT || mode == kBlockModeH) + { + static const int distTable[8] = { 3, 6, 11, 16, 23, 32, 41, 64 }; + + UInt8 paintR[4]; + UInt8 paintG[4]; + UInt8 paintB[4]; + + if (mode == kBlockModeT) + { + // T mode, calculate paint values. + UInt8 R1a = (UInt8)GetBitRange(src, 59, 60); + UInt8 R1b = (UInt8)GetBitRange(src, 56, 57); + UInt8 G1 = (UInt8)GetBitRange(src, 52, 55); + UInt8 B1 = (UInt8)GetBitRange(src, 48, 51); + UInt8 R2 = (UInt8)GetBitRange(src, 44, 47); + UInt8 G2 = (UInt8)GetBitRange(src, 40, 43); + UInt8 B2 = (UInt8)GetBitRange(src, 36, 39); + UInt32 distNdx = (GetBitRange(src, 34, 35) << 1) | GetSingleBit(src, 32); + int dist = distTable[distNdx]; + + paintR[0] = ExtendTo8(((R1a << 2) | R1b), 4); + paintG[0] = ExtendTo8(G1, 4); + paintB[0] = ExtendTo8(B1, 4); + paintR[2] = ExtendTo8(R2, 4); + paintG[2] = ExtendTo8(G2, 4); + paintB[2] = ExtendTo8(B2, 4); + paintR[1] = (UInt8)Clamp<int>((int)paintR[2] + dist, 0, 255); + paintG[1] = (UInt8)Clamp<int>((int)paintG[2] + dist, 0, 255); + paintB[1] = (UInt8)Clamp<int>((int)paintB[2] + dist, 0, 255); + paintR[3] = (UInt8)Clamp<int>((int)paintR[2] - dist, 0, 255); + paintG[3] = (UInt8)Clamp<int>((int)paintG[2] - dist, 0, 255); + paintB[3] = (UInt8)Clamp<int>((int)paintB[2] - dist, 0, 255); + } + else + { + // H mode, calculate paint values. + UInt8 R1 = (UInt8)GetBitRange(src, 59, 62); + UInt8 G1a = (UInt8)GetBitRange(src, 56, 58); + UInt8 G1b = (UInt8)GetSingleBit(src, 52); + UInt8 B1a = (UInt8)GetSingleBit(src, 51); + UInt8 B1b = (UInt8)GetBitRange(src, 47, 49); + UInt8 R2 = (UInt8)GetBitRange(src, 43, 46); + UInt8 G2 = (UInt8)GetBitRange(src, 39, 42); + UInt8 B2 = (UInt8)GetBitRange(src, 35, 38); + UInt8 baseR[2]; + UInt8 baseG[2]; + UInt8 baseB[2]; + UInt32 baseValue[2]; + UInt32 distNdx; + int dist; + + baseR[0] = ExtendTo8(R1, 4); + baseG[0] = ExtendTo8(((G1a << 1) | G1b), 4); + baseB[0] = ExtendTo8(((B1a << 3) | B1b), 4); + baseR[1] = ExtendTo8(R2, 4); + baseG[1] = ExtendTo8(G2, 4); + baseB[1] = ExtendTo8(B2, 4); + baseValue[0] = (((UInt32)baseR[0]) << 16) | (((UInt32)baseG[0]) << 8) | baseB[0]; + baseValue[1] = (((UInt32)baseR[1]) << 16) | (((UInt32)baseG[1]) << 8) | baseB[1]; + distNdx = (GetSingleBit(src, 34) << 2) | (GetSingleBit(src, 32) << 1) | (UInt32)(baseValue[0] >= baseValue[1]); + dist = distTable[distNdx]; + + paintR[0] = (UInt8)Clamp<int>((int)baseR[0] + dist, 0, 255); + paintG[0] = (UInt8)Clamp<int>((int)baseG[0] + dist, 0, 255); + paintB[0] = (UInt8)Clamp<int>((int)baseB[0] + dist, 0, 255); + paintR[1] = (UInt8)Clamp<int>((int)baseR[0] - dist, 0, 255); + paintG[1] = (UInt8)Clamp<int>((int)baseG[0] - dist, 0, 255); + paintB[1] = (UInt8)Clamp<int>((int)baseB[0] - dist, 0, 255); + paintR[2] = (UInt8)Clamp<int>((int)baseR[1] + dist, 0, 255); + paintG[2] = (UInt8)Clamp<int>((int)baseG[1] + dist, 0, 255); + paintB[2] = (UInt8)Clamp<int>((int)baseB[1] + dist, 0, 255); + paintR[3] = (UInt8)Clamp<int>((int)baseR[1] - dist, 0, 255); + paintG[3] = (UInt8)Clamp<int>((int)baseG[1] - dist, 0, 255); + paintB[3] = (UInt8)Clamp<int>((int)baseB[1] - dist, 0, 255); + } + + for (int pixelNdx = 0; pixelNdx < kETC2BlockHeight*kETC2BlockWidth; pixelNdx++) + { + int x = pixelNdx / kETC2BlockHeight; + int y = pixelNdx % kETC2BlockHeight; + int dstOffset = (y*kETC2BlockWidth + x)*kETC2UncompressedPixelSizeRGB8; + UInt32 paintNdx = (GetSingleBit(src, 16+pixelNdx) << 1) | GetSingleBit(src, pixelNdx); + int alphaDstOffset = (y*kETC2BlockWidth + x)*kETC2UncompressedPixelSizeA8; // Only needed for PUNCHTHROUGH version. + + if (alphaMode && diffOpaqueBit == 0 && paintNdx == 2) + { + dst[dstOffset+0] = 0; + dst[dstOffset+1] = 0; + dst[dstOffset+2] = 0; + alphaDst[alphaDstOffset] = 0; + } + else + { + dst[dstOffset+0] = (UInt8)Clamp<int>((int)paintR[paintNdx], 0, 255); + dst[dstOffset+1] = (UInt8)Clamp<int>((int)paintG[paintNdx], 0, 255); + dst[dstOffset+2] = (UInt8)Clamp<int>((int)paintB[paintNdx], 0, 255); + + if (alphaMode) + alphaDst[alphaDstOffset] = 255; + } + } + } + else + { + // Planar mode. + UInt8 GO1 = (UInt8)GetSingleBit(src, 56); + UInt8 GO2 = (UInt8)GetBitRange(src, 49, 54); + UInt8 BO1 = (UInt8)GetSingleBit(src, 48); + UInt8 BO2 = (UInt8)GetBitRange(src, 43, 44); + UInt8 BO3 = (UInt8)GetBitRange(src, 39, 41); + UInt8 RH1 = (UInt8)GetBitRange(src, 34, 38); + UInt8 RH2 = (UInt8)GetSingleBit(src, 32); + UInt8 RO = ExtendTo8((UInt8)GetBitRange(src, 57, 62), 6); + UInt8 GO = ExtendTo8(((GO1 << 6) | GO2), 7); + UInt8 BO = ExtendTo8(((BO1 << 5) | (BO2 << 3) | BO3), 6); + UInt8 RH = ExtendTo8(((RH1 << 1) | RH2), 6); + UInt8 GH = ExtendTo8((UInt8)GetBitRange(src, 25, 31), 7); + UInt8 BH = ExtendTo8((UInt8)GetBitRange(src, 19, 24), 6); + UInt8 RV = ExtendTo8((UInt8)GetBitRange(src, 13, 18), 6); + UInt8 GV = ExtendTo8((UInt8)GetBitRange(src, 6, 12), 7); + UInt8 BV = ExtendTo8((UInt8)GetBitRange(src, 0, 5), 6); + + for (int y = 0; y < 4; y++) + { + for (int x = 0; x < 4; x++) + { + int dstOffset = (y*kETC2BlockWidth + x)*kETC2UncompressedPixelSizeRGB8; + int unclampedR = (x * ((int)RH-(int)RO) + y * ((int)RV-(int)RO) + 4*(int)RO + 2) >> 2; + int unclampedG = (x * ((int)GH-(int)GO) + y * ((int)GV-(int)GO) + 4*(int)GO + 2) >> 2; + int unclampedB = (x * ((int)BH-(int)BO) + y * ((int)BV-(int)BO) + 4*(int)BO + 2) >> 2; + int alphaDstOffset = (y*kETC2BlockWidth + x)*kETC2UncompressedPixelSizeA8; // Only needed for PUNCHTHROUGH version. + + dst[dstOffset+0] = (UInt8)Clamp<int>(unclampedR, 0, 255); + dst[dstOffset+1] = (UInt8)Clamp<int>(unclampedG, 0, 255); + dst[dstOffset+2] = (UInt8)Clamp<int>(unclampedB, 0, 255); + + if (alphaMode) + alphaDst[alphaDstOffset] = 255; + } + } + } +} + +static void DecompressEAC8Block (UInt8 dst[kETC2UncompressedBlockSizeA8], UInt64 src) +{ + static const int modifierLUT[16][8] = + { + { -3, -6, -9, -15, 2, 5, 8, 14 }, + { -3, -7, -10, -13, 2, 6, 9, 12 }, + { -2, -5, -8, -13, 1, 4, 7, 12 }, + { -2, -4, -6, -13, 1, 3, 5, 12 }, + { -3, -6, -8, -12, 2, 5, 7, 11 }, + { -3, -7, -9, -11, 2, 6, 8, 10 }, + { -4, -7, -8, -11, 3, 6, 7, 10 }, + { -3, -5, -8, -11, 2, 4, 7, 10 }, + { -2, -6, -8, -10, 1, 5, 7, 9 }, + { -2, -5, -8, -10, 1, 4, 7, 9 }, + { -2, -4, -8, -10, 1, 3, 7, 9 }, + { -2, -5, -7, -10, 1, 4, 6, 9 }, + { -3, -4, -7, -10, 2, 3, 6, 9 }, + { -1, -2, -3, -10, 0, 1, 2, 9 }, + { -4, -6, -8, -9, 3, 5, 7, 8 }, + { -3, -5, -7, -9, 2, 4, 6, 8 } + }; + + UInt8 baseCodeword = (UInt8)GetBitRange(src, 56, 63); + UInt8 multiplier = (UInt8)GetBitRange(src, 52, 55); + UInt32 tableNdx = GetBitRange(src, 48, 51); + + for (int pixelNdx = 0; pixelNdx < kETC2BlockHeight*kETC2BlockWidth; pixelNdx++) + { + int x = pixelNdx / kETC2BlockHeight; + int y = pixelNdx % kETC2BlockHeight; + int dstOffset = (y*kETC2BlockWidth + x)*kETC2UncompressedPixelSizeA8; + int pixelBitNdx = 45 - 3*pixelNdx; + UInt32 modifierNdx = (GetSingleBit(src, pixelBitNdx + 2) << 2) | (GetSingleBit(src, pixelBitNdx + 1) << 1) | GetSingleBit(src, pixelBitNdx); + int modifier = modifierLUT[tableNdx][modifierNdx]; + + dst[dstOffset] = (UInt8)Clamp<int>((int)baseCodeword + (int)multiplier*modifier, 0, 255); + } +} + +// Decompression API + +void DecompressETC2_RGB8 (UInt8* dst, const UInt8* src, int width, int height) +{ + int numBlocksX = DivRoundUp(width, 4); + int numBlocksY = DivRoundUp(height, 4); + int dstPixelSize = kETC2UncompressedPixelSizeRGBA8; + int dstRowPitch = width*dstPixelSize; + + for (int blockY = 0; blockY < numBlocksY; blockY++) + { + for (int blockX = 0; blockX < numBlocksX; blockX++) + { + UInt64 compressedBlock = Get64BitBlock(src, blockY*numBlocksX + blockX); + UInt8 uncompressedBlock[kETC2UncompressedBlockSizeRGB8]; + + DecompressETC2Block(compressedBlock, uncompressedBlock, NULL /* no alpha */, false); + + int baseX = blockX*kETC2BlockWidth; + int baseY = blockY*kETC2BlockHeight; + for (int y = 0; y < std::min((int)kETC2BlockHeight, height-baseY); y++) + { + for (int x = 0; x < std::min((int)kETC2BlockWidth, width-baseX); x++) + { + const UInt8* srcPixel = &uncompressedBlock[(y*kETC2BlockWidth + x)*kETC2UncompressedPixelSizeRGB8]; + UInt8* dstPixel = dst + (baseY+y)*dstRowPitch + (baseX+x)*dstPixelSize; + + dstPixel[0] = srcPixel[0]; + dstPixel[1] = srcPixel[1]; + dstPixel[2] = srcPixel[2]; + dstPixel[3] = 0xff; // Force alpha to 1 + } + } + } + } +} + +void DecompressETC2_RGB8_A1 (UInt8* dst, const UInt8* src, int width, int height) +{ + int numBlocksX = DivRoundUp(width, 4); + int numBlocksY = DivRoundUp(height, 4); + int dstPixelSize = kETC2UncompressedPixelSizeRGBA8; + int dstRowPitch = width*dstPixelSize; + + for (int blockY = 0; blockY < numBlocksY; blockY++) + { + for (int blockX = 0; blockX < numBlocksX; blockX++) + { + UInt64 compressedBlockRGBA = Get64BitBlock(src, blockY*numBlocksX + blockX); + UInt8 uncompressedBlockRGB[kETC2UncompressedBlockSizeRGB8]; + UInt8 uncompressedBlockAlpha[kETC2UncompressedBlockSizeA8]; + + DecompressETC2Block(compressedBlockRGBA, uncompressedBlockRGB, uncompressedBlockAlpha, true); + + int baseX = blockX*kETC2BlockWidth; + int baseY = blockY*kETC2BlockHeight; + for (int y = 0; y < std::min((int)kETC2BlockHeight, height-baseY); y++) + { + for (int x = 0; x < std::min((int)kETC2BlockWidth, width-baseX); x++) + { + const UInt8* srcPixel = &uncompressedBlockRGB[(y*kETC2BlockWidth + x)*kETC2UncompressedPixelSizeRGB8]; + const UInt8* srcPixelAlpha = &uncompressedBlockAlpha[(y*kETC2BlockWidth + x)*kETC2UncompressedPixelSizeA8]; + UInt8* dstPixel = dst + (baseY+y)*dstRowPitch + (baseX+x)*dstPixelSize; + + dstPixel[0] = srcPixel[0]; + dstPixel[1] = srcPixel[1]; + dstPixel[2] = srcPixel[2]; + dstPixel[3] = srcPixelAlpha[0]; + } + } + } + } +} + +void DecompressETC2_RGBA8 (UInt8* dst, const UInt8* src, int width, int height) +{ + int numBlocksX = DivRoundUp(width, 4); + int numBlocksY = DivRoundUp(height, 4); + int dstPixelSize = kETC2UncompressedPixelSizeRGBA8; + int dstRowPitch = width*dstPixelSize; + + for (int blockY = 0; blockY < numBlocksY; blockY++) + { + for (int blockX = 0; blockX < numBlocksX; blockX++) + { + UInt64 compressedBlockAlpha = Get128BitBlockStart(src, blockY*numBlocksX + blockX); + UInt64 compressedBlockRGB = Get128BitBlockEnd(src, blockY*numBlocksX + blockX); + UInt8 uncompressedBlockAlpha[kETC2UncompressedBlockSizeA8]; + UInt8 uncompressedBlockRGB[kETC2UncompressedBlockSizeRGB8]; + + DecompressETC2Block(compressedBlockRGB, uncompressedBlockRGB, NULL /* no alpha */, false); + DecompressEAC8Block(uncompressedBlockAlpha, compressedBlockAlpha); + + int baseX = blockX*kETC2BlockWidth; + int baseY = blockY*kETC2BlockHeight; + for (int y = 0; y < std::min((int)kETC2BlockHeight, height-baseY); y++) + { + for (int x = 0; x < std::min((int)kETC2BlockWidth, width-baseX); x++) + { + const UInt8* srcPixelRGB = &uncompressedBlockRGB[(y*kETC2BlockWidth + x)*kETC2UncompressedPixelSizeRGB8]; + const UInt8* srcPixelAlpha = &uncompressedBlockAlpha[(y*kETC2BlockWidth + x)*kETC2UncompressedPixelSizeA8]; + UInt8* dstPixel = dst + (baseY+y)*dstRowPitch + (baseX+x)*dstPixelSize; + + dstPixel[0] = srcPixelRGB[0]; + dstPixel[1] = srcPixelRGB[1]; + dstPixel[2] = srcPixelRGB[2]; + dstPixel[3] = srcPixelAlpha[0]; + } + } + } + } +} diff --git a/Runtime/Graphics/ETC2Decompression.h b/Runtime/Graphics/ETC2Decompression.h new file mode 100644 index 0000000..5e97047 --- /dev/null +++ b/Runtime/Graphics/ETC2Decompression.h @@ -0,0 +1,39 @@ +#ifndef ETC2DECOMPRESSION_H +#define ETC2DECOMPRESSION_H + +#include "UnityPrefix.h" + +// ETC-2 decompressor +// +// Input is expected to be in standard ETC-2 (+EAC) memory layout. In ETC2 block size is 64b +// and alpha EAC block adds another 64b. +// +// Output must be RGBA with 8 bits for each channel, stored in R, G, B, A order +// (from offset 0 to 3). +// +// Texture sizes that are not exactly divisible by block size are supported. In such +// case block count is rounded up and extra decoded pixels are just ignored. + +//! ETC2 memory layout configuration +enum +{ + kETC2BlockWidth = 4, + kETC2BlockHeight = 4, + kETC2UncompressedPixelSizeA8 = 1, + kETC2UncompressedPixelSizeRGB8 = 3, + kETC2UncompressedPixelSizeRGBA8 = 4, + kETC2UncompressedBlockSizeA8 = kETC2BlockWidth*kETC2BlockHeight*kETC2UncompressedPixelSizeA8, + kETC2UncompressedBlockSizeRGB8 = kETC2BlockWidth*kETC2BlockHeight*kETC2UncompressedPixelSizeRGB8, + kETC2UncompressedBlockSizeRGBA8 = kETC2BlockWidth*kETC2BlockHeight*kETC2UncompressedPixelSizeRGBA8 +}; + +//! Decompress ETC2 to RGBA8 buffer +void DecompressETC2_RGB8 (UInt8* dst, const UInt8* src, int width, int height); + +//! Decompress ETC2 with punchthrough alpha to RGBA8 buffer +void DecompressETC2_RGB8_A1 (UInt8* dst, const UInt8* src, int width, int height); + +//! Decompress ETC2+EAC to RGBA8 buffer +void DecompressETC2_RGBA8 (UInt8* dst, const UInt8* src, int width, int height); + +#endif diff --git a/Runtime/Graphics/FlashATFDecompression.h b/Runtime/Graphics/FlashATFDecompression.h new file mode 100644 index 0000000..9819dbe --- /dev/null +++ b/Runtime/Graphics/FlashATFDecompression.h @@ -0,0 +1,7 @@ + +#if HAS_FLASH_ATF_DECOMPRESSOR + +// Flash ATFFormat to kTexFormatRGBA32 +void DecompressFlashATFTexture ( const UInt8* srcData, int dstWidth, int dstHeight, int mipLevel, UInt8* dstData ); + +#endif
\ No newline at end of file diff --git a/Runtime/Graphics/GeneratedTextures.cpp b/Runtime/Graphics/GeneratedTextures.cpp new file mode 100644 index 0000000..c27eb2c --- /dev/null +++ b/Runtime/Graphics/GeneratedTextures.cpp @@ -0,0 +1,258 @@ +#include "UnityPrefix.h" +#include "External/shaderlab/Library/properties.h" +#include "Texture.h" +#include "External/shaderlab/Library/texenv.h" +#include "GeneratedTextures.h" +#include "Runtime/Shaders/GraphicsCaps.h" +#include "Runtime/Camera/Light.h" +#include "Runtime/Math/Random/rand.h" +#include "Runtime/Math/Random/Random.h" +#include "TextureGenerator.h" +#include "Runtime/GfxDevice/BuiltinShaderParams.h" +#include "Runtime/GfxDevice/GfxDevice.h" + +#include <limits> + +static PPtr<Texture2D> gWhiteTex = NULL; +static PPtr<Texture2D> gBlackTex = NULL; +static PPtr<Texture2D> gAttenuationTex = NULL; +static PPtr<Texture2D> gHaloTex = NULL; +static PPtr<Texture3D> gDitherMaskTex = NULL; +static PPtr<Texture2D> s_RandomRotationTex; +static PPtr<Texture2D> s_NormalMapTex; +static PPtr<Texture2D> s_RedTex; +static PPtr<Texture2D> s_GrayTex; +static PPtr<Texture2D> s_GrayRampTex; + + +static TextureID gDefaultTextures[kTexDimCount]; + +static Rand gRandomSeed = 0; + +using namespace ShaderLab; +using namespace std; + + + +inline void EmptyNormalMap (Texture2D *tex, unsigned char *data, int x, int y, int maxX, int maxY) { + // make "empty normal map" work in both plain encoding and DXT5nm + data[0] = 127; // X=0.5 for plain + data[1] = 127; // Y=0.5 for plain & DXT5nm + data[2] = 255; // Z=1.0 for plain + data[3] = 127; // X=0.5 for DXT5nm +} + +inline void White (Texture2D *tex, unsigned char *data, int x, int y, int maxX, int maxY) { + data[0] = 255; + data[1] = 255; + data[2] = 255; + data[3] = 255; +} + +inline void GrayscaleRamp (Texture2D *tex, unsigned char *data, int x, int y, int maxX, int maxY) { + data[0] = x; + data[1] = x; + data[2] = x; + data[3] = x; +} + +inline void Black (Texture2D *tex, unsigned char *data, int x, int y, int maxX, int maxY) { + data[0] = 0; + data[1] = 0; + data[2] = 0; + data[3] = 0; +} + +inline void Red (Texture2D *tex, unsigned char *data, int x, int y, int maxX, int maxY) { + data[0] = 255; + data[1] = 0; + data[2] = 0; + data[3] = 0; +} + +inline void Gray (Texture2D *tex, unsigned char *data, int x, int y, int maxX, int maxY) { + data[0] = 127; + data[1] = 127; + data[2] = 127; + data[3] = 127; +} + +inline float ToUnsigned(const float s) +{ + return 0.5f * s + 0.5f; +} + +inline void RandomRotation (Texture2D *tex, unsigned char *data, int x, int y, int maxX, int maxY) { + float randAngle = 2.0f * kPI * Random01(gRandomSeed); + data[0] = NormalizedToByte (ToUnsigned ( Cos (randAngle))); + data[1] = NormalizedToByte (ToUnsigned (-Sin (randAngle))); + data[2] = NormalizedToByte (ToUnsigned ( Sin (randAngle))); + data[3] = NormalizedToByte (ToUnsigned ( Cos (randAngle))); +} + +template <typename T> +inline void LightAttenuation (Texture2D *tex, T *data, int x, int y, int maxX, int maxY) +{ + float sqrRange = (float)x / (float)maxX; + float val = Light::AttenuateNormalized(sqrRange); + T b = RoundfToInt( val * std::numeric_limits<T>::max() ); + *data = b; +} + +inline void HaloTex (Texture2D *tex, unsigned char *data, int x, int y, int maxX, int maxY) +{ + maxX >>= 1; + maxY >>= 1; + float xFac = (float)(x - maxX) / (float)(maxX); + float yFac = (float)(y - maxY) / (float)(maxY); + float sqrRange = xFac * xFac + yFac * yFac; + if (sqrRange > 1.0f) + sqrRange = 1.0f; + *data = RoundfToInt((1.0f - sqrRange) * 255.0f); + return; +} + +inline void WhiteTex (Texture2D *tex, unsigned char *data, int x, int y, int maxX, int maxY) { + *data = 255; +} + +inline void Empty1D (Texture2D *tex, unsigned char *data, int x, int maxX) { + data[0] = data[1] = data[2] = data[3] = 128; +} +inline void Empty2D (Texture2D *tex, unsigned char *data, int x, int y, int maxX, int maxY) { + data[0] = data[1] = data[2] = data[3] = 128; +} +inline void Empty3D( unsigned char *data, int x, int y, int z, int maxX, int maxY, int maxZ ) { + data[0] = data[1] = data[2] = data[3] = 128; +} +inline void EmptyCube( unsigned char *data, Vector3f normal ) { + data[0] = data[1] = data[2] = data[3] = 128; +} + +void GenerateDitherTextures() +{ + const int width = 4; + const int height = 4; + const int numSlices = 16; + const int numPixelsPerSlice = width*height; + + const unsigned char mask [numPixelsPerSlice] = + { + 0,9,3,9, + 9,4,9,7, + 2,9,1,9, + 9,6,9,5, + }; + + gDitherMaskTex = CreateObjectFromCode<Texture3D>(); + gDitherMaskTex->SetHideFlags(Object::kHideAndDontSave); + gDitherMaskTex->InitTexture (width, height, 16, kTexFormatAlpha8, false); + gDitherMaskTex->GetSettings().m_Aniso = 0; // disable aniso + gDitherMaskTex->GetSettings().m_FilterMode = 0; // disable aniso + + gDitherMaskTex->ApplySettings (); + + unsigned char* data = (unsigned char*)gDitherMaskTex->GetImageDataPointer(); + for(int slice = 0; slice < numSlices/2; slice++) + { + int index = slice; + int invIndex = numSlices-1-slice; + unsigned char* sliceData0 = data + numPixelsPerSlice * index; + unsigned char* sliceData1 = data + numPixelsPerSlice * invIndex; + for(int i = 0; i < numPixelsPerSlice; i++) + { + unsigned char maskValue = mask[i] >= slice ? 0x00 : 0xff; + sliceData0[i] = maskValue; + sliceData1[i] = ~maskValue; + } + } + gDitherMaskTex->UpdateImageData(false); +} + + +void builtintex::ReinitBuiltinTextures() +{ +# define INIT_BUILTIN_TEX(texI, tsI, tex) GetGfxDevice().GetBuiltinParamValues().GetWritableTexEnvParam(texI).InitFromTexture(tex, NULL, tsI) + + INIT_BUILTIN_TEX(kShaderTexEnvWhite, kShaderVecWhiteTexelSize, gWhiteTex); + INIT_BUILTIN_TEX(kShaderTexEnvBlack, kShaderVecBlackTexelSize, gBlackTex); + INIT_BUILTIN_TEX(kShaderTexEnvRed, kShaderVecRedTexelSize, s_RedTex); + INIT_BUILTIN_TEX(kShaderTexEnvGray, kShaderVecGrayTexelSize, s_GrayTex); + INIT_BUILTIN_TEX(kShaderTexEnvGrey, kShaderVecGreyTexelSize, s_GrayTex); + INIT_BUILTIN_TEX(kShaderTexEnvGrayscaleRamp, kShaderVecGrayscaleRampTexelSize, s_GrayRampTex); + INIT_BUILTIN_TEX(kShaderTexEnvGreyscaleRamp, kShaderVecGreyscaleRampTexelSize, s_GrayRampTex); + INIT_BUILTIN_TEX(kShaderTexEnvBump, kShaderVecBumpTexelSize, s_NormalMapTex); + INIT_BUILTIN_TEX(kShaderTexEnvLightmap, kShaderVecLightmapTexelSize, gBlackTex); + INIT_BUILTIN_TEX(kShaderTexEnvUnityLightmap, kShaderVecUnityLightmapTexelSize, gBlackTex); + INIT_BUILTIN_TEX(kShaderTexEnvUnityLightmapInd, kShaderVecUnityLightmapIndTexelSize, gBlackTex); + INIT_BUILTIN_TEX(kShaderTexEnvUnityLightmapThird, kShaderVecUnityLightmapThirdTexelSize, gBlackTex); + INIT_BUILTIN_TEX(kShaderTexEnvDitherMaskLOD, kShaderVecDitherMaskLODSize, gDitherMaskTex); + INIT_BUILTIN_TEX(kShaderTexEnvRandomRotation, kShaderVecRandomRotationTexelSize, s_RandomRotationTex); + +# undef INIT_BUILTIN_TEX +} + + +void builtintex::GenerateBuiltinTextures() +{ + static bool texturesGenerated = false; + if( texturesGenerated ) + return; + texturesGenerated = true; + + s_NormalMapTex = BuildTexture<unsigned char> (4, 4, kTexFormatRGBA32, &EmptyNormalMap); + + gWhiteTex = BuildTexture<unsigned char> (4, 4, kTexFormatRGBA32, &White); + + gBlackTex = BuildTexture<unsigned char> (4, 4, kTexFormatRGBA32, &Black); + + // Random rotation texture for rotating points. Stores unsigned cos(theta) in .r and unsigned sin(theta) in .g + s_RandomRotationTex = BuildTexture<unsigned char> (16, 16, kTexFormatRGBA32, &RandomRotation); + + // The red texture will be created with mipmaps. The reason is a driver bug in Nvidia GTX 4xx cards + // (Win7, driver 270.61), which makes terrain's FirstPass splat map shader flicker like mad if + // the control texture has no mips and is not compressed. This happens on any new terrain + // (the red texture is used there by default). + s_RedTex = BuildTexture<unsigned char> (4, 4, kTexFormatRGBA32, &Red, true); + + s_GrayTex = BuildTexture<unsigned char> (4, 4, kTexFormatRGBA32, &Gray); + + s_GrayRampTex = BuildTexture<unsigned char> (256, 2, kTexFormatRGBA32, &GrayscaleRamp); + s_GrayRampTex->GetSettings().m_WrapMode = kTexWrapClamp; + s_GrayRampTex->ApplySettings (); + + gHaloTex = BuildTexture<unsigned char> (64, 64, kTexFormatAlpha8, &HaloTex); + gHaloTex->GetSettings ().m_WrapMode = kTexWrapClamp; + gHaloTex->ApplySettings (); + + if (gGraphicsCaps.supportsTextureFormat[kTexFormatAlphaLum16]) + gAttenuationTex = BuildTexture<unsigned short> (1024, 1, kTexFormatAlphaLum16, &LightAttenuation<unsigned short>); + else + gAttenuationTex = BuildTexture<unsigned char> (1024, 1, kTexFormatAlpha8, &LightAttenuation<unsigned char>); + gAttenuationTex->GetSettings ().m_WrapMode = kTexWrapClamp; + gAttenuationTex->ApplySettings (); + + // Initialize the textures used when no texture has been bound. + gDefaultTextures[kTexDim2D] = BuildTexture<unsigned char> (16,16, kTexFormatRGBA32, &Empty2D)->GetTextureID(); + + if (gGraphicsCaps.has3DTexture) + gDefaultTextures[kTexDim3D] = Build3DTexture<unsigned char> (1,1,1, kTexFormatRGBA32, &Empty3D)->GetTextureID(); + + gDefaultTextures[kTexDimCUBE] = BuildCubeTexture<unsigned char> (1, kTexFormatRGBA32, &EmptyCube)->GetTextureID(); + + if (gGraphicsCaps.has3DTexture) + GenerateDitherTextures(); + + // Make the default for a 'any' be the same as the 2D case + gDefaultTextures[kTexDimAny] = gDefaultTextures[kTexDim2D]; + + ReinitBuiltinTextures(); +} + +Texture2D* builtintex::GetWhiteTexture() { return gWhiteTex; } +Texture2D* builtintex::GetBlackTexture() { return gBlackTex; } +Texture3D* builtintex::GetDitherMaskTexture() { return gDitherMaskTex; } +Texture* builtintex::GetAttenuationTexture() { return gAttenuationTex; } +Texture* builtintex::GetHaloTexture() { return gHaloTex; } +TextureID builtintex::GetDefaultTexture( TextureDimension texDim ) { return gDefaultTextures[texDim]; } +TextureID builtintex::GetBlackTextureID () { return gBlackTex->GetTextureID(); } diff --git a/Runtime/Graphics/GeneratedTextures.h b/Runtime/Graphics/GeneratedTextures.h new file mode 100644 index 0000000..bc474c5 --- /dev/null +++ b/Runtime/Graphics/GeneratedTextures.h @@ -0,0 +1,28 @@ +#ifndef GENERATEDTEXTURES_H +#define GENERATEDTEXTURES_H + +#include "Runtime/GfxDevice/GfxDeviceTypes.h" +class Texture; +class Texture2D; +class Texture3D; + +namespace builtintex { + +void GenerateBuiltinTextures(); +void ReinitBuiltinTextures(); + +Texture2D* GetWhiteTexture (); +Texture2D* GetBlackTexture (); +Texture3D* GetDitherMaskTexture(); +Texture* GetAttenuationTexture (); +Texture* GetHaloTexture (); + +// Get the default texture for a texture dimension +TextureID GetDefaultTexture( TextureDimension texDim ); + +TextureID GetBlackTextureID (); + +} // namespace + + +#endif diff --git a/Runtime/Graphics/GraphicsHelper.cpp b/Runtime/Graphics/GraphicsHelper.cpp new file mode 100644 index 0000000..6a32e72 --- /dev/null +++ b/Runtime/Graphics/GraphicsHelper.cpp @@ -0,0 +1,103 @@ +#include "UnityPrefix.h" +#include "Runtime/GfxDevice/GfxDevice.h" +#include "GraphicsHelper.h" +#include "Runtime/Camera/CameraUtil.h" +#include "Runtime/Shaders/Shader.h" +#include "External/shaderlab/Library/intshader.h" + +namespace GraphicsHelper { + +void Clear( UInt32 clearFlags, const float color[4], float depth, int stencil ) +{ + GfxDevice &device = GetGfxDevice(); + + +#if UNITY_WIN + int viewport[4]; + device.GetViewport(viewport); + + bool canUseNativeClear = + gGraphicsCaps.hasNonFullscreenClear || + (viewport[0]==0 && viewport[1]==0 && viewport[2]==device.GetCurrentTargetWidth() && viewport[3]==device.GetCurrentTargetHeight()); + if (!canUseNativeClear) + { +#if ENABLE_MULTITHREADED_CODE + DebugAssert(Thread::CurrentThreadIsMainThread()); +#endif + + // Some devices (e.g. D3D11) can't clear sub-areas of a render target, so must draw a quad instead. + Shader* clearShader = Shader::GetScreenClearShader(); + if (!clearShader || clearShader->GetShaderLabShader()->GetActiveSubShader().GetValidPassCount() != 4) + { + AssertString ("Valid screen clear shader not found"); + return; + } + ShaderLab::SubShader& ss = clearShader->GetShaderLabShader()->GetActiveSubShader(); + const int clearIndex = clearFlags & 3; + ss.GetPass (clearIndex)->ApplyPass(0, NULL); + + bool oldWireframe = device.GetWireframe(); + device.SetWireframe (false); + DeviceMVPMatricesState saveMVPMatrices; + LoadFullScreenOrthoMatrix (); + device.ImmediateBegin (kPrimitiveQuads); + device.ImmediateColor(color[0], color[1], color[2], color[3]); + float z = -100.0f - 1e-9f; + device.ImmediateVertex (0.0f, 0.0f, z); + device.ImmediateVertex (0.0f, 1.0f, z); + device.ImmediateVertex (1.0f, 1.0f, z); + device.ImmediateVertex (1.0f, 0.0f, z); + device.ImmediateEnd (); + device.SetWireframe (oldWireframe); + return; + } +#endif + + device.Clear(clearFlags, color, depth, stencil); +} + +void SetBlendState( const DeviceBlendState* state, const ShaderLab::FloatVal& alphaRef, const ShaderLab::PropertySheet* props ) +{ + GfxDevice& device = GetGfxDevice(); + if (device.IsRecording()) + device.RecordSetBlendState(state, alphaRef, props); + else + device.SetBlendState(state, alphaRef.ToFloat(props)); +} + +void SetMaterial( const ShaderLab::VectorVal& ambient, const ShaderLab::VectorVal& diffuse, const ShaderLab::VectorVal& specular, const ShaderLab::VectorVal& emissive, const ShaderLab::FloatVal& shininess, const ShaderLab::PropertySheet* props ) +{ + GfxDevice& device = GetGfxDevice(); + if (device.IsRecording()) + device.RecordSetMaterial(ambient, diffuse, specular, emissive, shininess, props); + else + device.SetMaterial(ambient.Get(props).GetPtr(), diffuse.Get(props).GetPtr(), specular.Get(props).GetPtr(), emissive.Get(props).GetPtr(), shininess.ToFloat(props)); +} + +void SetColor( const ShaderLab::VectorVal& color, const ShaderLab::PropertySheet* props ) +{ + GfxDevice& device = GetGfxDevice(); + if (device.IsRecording()) + device.RecordSetColor(color, props); + else + device.SetColor(color.Get(props).GetPtr()); +} + +void EnableFog( FogMode fogMode, const ShaderLab::FloatVal& fogStart, const ShaderLab::FloatVal& fogEnd, const ShaderLab::FloatVal& fogDensity, const ShaderLab::VectorVal& fogColor, const ShaderLab::PropertySheet* props ) +{ + GfxDevice& device = GetGfxDevice(); + if (device.IsRecording()) + device.RecordEnableFog(fogMode, fogStart, fogEnd, fogDensity, fogColor, props); + else + { + GfxFogParams fog; + fog.mode = fogMode; + fog.color = fogColor.Get(props); + fog.start = fogStart.ToFloat(props); + fog.end = fogEnd.ToFloat(props); + fog.density = fogDensity.ToFloat(props); + device.EnableFog(fog); + } +} + +} // namespace GfxDeviceHelper
\ No newline at end of file diff --git a/Runtime/Graphics/GraphicsHelper.h b/Runtime/Graphics/GraphicsHelper.h new file mode 100644 index 0000000..1c23522 --- /dev/null +++ b/Runtime/Graphics/GraphicsHelper.h @@ -0,0 +1,98 @@ +#pragma once + +// Remove this when SetShaders has been inlined +#include <Runtime/GfxDevice/GfxDevice.h> +#include "External/shaderlab/Library/program.h" + +namespace ShaderLab { + class FloatVal; + struct VectorVal; + struct TextureBinding; + class PropertySheet; +} + +namespace GraphicsHelper +{ + void Clear( UInt32 clearFlags, const float color[4], float depth, int stencil ); + + void SetBlendState( const DeviceBlendState* state, const ShaderLab::FloatVal& alphaRef, const ShaderLab::PropertySheet* props ); + void SetMaterial( const ShaderLab::VectorVal& ambient, const ShaderLab::VectorVal& diffuse, const ShaderLab::VectorVal& specular, const ShaderLab::VectorVal& emissive, const ShaderLab::FloatVal& shininess, const ShaderLab::PropertySheet* props ); + void SetColor( const ShaderLab::VectorVal& color, const ShaderLab::PropertySheet* props ); + void EnableFog( FogMode fogMode, const ShaderLab::FloatVal& fogStart, const ShaderLab::FloatVal& fogEnd, const ShaderLab::FloatVal& fogDensity, const ShaderLab::VectorVal& fogColor, const ShaderLab::PropertySheet* props ); + + //TextureCombinersHandle CreateTextureCombiners( int count, const ShaderLab::TextureBinding* texEnvs, const ShaderLab::PropertySheet* props, bool hasVertexColorOrLighting, bool usesAddSpecular ); + //void SetTextureCombiners( TextureCombinersHandle textureCombiners, const ShaderLab::PropertySheet* props ); + + // This is inlined until GfxDeviceClient has been moved out of GfxDevice module + inline void SetShaders (GfxDevice& device, ShaderLab::SubProgram* programs[kShaderTypeCount], const ShaderLab::PropertySheet* props) + { + // We do this outside of GfxDevice module to avoid dependency on ShaderLab + + if (device.IsThreadable()) + { + // This is a threadable device running in single-threaded mode + GfxThreadableDevice& threadableDevice = static_cast<GfxThreadableDevice&>(device); + + // GLSL-like platforms might result in different set of shader parameters + // for fog modes. + const bool useGLStyleFogParams = (device.GetRenderer() == kGfxRendererOpenGL || + device.GetRenderer() == kGfxRendererOpenGLES20Desktop || + device.GetRenderer() == kGfxRendererOpenGLES20Mobile || + device.GetRenderer() == kGfxRendererOpenGLES30); + const FogMode fogMode = useGLStyleFogParams ? device.GetFogParams().mode : kFogDisabled; + + UInt8* paramsBuffer[kShaderTypeCount]; + for (int pt = 0; pt < kShaderTypeCount; ++pt) + { + paramsBuffer[pt] = NULL; + ShaderLab::SubProgram* prog = programs[pt]; + if (!prog) + continue; + + GpuProgramParameters* params = &prog->GetParams(fogMode); + if (params && !params->IsReady()) + threadableDevice.CreateShaderParameters (prog, fogMode); + + int bufferSize = params->GetValuesSize(); + if (bufferSize > 0) + { + paramsBuffer[pt] = ALLOC_TEMP_MANUAL (UInt8, bufferSize); + params->PrepareValues (props, paramsBuffer[pt]); + } + } + + GpuProgram* gpuPrograms[kShaderTypeCount]; + for (int pt = 0; pt < kShaderTypeCount; ++pt) + { + if (programs[pt]) + gpuPrograms[pt] = &programs[pt]->GetGpuProgram(); + else + gpuPrograms[pt] = NULL; + } + + const GpuProgramParameters* params[kShaderTypeCount]; + for (int pt = 0; pt < kShaderTypeCount; ++pt) + { + if (programs[pt]) + params[pt] = &programs[pt]->GetParams(fogMode); + else + params[pt] = NULL; + } + + threadableDevice.SetShadersThreadable (gpuPrograms, params, paramsBuffer); + + for (int pt = 0; pt < kShaderTypeCount; ++pt) + { + if (paramsBuffer[pt]) + FREE_TEMP_MANUAL(paramsBuffer[pt]); + } + } + else + { + // Old school device that doesn't support threadable interface, + // or GfxDeviceClient that wants to deal with parameters itself + device.SetShadersMainThread(programs, props); + } + } + +}
\ No newline at end of file diff --git a/Runtime/Graphics/Image.cpp b/Runtime/Graphics/Image.cpp new file mode 100644 index 0000000..b4ad02d --- /dev/null +++ b/Runtime/Graphics/Image.cpp @@ -0,0 +1,1657 @@ +#include "UnityPrefix.h" +#include "Configuration/UnityConfigure.h" +#include "Image.h" +#include "S3Decompression.h" +#include "Runtime/Allocator/MemoryMacros.h" +#include "Runtime/Utilities/Utility.h" +#include "Runtime/Utilities/FileUtilities.h" +#include "Runtime/Utilities/LogAssert.h" +#include "Runtime/Utilities/vector_utility.h" +#include "Runtime/Utilities/BitUtility.h" +#include "External/ProphecySDK/include/prcore/surface.hpp" + +#include "Runtime/Math/Color.h" +#include "Runtime/Math/ColorSpaceConversion.h" + +using namespace std; + +prcore::PixelFormat GetProphecyPixelFormat (TextureFormat format); + +void BlitCopyCompressedImage( TextureFormat format, const UInt8* src, int srcWidth, int srcHeight, UInt8* dst, int dstWidth, int dstHeight, bool fillRest ) +{ + AssertIf( dstWidth < srcWidth ); + AssertIf( dstHeight < srcHeight ); + + int blockBytes, srcBlocksX, srcBlocksY, dstBlocksX, dstBlocksY; + + { + AssertIf( !IsCompressedDXTTextureFormat(format) && !IsCompressedETCTextureFormat(format) + && !IsCompressedATCTextureFormat(format) && !IsCompressedETC2TextureFormat(format) + && !IsCompressedEACTextureFormat(format)); + + blockBytes = (format == kTexFormatDXT1 || format == kTexFormatETC_RGB4 || format == kTexFormatATC_RGB4 + || format == kTexFormatETC2_RGB || format == kTexFormatETC2_RGBA1 + || format == kTexFormatEAC_R || format == kTexFormatEAC_R_SIGNED) + ? 8 : 16; + + srcBlocksX = (srcWidth + 3) / 4; + srcBlocksY = (srcHeight + 3) / 4; + dstBlocksX = (dstWidth + 3) / 4; + dstBlocksY = (dstHeight + 3) / 4; + } + + int srcRowBytes = srcBlocksX * blockBytes; + int dstRowBytes = dstBlocksX * blockBytes; + + const UInt8* srcPtr = src; + UInt8* dstPtr = dst; + int y; + for( y = 0; y < srcBlocksY; ++y ) + { + memcpy( dstPtr, srcPtr, srcRowBytes ); // copy DXT blocks + if( fillRest ) + memset( dstPtr + srcRowBytes, 0, dstRowBytes - srcRowBytes ); // fill rest with black/transparent + srcPtr += srcRowBytes; + dstPtr += dstRowBytes; + } + if( fillRest ) + memset( dstPtr, 0, dstRowBytes * (dstBlocksY-srcBlocksY) ); // fill rest with black/transparent +} + +void BlitCopyCompressedDXT1ToDXT5( const UInt8* src, int srcWidth, int srcHeight, UInt8* dst, int dstWidth, int dstHeight ) +{ + AssertIf( dstWidth < srcWidth ); + AssertIf( dstHeight < srcHeight ); + + int blockBytesSrc = 8; + int blockBytesDst = 16; + + int srcBlocksX = (srcWidth + 3) / 4; + int srcBlocksY = (srcHeight + 3) / 4; + int dstBlocksX = (dstWidth + 3) / 4; + + int srcRowBytes = srcBlocksX * blockBytesSrc; + int dstRowBytes = dstBlocksX * blockBytesDst; + + const UInt8* srcPtr = src; + UInt8* dstPtr = dst; + int y; + for( y = 0; y < srcBlocksY; ++y ) + { + for( int x = 0; x < srcBlocksX; ++x ) + { + // set alpha to opaque white + memset( dstPtr + x * blockBytesDst, 0xFF, blockBytesDst-blockBytesSrc ); + // copy block + memcpy( dstPtr + x * blockBytesDst + blockBytesSrc, srcPtr + x * blockBytesSrc, blockBytesSrc ); + } + srcPtr += srcRowBytes; + dstPtr += dstRowBytes; + } +} + + +void PadImageBorder( ImageReference& image, int sizeX, int sizeY ) +{ + AssertIf( IsAnyCompressedTextureFormat(image.GetFormat()) ); + + int width = image.GetWidth(); + int height = image.GetHeight(); + AssertIf( sizeX < 1 || sizeY < 1 || sizeX > width || sizeY > height ); + + int y; + UInt8* rowptr = image.GetImageData(); + + int bpp = GetBytesFromTextureFormat(image.GetFormat()); + AssertIf(bpp > 4); // we only support up to 4 bpp + UInt8 pixel[4]; + + // pad rightmost part by repeating last pixel in each row + for( y = 0; y < sizeY; ++y ) + { + UInt8* ptr = rowptr + (sizeX-1) * bpp; + + for( int b = 0; b < bpp; ++b ) + pixel[b] = ptr[b]; + ptr += bpp; + for( int x = sizeX; x < width; ++x ) + { + for( int b = 0; b < bpp; ++b ) + ptr[b] = pixel[b]; + ptr += bpp; + } + + rowptr += image.GetRowBytes(); + } + + // pad bottom part by repeating last row + UInt8* lastRowPtr = image.GetRowPtr( sizeY - 1 ); + for( int b = 0; b < bpp; ++b ) + pixel[b] = lastRowPtr[ (sizeX-1)*bpp + b ]; + + for( y = sizeY; y < height; ++y ) + { + UInt8* ptr = rowptr; + memcpy( ptr, lastRowPtr, sizeX * bpp ); // copy + ptr += sizeX * bpp; + for( int x = sizeX; x < width; ++x ) // repeat last pixel + { + for( int b = 0; b < bpp; ++b ) + ptr[b] = pixel[b]; + ptr += bpp; + } + + rowptr += image.GetRowBytes(); + } +} + + +int CalculateMipMapCount3D (int width, int height, int depth) +{ + //AssertIf( !IsPowerOfTwo(width) || !IsPowerOfTwo(height) || !IsPowerOfTwo(depth) ); + + // Mip-levels for non-power-of-two textures follow OpenGL's NPOT texture rules: size is divided + // by two and floor'ed. This allows just to use same old code I think. + + int minSizeLog2 = HighestBit (width); + minSizeLog2 = max (minSizeLog2, HighestBit (height)); + minSizeLog2 = max (minSizeLog2, HighestBit (depth)); + + AssertIf( (width >> minSizeLog2) < 1 && (height >> minSizeLog2) < 1 && (depth >> minSizeLog2) < 1 ); + AssertIf( (width >> minSizeLog2) > 1 && (height >> minSizeLog2) > 1 && (depth >> minSizeLog2) < 1 ); + + return minSizeLog2 + 1; +} + +int CalculateImageSize (int width, int height, TextureFormat format) +{ + if (format == kTexFormatDXT1 || format == kTexFormatATC_RGB4) + return ((width + 3) / 4) * ((height + 3) / 4) * 8; + else if (format == kTexFormatDXT3 || format == kTexFormatDXT5 || format == kTexFormatATC_RGBA8) + return ((width + 3) / 4) * ((height + 3) / 4) * 16; + else if (format == kTexFormatPVRTC_RGB4 || format == kTexFormatPVRTC_RGBA4) + return (max(width, 8) * max(height, 8) * 4 + 7) / 8; + else if (format == kTexFormatPVRTC_RGB2 || format == kTexFormatPVRTC_RGBA2) + return (max(width, 16) * max(height, 8) * 2 + 7) / 8; + else if (format == kTexFormatETC_RGB4 || format == kTexFormatETC2_RGB || format == kTexFormatETC2_RGBA1 + || format == kTexFormatEAC_R || format == kTexFormatEAC_R_SIGNED) + return (max(width, 4) * max(height, 4) * 4 + 7) / 8; + else if (format == kTexFormatETC2_RGBA8 || format == kTexFormatEAC_RG || format == kTexFormatEAC_RG_SIGNED) + return (max(width, 4) * max(height, 4) * 8 + 7) / 8; + +#define STR_(x) #x +#define STR(x) STR_(x) + // The size of the ASTC block is always 128 bits = 16 bytes, width of image in blocks is ceiling(width/blockwidth) +#define DO_ASTC(bx, by) else if( format == kTexFormatASTC_RGB_##bx##x##by || format == kTexFormatASTC_RGBA_##bx##x##by)\ + return ( (CeilfToInt(((float)width) / ((float)bx))) * (CeilfToInt(((float)height) / ((float)by))) * 16 ) + + DO_ASTC(4, 4); + DO_ASTC(5, 5); + DO_ASTC(6, 6); + DO_ASTC(8, 8); + DO_ASTC(10, 10); + DO_ASTC(12, 12); + +#undef DO_ASTC +#undef STR_ +#undef STR + + // ATF Format is a container format and has no known size based on width & height + else if (format == kTexFormatFlashATF_RGB_DXT1 || format == kTexFormatFlashATF_RGBA_JPG || format == kTexFormatFlashATF_RGB_JPG) + return 0; + else + return GetRowBytesFromWidthAndFormat (width, format) * height; +} + +int CalculateMipMapOffset (int width, int height, TextureFormat format, int miplevel) +{ + if (width == 0 || height == 0) + return 0; + + // Allow NPOT textures as well. + //AssertIf (!IsPowerOfTwo (width) || !IsPowerOfTwo (height)); + int completeSize = 0; + for (int i=0;i < miplevel;i++) + completeSize += CalculateImageSize (std::max (width >> i, 1), std::max (height >> i, 1), format); + return completeSize; +} + +int CalculateImageMipMapSize (int width, int height, TextureFormat format) +{ + return CalculateMipMapOffset( width, height, format, CalculateMipMapCount3D(width, height, 1) ); +} + + + +void CreateMipMap (UInt8* inData, int width, int height, int depth, TextureFormat format) +{ + const int bpp = GetBytesFromTextureFormat (format); + int mipCount = CalculateMipMapCount3D (width, height, depth) - 1; + UInt8* srcPtr = inData; + + UInt8* tempBuffer = NULL; + + for (int mip = 0; mip < mipCount; ++mip) + { + int nextWidth = std::max(width/2,1); + int nextHeight = std::max(height/2,1); + int nextDepth = std::max(depth/2,1); + + UInt8* nextMipPtr = srcPtr + width * height * depth * bpp; + UInt8* dstPtr = nextMipPtr; + if (depth > 1) + { + if (!tempBuffer) + tempBuffer = ALLOC_TEMP_MANUAL(UInt8, nextWidth*nextHeight*bpp+bpp); + + for (int d = 0; d < nextDepth; ++d) + { + // blit two slices of this mip level into two 2x2 smaller images + ImageReference src1 (width, height, width * bpp, format, srcPtr); + srcPtr += width * height * bpp; + ImageReference src2 (width, height, width * bpp, format, srcPtr); + srcPtr += width * height * bpp; + + ImageReference dst1 (nextWidth, nextHeight, nextWidth * bpp, format, dstPtr); // one directly into dst + dst1.BlitImage (src1, ImageReference::BLIT_BILINEAR_SCALE); + ImageReference dst2 (nextWidth, nextHeight, nextWidth * bpp, format, tempBuffer); // one into temp buffer + dst2.BlitImage (src2, ImageReference::BLIT_BILINEAR_SCALE); + + // now average the two smaller images + for (int i = 0; i < nextWidth*nextHeight*bpp; ++i) + { + unsigned b1 = dstPtr[i]; + unsigned b2 = tempBuffer[i]; + dstPtr[i] = (b1 + b2) / 2; + } + + dstPtr += nextWidth * nextHeight * bpp; + } + } + else + { + ImageReference src (width, height, width * bpp, format, srcPtr); + ImageReference dst (nextWidth, nextHeight, nextWidth * bpp, format, dstPtr); + dst.BlitImage (src, ImageReference::BLIT_BILINEAR_SCALE); + } + + srcPtr = nextMipPtr; + width = nextWidth; + height = nextHeight; + depth = nextDepth; + } + + FREE_TEMP_MANUAL(tempBuffer); +} + + + +ImageReference::ImageReference (int width, int height, int rowbytes, TextureFormat format, void* image) +{ + m_Width = width; + m_Height = height; + m_Format = format; + m_RowBytes = rowbytes; + + if (image && CheckImageFormatValid (width, height, format)) + m_Image = reinterpret_cast<UInt8*> (image); + else + m_Image = NULL; +} + +ImageReference::ImageReference (int width, int height, TextureFormat format) +{ + m_Width = width; + m_Height = height; + m_Format = format; + m_RowBytes = GetRowBytesFromWidthAndFormat (m_Width, format); + m_Image = NULL; +} + +void ImageReference::FlipImageY () +{ + if (m_Image) + { + prcore::Surface srcSurface (GetWidth (), GetHeight (), GetRowBytes (), GetProphecyPixelFormat (GetFormat ()), GetImageData ()); + srcSurface.FlipImageY (); + } +} + +#if UNITY_EDITOR +static inline void SwapPixels( UInt8* a, UInt8* b, int bpp ) { + for( int i = 0; i < bpp; ++i ) { + UInt8 t = a[i]; + a[i] = b[i]; + b[i] = t; + } +} + +void ImageReference::FlipImageX () +{ + if (!m_Image) + return; + + const int width = GetWidth(); + const int height = GetHeight(); + const TextureFormat format = GetFormat(); + if( IsAnyCompressedTextureFormat(format) ) { + AssertString("FlipImageX does not work on compressed formats"); + return; + } + const int bpp = GetBytesFromTextureFormat(format); + for( int y = 0; y < height; ++y ) { + UInt8* rowFirst = GetRowPtr(y); + UInt8* rowLast = rowFirst + (width-1) * bpp; + for( int x = 0; x < width/2; ++x ) { + SwapPixels( rowFirst, rowLast, bpp ); + rowFirst += bpp; + rowLast -= bpp; + } + } +} +#endif + + +void ImageReference::BlitImage (const ImageReference& source, BlitMode mode) +{ + if (m_Image && source.m_Image) + { + prcore::Surface srcSurface (source.GetWidth (), source.GetHeight (), source.GetRowBytes (), GetProphecyPixelFormat (source.GetFormat ()), source.GetImageData ()); + prcore::Surface dstSurface (GetWidth (), GetHeight (), GetRowBytes (), GetProphecyPixelFormat (GetFormat ()), GetImageData ()); + dstSurface.BlitImage (srcSurface, (prcore::Surface::BlitMode)mode); + } +} + +void ImageReference::BlitImage (int x, int y, const ImageReference& source) +{ + if (source.m_Image && m_Image) + { + prcore::Surface srcSurface (source.GetWidth (), source.GetHeight (), source.GetRowBytes (), GetProphecyPixelFormat (source.GetFormat ()), source.GetImageData ()); + prcore::Surface dstSurface (GetWidth (), GetHeight (), GetRowBytes (), GetProphecyPixelFormat (GetFormat ()), GetImageData ()); + dstSurface.BlitImage (x, y, srcSurface); + } +} + +void ImageReference::ClearImage (const ColorRGBA32& color, ClearMode mode) +{ + if (m_Image) + { + prcore::color32 c; + c.r = color.r; + c.g = color.g; + c.b = color.b; + c.a = color.a; + prcore::Surface dstSurface (GetWidth (), GetHeight (), GetRowBytes (), GetProphecyPixelFormat (GetFormat ()), GetImageData ()); + dstSurface.ClearImage (c, (prcore::Surface::ClearMode)mode); + } +} + +ImageReference ImageReference::ClipImage (int x, int y, int width, int height) const +{ + if (m_Image) + { + x = clamp<int> (x, 0, m_Width); + y = clamp<int> (y, 0, m_Height); + width = min<int> (m_Width, x + width) - x; + height = min<int> (m_Height, y + height) - y; + width = max<int> (0, width); + height = max<int> (0, height); + + int bytesPerPixel = GetBytesFromTextureFormat( m_Format ); + return ImageReference( width, height, m_RowBytes, m_Format, m_Image + bytesPerPixel * x + m_RowBytes * y ); + } + else + return ImageReference( 0, 0, 0, m_Format, NULL ); +} + +bool ImageReference::NeedsReformat(int width, int height, TextureFormat format) const +{ + // TODO : it actually doesn't need to reallocate when downsizing! + return width != m_Width || height != m_Height || format != m_Format; +} + +Image::Image (int width, int height, TextureFormat format) +{ + m_Height = height; + m_Width = width; + m_Format = format; + int bpp = GetBytesFromTextureFormat( m_Format ); + m_RowBytes = m_Width * bpp; + if (CheckImageFormatValid (width, height, format)) + m_Image = (UInt8*)UNITY_MALLOC_ALIGNED(kMemNewDelete, m_RowBytes * m_Height + GetMaxBytesPerPixel( m_Format ), kImageDataAlignment); // allocate one pixel more for bilinear blits + else + m_Image = NULL; +} + +Image::Image (int width, int height, int rowbytes, TextureFormat format, void* image) +{ + m_Height = height; + m_Width = width; + m_Format = format; + int bpp = GetBytesFromTextureFormat( m_Format ); + m_RowBytes = m_Width * bpp; + if( CheckImageFormatValid (width, height, format) ) + m_Image = (UInt8*)UNITY_MALLOC_ALIGNED(kMemNewDelete, m_RowBytes * m_Height + GetMaxBytesPerPixel( m_Format ), kImageDataAlignment); // allocate one pixel more for bilinear blits + else + m_Image = NULL; + + if (image && m_Image) + BlitImage (ImageReference (width, height, rowbytes, format, image), BLIT_COPY); +} + +void Image::SetImage(SInt32 width, SInt32 height, UInt32 format, bool shrinkAllowed) +{ + // TODO : this size is wrong if it has been shrunk already, but we don't care at the moment, + // because we use it for calculation of mipmaps only + const int oldSize = m_RowBytes * m_Height + GetBytesFromTextureFormat( m_Format ); + + m_Width = width; + m_Height = height; + m_Format = format; + const int bpp = GetBytesFromTextureFormat( m_Format ); + m_RowBytes = m_Width * bpp; + + const int newSize = m_RowBytes * m_Height + bpp; + + if ((!shrinkAllowed && (oldSize < newSize)) || + (shrinkAllowed && (oldSize != newSize))) + { + UNITY_FREE(kMemNewDelete, m_Image); + m_Image = NULL; + if( m_Format != 0 && CheckImageFormatValid (m_Width, m_Height, m_Format) ) + m_Image = (UInt8*)UNITY_MALLOC_ALIGNED(kMemNewDelete, m_RowBytes * m_Height + GetMaxBytesPerPixel( m_Format ), kImageDataAlignment); // allocate one pixel more for bilinear blits + } +} + +void Image::SetImage(const ImageReference& src, bool shrinkAllowed) +{ + if (this == &src) + return; + + SetImage (src.GetWidth(), src.GetHeight(), src.GetFormat(), shrinkAllowed); + BlitImage (src, BLIT_COPY); +} + +void Image::ReformatImage (const ImageReference& image, int width, int height, TextureFormat format, BlitMode mode) +{ + AssertIf(!image.NeedsReformat(width, height, format)); + + int bpp = GetBytesFromTextureFormat(format); + int newRowBytes = width * bpp; + UInt8* newImageData = NULL; + if (CheckImageFormatValid (width, height, format)) + newImageData = (UInt8*)UNITY_MALLOC_ALIGNED(kMemNewDelete, height * newRowBytes + GetMaxBytesPerPixel( m_Format ), kImageDataAlignment); // allocate one pixel more for bilinear blits + + ImageReference newImage (width, height, newRowBytes, format, newImageData); + newImage.BlitImage (image, mode); + UNITY_FREE(kMemNewDelete, m_Image); + + m_Height = height; + m_Width = width; + m_Format = format; + m_RowBytes = newRowBytes; + m_Image = newImageData; +} + +void Image::ReformatImage (int width, int height, TextureFormat format, BlitMode mode) +{ + if (!NeedsReformat(width, height, format)) + return; + + ReformatImage (*this, width, height, format, mode); +} + +bool ImageReference::IsValidImage () const +{ + return m_Image != NULL && CheckImageFormatValid (GetWidth(), GetHeight(), GetFormat()); +} + +bool CheckImageFormatValid (int width, int height, TextureFormat format) +{ + if (width > 0 && height > 0 && format > 0 && (format <= kTexFormatBGR24 || format == kTexFormatBGRA32 || format == kTexFormatRGBA4444)) + return true; + else + { + if (width < 0) { + AssertString ("Image invalid width!"); + } + if (height < 0) { + AssertString ("Image invalid height!"); + } + if (format > kTexFormatBGR24 && format != kTexFormatRGBA4444 && format != kTexFormatBGRA32) { + AssertString ("Image invalid format!"); + } + return false; + } +} + +prcore::PixelFormat GetProphecyPixelFormat (TextureFormat format) +{ + switch (format) + { + case kTexFormatAlpha8: + return prcore::PixelFormat (8,0,0xff); + case kTexFormatARGB4444: + return prcore::PixelFormat (16,0x00000f00,0x000000f0,0x0000000f,0x0000f000); + case kTexFormatRGBA4444: + return prcore::PixelFormat (16,0x0000f000,0x00000f00,0x000000f0,0x0000000f); + case kTexFormatRGB24: + #if UNITY_BIG_ENDIAN + return prcore::PixelFormat (24,0x00ff0000,0x0000ff00,0x000000ff,0x00000000); + #else + return prcore::PixelFormat (24,0x000000ff,0x0000ff00,0x00ff0000,0x00000000); + #endif + case kTexFormatRGBA32: + #if UNITY_BIG_ENDIAN + return prcore::PixelFormat (32,0xff000000,0x00ff0000,0x0000ff00,0x000000ff); + #else + return prcore::PixelFormat (32,0x000000ff,0x0000ff00,0x00ff0000,0xff000000); + #endif + case kTexFormatBGRA32: + #if UNITY_BIG_ENDIAN + return prcore::PixelFormat (32,0x0000ff00,0x00ff0000,0xff000000,0x000000ff); + #else + return prcore::PixelFormat (32,0x00ff0000,0x0000ff00,0x000000ff,0xff000000); + #endif + case kTexFormatARGB32: + #if UNITY_BIG_ENDIAN + return prcore::PixelFormat (32,0x00ff0000,0x0000ff00,0x000000ff,0xff000000); + #else + return prcore::PixelFormat (32,0x0000ff00,0x00ff0000,0xff000000,0x000000ff); + #endif + case kTexFormatARGBFloat: + return prcore::PixelFormat (128, 2, 4, 8, 1, prcore::PIXELFORMAT_FLOAT32); + case kTexFormatRGB565: + return prcore::PixelFormat (16,0x0000f800,0x000007e0,0x0000001f,0x00000000); + case kTexFormatBGR24: +#if UNITY_BIG_ENDIAN + return prcore::PixelFormat (24,0x00ff0000,0x0000ff00,0x000000ff,0x00000000); +#else + return prcore::PixelFormat (24,0x000000ff,0x0000ff00,0x00ff0000,0x00000000); +#endif + default: + DebugAssertIf( true ); + return prcore::PixelFormat (); + } +} + +bool operator == (const ImageReference& lhs, const ImageReference& rhs) +{ + if (lhs.GetWidth () != rhs.GetWidth ()) return false; + if (lhs.GetHeight () != rhs.GetHeight ()) return false; + if (lhs.GetRowBytes () != rhs.GetRowBytes ()) return false; + if (lhs.GetFormat () != rhs.GetFormat ()) return false; + UInt8* lhsData = lhs.GetImageData (); + UInt8* rhsData = rhs.GetImageData (); + if (lhsData == NULL || rhsData == NULL) + return lhsData == rhsData; + + int size = lhs.GetRowBytes () * lhs.GetHeight (); + for (int i=0;i<size / 4;i++) + { + if (*reinterpret_cast<UInt32*> (lhsData) != *reinterpret_cast<UInt32*> (rhsData)) + return false; + lhsData += 4; rhsData += 4; + } + for (int i=0;i<size % 4;i++) + { + if (*lhsData != *rhsData) + return false; + lhsData++; rhsData++; + } + return true; +} + +void SwizzleARGB32ToBGRA32 (UInt8* bytes, int imageSize) +{ + for (int i=0;i<imageSize;i+=4) + { + UInt8 swizzle[4]; + UInt8* dst = bytes + i; + memcpy(swizzle, dst, sizeof(swizzle)); + + dst[0] = swizzle[3]; + dst[1] = swizzle[2]; + dst[2] = swizzle[1]; + dst[3] = swizzle[0]; + } +} + +void SwizzleRGBA32ToBGRA32 (UInt8* bytes, int imageSize) +{ + for (int i=0;i<imageSize;i+=4) + { + UInt8 swizzle[4]; + UInt8* dst = bytes + i; + memcpy(swizzle, dst, sizeof(swizzle)); + + dst[0] = swizzle[2]; + dst[1] = swizzle[1]; + dst[2] = swizzle[0]; + dst[3] = swizzle[3]; + } +} + +void SwizzleBGRAToRGBA32 (UInt8* bytes, int imageSize) +{ + for (int i=0;i<imageSize;i+=4) + { + UInt8 swizzle[4]; + UInt8* dst = bytes + i; + memcpy(swizzle, dst, sizeof(swizzle)); + + dst[0] = swizzle[2]; + dst[1] = swizzle[1]; + dst[2] = swizzle[0]; + dst[3] = swizzle[3]; + } +} + +void SwizzleRGB24ToBGR24 (UInt8* bytes, int imageSize) +{ + for (int i=0;i<imageSize;i+=3) + { + UInt8 swizzle[3]; + UInt8* dst = bytes + i; + memcpy(swizzle, dst, sizeof(swizzle)); + + dst[0] = swizzle[2]; + dst[1] = swizzle[1]; + dst[2] = swizzle[0]; + } +} + +void Premultiply( ImageReference& image ) +{ + if (image.GetFormat() == kTexFormatRGBA32) + { + UInt8* data = image.GetImageData(); + int size = image.GetRowBytes() * image.GetHeight() / 4; + for (int i=0;i<size;i++) + { + UInt8* rgba = data + 4 * i; + rgba[0] = ((int)rgba[0] * (int)rgba[3]) / 255; + rgba[1] = ((int)rgba[1] * (int)rgba[3]) / 255; + rgba[2] = ((int)rgba[2] * (int)rgba[3]) / 255; + } + } + else if (image.GetFormat() == kTexFormatARGB32) + { + UInt8* data = image.GetImageData(); + int size = image.GetRowBytes() * image.GetHeight() / 4; + for (int i=0;i<size;i++) + { + UInt8* rgba = data + 4 * i; + rgba[1] = ((int)rgba[1] * (int)rgba[0]) / 255; + rgba[2] = ((int)rgba[2] * (int)rgba[0]) / 255; + rgba[3] = ((int)rgba[3] * (int)rgba[0]) / 255; + } + } + else + { + ErrorString("Unsupported"); + } +} + +inline UInt32 DecodeRGBMChannel(UInt32 val, UInt32 mult, UInt32 mask, UInt8 offset) +{ + UInt32 channel = (val & mask) >> offset; + channel *= mult; + channel /= 255 * 2; + channel = channel > 255 ? 255 : channel; + channel <<= offset; + + return channel; +} + +// Decodes RGBM encoded lightmaps into doubleLDR in place. +// Handles kTexFormatRGBA32 and kTexFormatARGB32 data. +void DecodeRGBM(int width, int height, UInt8* data, int pitch, const prcore::PixelFormat& pf) +{ + UInt8* rowData = data; + uint32 rmask = pf.GetRedMask(); + uint32 gmask = pf.GetGreenMask(); + uint32 bmask = pf.GetBlueMask(); + uint32 amask = pf.GetAlphaMask(); + uint8 roffset = (int)pf.GetRedOffset() - (int)pf.GetRedBits() + 1; + uint8 goffset = (int)pf.GetGreenOffset() - (int)pf.GetGreenBits() + 1; + uint8 boffset = (int)pf.GetBlueOffset() - (int)pf.GetBlueBits() + 1; + uint8 aoffset = (int)pf.GetAlphaOffset() - (int)pf.GetAlphaBits() + 1; + + for( int r = 0; r < height; ++r ) + { + uint32* pixel = (uint32*)rowData; + for( int c = 0; c < width; ++c ) + { + uint32 val = pixel[c]; + uint32 alpha = ((val & amask) >> aoffset) * kRGBMMaxRange; + pixel[c] = DecodeRGBMChannel(val, alpha, rmask, roffset) + + DecodeRGBMChannel(val, alpha, gmask, goffset) + + DecodeRGBMChannel(val, alpha, bmask, boffset) + + (255 << aoffset); + } + rowData += pitch; + } +} + + +void SetAlphaChannel(int width, int height, UInt8* data, int pitch, const prcore::PixelFormat& pf, UInt8 alpha) +{ + UInt8* rowData = data; + uint32 amask = pf.GetAlphaMask(); + uint8 aoffset = (int)pf.GetAlphaOffset() - (int)pf.GetAlphaBits() + 1; + uint32 properAlpha = (alpha << aoffset); + + for( int r = 0; r < height; ++r ) + { + uint32* pixel = (uint32*)rowData; + for( int c = 0; c < width; ++c ) + { + uint32 val = pixel[c]; + + // merge bits from two values according to a mask, + // equivalent to: (val & ~amask) | (properAlpha & amask) + pixel[c] = val ^ ((val ^ properAlpha) & amask); + } + rowData += pitch; + } +} + +void SetAlphaToRedChannel (int width, int height, UInt8* data, int pitch, const prcore::PixelFormat& pf) +{ + UInt8* rowData = data; + uint32 rmask = pf.GetRedMask(); + uint32 amask = pf.GetAlphaMask(); + uint8 roffset = (int)pf.GetRedOffset() - (int)pf.GetRedBits() + 1; + uint8 aoffset = (int)pf.GetAlphaOffset() - (int)pf.GetAlphaBits() + 1; + + for( int r = 0; r < height; ++r ) + { + uint32* pixel = (uint32*)rowData; + for( int c = 0; c < width; ++c ) + { + uint32 val = pixel[c]; + uint32 red = (val & rmask) >> roffset; + pixel[c] = (val & ~amask) | (red << aoffset); + } + rowData += pitch; + } +} + +inline UInt8 XenonToNormalSRGBTexture (UInt32 value) +{ + value = UInt32(LinearToGammaSpace(GammaToLinearSpaceXenon (value / 255.0F)) * 255.0F); + return std::min<UInt32>(value, 255); +} + +void XenonToNormalSRGBTexture (int width, int height, UInt8* data, int pitch, const prcore::PixelFormat& pf) +{ + UInt8* rowData = data; + uint32 rmask = pf.GetRedMask(); + uint32 gmask = pf.GetGreenMask(); + uint32 bmask = pf.GetBlueMask(); + uint32 amask = pf.GetAlphaMask(); + uint8 roffset = (int)pf.GetRedOffset() - (int)pf.GetRedBits() + 1; + uint8 goffset = (int)pf.GetGreenOffset() - (int)pf.GetGreenBits() + 1; + uint8 boffset = (int)pf.GetBlueOffset() - (int)pf.GetBlueBits() + 1; + uint8 aoffset = (int)pf.GetAlphaOffset() - (int)pf.GetAlphaBits() + 1; + + for( int r = 0; r < height; ++r ) + { + uint32* pixel = (uint32*)rowData; + for( int c = 0; c < width; ++c ) + { + uint32 val = pixel[c]; + uint32 r = (val & rmask) >> roffset; + uint32 g = (val & gmask) >> goffset; + uint32 b = (val & bmask) >> boffset; + uint32 a = (val & amask) >> aoffset; + + r = XenonToNormalSRGBTexture (r); + g = XenonToNormalSRGBTexture (g); + b = XenonToNormalSRGBTexture (b); + + pixel[c] = (r << roffset) | (g << goffset) | (b << boffset) | (a << aoffset); + } + rowData += pitch; + } +} + + + + +// -------------------------------------------------------------------------- + + +const char* kUnsupportedGetPixelOpFormatMessage = "Unsupported texture format - needs to be ARGB32, RGBA32, BGRA32, RGB24, Alpha8 or DXT"; +const char* kUnsupportedSetPixelOpFormatMessage = "Unsupported texture format - needs to be ARGB32, RGBA32, RGB24 or Alpha8"; + +inline int RepeatInt (int x, int width) +{ + if (width == 0) return 0; + if( x < 0 ) { + int times = -(x/width)+1; + x += width * times; + } + x = x % width; + return x; +} + +inline int TextureWrap(int x, int max, TextureWrapMode wrapMode) +{ + if (wrapMode == kTexWrapRepeat) + { + return RepeatInt(x, max); + } + else + { + if (max <= 0) + return 0; + return clamp(x, 0, max-1); + } +} + +void SetImagePixel (ImageReference& image, int x, int y, TextureWrapMode wrap, const ColorRGBAf& color) +{ + int width = image.GetWidth(); + int height = image.GetHeight(); + if (x < 0 || x >= width || y < 0 || y >= height) + { + x = TextureWrap(x, width, wrap); + y = TextureWrap(y, height, wrap); + } + + if (image.GetFormat() == kTexFormatARGB32) + { + UInt8* pixel = image.GetRowPtr(y) + x * 4; + pixel[1] = RoundfToIntPos(clamp01(color.r) * 255.0); + pixel[2] = RoundfToIntPos(clamp01(color.g) * 255.0); + pixel[3] = RoundfToIntPos(clamp01(color.b) * 255.0); + pixel[0] = RoundfToIntPos(clamp01(color.a) * 255.0); + } + else if (image.GetFormat() == kTexFormatRGBA32) + { + UInt8* pixel = image.GetRowPtr(y) + x * 4; + pixel[0] = RoundfToIntPos(clamp01(color.r) * 255.0); + pixel[1] = RoundfToIntPos(clamp01(color.g) * 255.0); + pixel[2] = RoundfToIntPos(clamp01(color.b) * 255.0); + pixel[3] = RoundfToIntPos(clamp01(color.a) * 255.0); + } + else if (image.GetFormat() == kTexFormatRGB24) + { + UInt8* pixel = image.GetRowPtr(y) + x * 3; + pixel[0] = RoundfToIntPos(clamp01(color.r) * 255.0); + pixel[1] = RoundfToIntPos(clamp01(color.g) * 255.0); + pixel[2] = RoundfToIntPos(clamp01(color.b) * 255.0); + } + else if (image.GetFormat() == kTexFormatRGB565) + { + UInt16* pixel = (UInt16 *)(image.GetRowPtr(y) + x * 2); + UInt16 r = (UInt16)(RoundfToIntPos( clamp01(color.r) * 31.0f)); + UInt16 g = (UInt16)(RoundfToIntPos( clamp01(color.g) * 63.0f)); + UInt16 b = (UInt16)(RoundfToIntPos( clamp01(color.b) * 31.0f)); + pixel[0] = r<<11 | g<<5 | b; + } + else if (image.GetFormat() == kTexFormatAlpha8) + { + UInt8* pixel = image.GetRowPtr(y) + x; + pixel[0] = RoundfToIntPos(clamp01(color.a) * 255.0); + } + else + { + ErrorString(kUnsupportedSetPixelOpFormatMessage); + } +} + + +ColorRGBA32 GetImagePixel (UInt8* data, int width, int height, TextureFormat format, TextureWrapMode wrap, int x, int y) +{ + if (x < 0 || x >= width || y < 0 || y >= height) + { + x = TextureWrap (x, width, wrap); + y = TextureWrap (y, height, wrap); + } + + if( IsCompressedDXTTextureFormat(format) ) + { + int texWidth = std::max (width, 4); + + UInt32 uncompressed [16]; + int blockBytes = (format == kTexFormatDXT1 ? 8 : 16); + + const UInt8 *pos = data + ((x/4) + (y/4)*(texWidth/4)) * blockBytes; + DecompressNativeTextureFormat (format, 4, 4, (const UInt32*)pos, 4, 4, uncompressed); + + const UInt8* pixel = (const UInt8*)(uncompressed + x%4 + 4* (y%4)); + return ColorRGBA32(pixel[0], pixel[1], pixel[2], pixel[3]); + } + else if( IsAnyCompressedTextureFormat (format) ) + { + ErrorString(kUnsupportedGetPixelOpFormatMessage); + } + else + { + ImageReference image (width, height, GetRowBytesFromWidthAndFormat(width, format), format, data); + const UInt8* pixel; + if (format == kTexFormatARGB32) + { + pixel = image.GetRowPtr(y) + x * 4; + return ColorRGBA32 (pixel[1], pixel[2], pixel[3], pixel[0]); + } + else if (format == kTexFormatRGBA32) + { + pixel = image.GetRowPtr(y) + x * 4; + return ColorRGBA32 (pixel[0], pixel[1], pixel[2], pixel[3]); + } + else if (format == kTexFormatBGRA32) + { + pixel = image.GetRowPtr(y) + x * 4; + return ColorRGBA32 (pixel[2], pixel[1], pixel[0], pixel[3]); + } + else if (format == kTexFormatRGB24) + { + pixel = image.GetRowPtr(y) + x * 3; + return ColorRGBA32 (pixel[0], pixel[1], pixel[2], 255); + } + else if (format == kTexFormatAlpha8) + { + pixel = image.GetRowPtr(y) + x; + return ColorRGBA32 (255, 255, 255, pixel[0]); + } + else if (format == kTexFormatRGBA4444) + { + pixel = image.GetRowPtr(y) + x * 2; + return ColorRGBA32( + (pixel[1] & 0xF0) << 4 | (pixel[1] & 0xF0), + (pixel[1] & 0x0F) << 4 | (pixel[1] & 0x0F), + (pixel[0] & 0xF0) << 4 | (pixel[0] & 0xF0), + (pixel[0] & 0x0F) << 4 | (pixel[0] & 0x0F) + ); + } + else if (format == kTexFormatARGB4444) + { + pixel = image.GetRowPtr(y) + x * 2; + return ColorRGBA32( + (pixel[1] & 0x0F) << 4 | (pixel[1] & 0x0F), + (pixel[0] & 0xF0) << 4 | (pixel[0] & 0xF0), + (pixel[0] & 0x0F) << 4 | (pixel[0] & 0x0F), + (pixel[1] & 0xF0) << 4 | (pixel[1] & 0xF0) + ); + } + else if (format == kTexFormatRGB565) + { + pixel = image.GetRowPtr(y) + x * 2; + UInt16 c = *reinterpret_cast<const UInt16*>(pixel); + UInt8 r = (UInt8)((c >> 11)&0x1F); + UInt8 g = (UInt8)((c >> 5)&0x3F); + UInt8 b = (UInt8)((c >> 0)&0x1F); + return ColorRGBA32 (r<<3|r>>2, g<<2|g>>4, b<<3|b>>2, 255); + } + else + { + ErrorString(kUnsupportedGetPixelOpFormatMessage); + } + } + return ColorRGBA32(255,255,255,255); +} + +ColorRGBAf GetImagePixelBilinear (UInt8* data, int width, int height, TextureFormat format, TextureWrapMode wrap, float u, float v) +{ + u *= width; + v *= height; + + int xBase = FloorfToInt(u); + int yBase = FloorfToInt(v); + + float s = u - xBase; + float t = v - yBase; + + ColorRGBAf colors[4]; + + if (IsCompressedDXTTextureFormat(format)) + { + if (xBase < 0 || xBase+1 >= width || yBase < 0 || yBase+1 >= height) + { + for (int i=0;i<4;i++) + { + int x = xBase; + int y = yBase; + if (i & 1) + x++; + if (i & 2) + y++; + + x = TextureWrap(x, width, wrap); + y = TextureWrap(y, height, wrap); + + colors[i] = GetImagePixel (data, width, height, format, wrap, x, y); + } + } + else + { + GetImagePixelBlock (data, width, height, format, xBase, yBase, 2, 2, colors); + } + } + else if( IsAnyCompressedTextureFormat(format) ) + { + ErrorString(kUnsupportedGetPixelOpFormatMessage); + return ColorRGBAf(1.0F,1.0F,1.0F,1.0F); + } + else + { + ImageReference image (width, height, GetRowBytesFromWidthAndFormat(width, format), format, data); + for (int i=0;i<4;i++) + { + int x = xBase; + int y = yBase; + if (i & 1) + x++; + if (i & 2) + y++; + + if (x < 0 || x >= width || y < 0 || y >= height) + { + x = TextureWrap(x, width, wrap); + y = TextureWrap(y, height, wrap); + } + + UInt8* pixel; + if (format == kTexFormatARGB32) + { + pixel = image.GetRowPtr(y) + x * 4; + colors[i] = ColorRGBA32 (pixel[1], pixel[2], pixel[3], pixel[0]); + } + else if (format == kTexFormatRGBA32) + { + pixel = image.GetRowPtr(y) + x * 4; + colors[i] = ColorRGBA32 (pixel[0], pixel[1], pixel[2], pixel[3]); + } + else if (format == kTexFormatBGRA32) + { + pixel = image.GetRowPtr(y) + x * 4; + colors[i] = ColorRGBA32 (pixel[2], pixel[1], pixel[0], pixel[3]); + } + else if (format == kTexFormatRGB24) + { + pixel = image.GetRowPtr(y) + x * 3; + colors[i] = ColorRGBA32 (pixel[0], pixel[1], pixel[2], 255); + } + else if (format == kTexFormatAlpha8) + { + pixel = image.GetRowPtr(y) + x; + colors[i] = ColorRGBA32 (255, 255, 255, pixel[0]); + } + else + { + ErrorString(kUnsupportedGetPixelOpFormatMessage); + return ColorRGBAf(1.0F,1.0F,1.0F,1.0F); + } + } + } + ColorRGBAf a = Lerp(colors[0], colors[1], s); + ColorRGBAf b = Lerp(colors[2], colors[3], s); + return Lerp(a, b, t); +} + +bool GetImagePixelBlock (UInt8* data, int dataWidth, int dataHeight, TextureFormat format, int x, int y, int blockWidth, int blockHeight, ColorRGBAf* outColors) +{ + // Checks + if (blockWidth <= 0 || blockHeight <= 0) + { + ErrorString ("Width and height must be positive"); + return false; + } + + // TODO: repeat/crop support + if (x < 0 || y < 0 || x + blockWidth < 0 || y + blockHeight < 0 || x + blockWidth > dataWidth || y + blockHeight > dataHeight) + { + char mad[255]; + + if (x < 0) + sprintf(mad, "Texture rectangle is out of bounds (%d < 0)", x); + if (y < 0) + sprintf(mad, "Texture rectangle is out of bounds (%d < 0)", y); + if (x + blockWidth > dataWidth) + sprintf(mad, "Texture rectangle is out of bounds (%d + %d > %d)", x, blockWidth, dataWidth); + if (y + blockHeight > dataHeight) + sprintf(mad, "Texture rectangle is out of bounds (%d + %d > %d)", y, blockHeight, dataHeight); + + ErrorString(mad); + return false; + } + + if (IsCompressedDXTTextureFormat(format)) + { + int texWidth = std::max (dataWidth, 4); + + int paddedWidth = x+blockWidth-(x&~3); + if(paddedWidth % 4) + paddedWidth = (paddedWidth&~3) + 4; + int paddedHeight = y+blockHeight-(y&~3); + if(paddedHeight % 4) + paddedHeight = (paddedHeight&~3) + 4; + + UInt32* uncompressed; + ALLOC_TEMP(uncompressed, UInt32, paddedWidth * paddedHeight); + + int blockBytes = (format == kTexFormatDXT1 ? 8 : 16); + + for(int line = 0; line< paddedHeight; line+=4) + { + UInt8 *pos = data + ((x/4) + ((y+line)/4)*(texWidth/4)) * blockBytes; + DecompressNativeTextureFormat(format, paddedWidth, 4, (UInt32*)pos, paddedWidth, 4, uncompressed+(line*paddedWidth)); + } + + ColorRGBAf* dest = outColors; + const UInt8* pixelRow = (UInt8*)(uncompressed + x%4 + (y%4)*paddedWidth); + for( int iy = 0; iy < blockHeight; ++iy ) + { + const UInt8* pixel = pixelRow; + for( int ix = 0; ix < blockWidth; ++ix ) + { + *dest = ColorRGBA32(pixel[0], pixel[1], pixel[2], pixel[3]); + pixel += 4; + ++dest; + } + pixelRow += paddedWidth * 4; + } + } + else + { + ImageReference image (dataWidth, dataHeight, GetRowBytesFromWidthAndFormat(dataWidth, format), format, data); + + ColorRGBAf* dest = outColors; + + if (format == kTexFormatARGB32) + { + const UInt8* pixelRow = image.GetRowPtr(y) + x * 4; + for( int iy = 0; iy < blockHeight; ++iy ) + { + const UInt8* pixel = pixelRow; + for( int ix = 0; ix < blockWidth; ++ix ) + { + *dest = ColorRGBA32(pixel[1], pixel[2], pixel[3], pixel[0]); + pixel += 4; + ++dest; + } + pixelRow += image.GetRowBytes(); + } + } + else if (format == kTexFormatRGBA32) + { + const UInt8* pixelRow = image.GetRowPtr(y) + x * 4; + for( int iy = 0; iy < blockHeight; ++iy ) + { + const UInt8* pixel = pixelRow; + for( int ix = 0; ix < blockWidth; ++ix ) + { + *dest = ColorRGBA32(pixel[0], pixel[1], pixel[2], pixel[3]); + pixel += 4; + ++dest; + } + pixelRow += image.GetRowBytes(); + } + } + else if (format == kTexFormatBGRA32) + { + const UInt8* pixelRow = image.GetRowPtr(y) + x * 4; + for( int iy = 0; iy < blockHeight; ++iy ) + { + const UInt8* pixel = pixelRow; + for( int ix = 0; ix < blockWidth; ++ix ) + { + *dest = ColorRGBA32(pixel[2], pixel[1], pixel[0], pixel[3]); + pixel += 4; + ++dest; + } + pixelRow += image.GetRowBytes(); + } + } + else if (format == kTexFormatRGB24) + { + const UInt8* pixelRow = image.GetRowPtr(y) + x * 3; + for( int iy = 0; iy < blockHeight; ++iy ) + { + const UInt8* pixel = pixelRow; + for( int ix = 0; ix < blockWidth; ++ix ) + { + *dest = ColorRGBA32(pixel[0], pixel[1], pixel[2], 255); + pixel += 3; + ++dest; + } + pixelRow += image.GetRowBytes(); + } + } + else if (format == kTexFormatAlpha8) + { + const UInt8* pixelRow = image.GetRowPtr(y) + x; + for( int iy = 0; iy < blockHeight; ++iy ) + { + const UInt8* pixel = pixelRow; + for( int ix = 0; ix < blockWidth; ++ix ) + { + *dest = ColorRGBA32 (255, 255, 255, pixel[0]); + pixel += 1; + ++dest; + } + pixelRow += image.GetRowBytes(); + } + } + else if (format == kTexFormatRGB565) + { + const UInt8* pixelRow = image.GetRowPtr(y) + x; + for( int iy = 0; iy < blockHeight; ++iy ) + { + const UInt8* pixel = pixelRow; + for( int ix = 0; ix < blockWidth; ++ix ) + { + UInt16 c = (UInt16)pixel[0] | ((UInt16)pixel[1])<<8; + UInt8 r = (UInt8)((c >> 11)&31); + UInt8 g = (UInt8)((c >> 5)&63); + UInt8 b = (UInt8)((c >> 0)&31); + + *dest = ColorRGBA32 (r<<3|r>>2, g<<2|g>>4, b<<3|b>>2, 255); + pixel += 2; + ++dest; + } + pixelRow += image.GetRowBytes(); + } + } + else + { + ErrorString(kUnsupportedGetPixelOpFormatMessage); + return false; + } + } + return true; +} + + +bool SetImagePixelBlock (UInt8* data, int dataWidth, int dataHeight, TextureFormat format, int x, int y, int blockWidth, int blockHeight, int pixelCount, const ColorRGBAf* pixels) +{ + if (IsAnyCompressedTextureFormat(format)) + { + ErrorString(kUnsupportedSetPixelOpFormatMessage); + return false; + } + if (blockWidth <= 0 || blockHeight <= 0) + { + ErrorString ("Width and height must be positive"); + return false; + } + + int tmp = blockWidth * blockHeight; + if (blockHeight != tmp / blockWidth || blockWidth * blockHeight > pixelCount ) // check for overflow as well + { + ErrorString ("Array size must be at least width*height"); + return false; + } + + if (x < 0 || y < 0 || x + blockWidth < 0 || y + blockHeight < 0 || x + blockWidth > dataWidth || y + blockHeight > dataHeight) + { + ErrorString ("Texture rectangle is out of bounds"); + return false; + } + + ImageReference image (dataWidth, dataHeight, GetRowBytesFromWidthAndFormat(dataWidth, format), format, data); + + if (format == kTexFormatARGB32) + { + UInt8* pixelRow = image.GetRowPtr(y) + x * 4; + for( int iy = 0; iy < blockHeight; ++iy ) + { + UInt8* pixel = pixelRow; + for( int ix = 0; ix < blockWidth; ++ix ) + { + pixel[1] = NormalizedToByte( pixels->r); + pixel[2] = NormalizedToByte( pixels->g); + pixel[3] = NormalizedToByte( pixels->b); + pixel[0] = NormalizedToByte( pixels->a); + pixel += 4; + ++pixels; + } + pixelRow += image.GetRowBytes(); + } + } + else if (format == kTexFormatRGBA32) + { + UInt8* pixelRow = image.GetRowPtr(y) + x * 4; + for( int iy = 0; iy < blockHeight; ++iy ) + { + UInt8* pixel = pixelRow; + for( int ix = 0; ix < blockWidth; ++ix ) + { + pixel[0] = NormalizedToByte( pixels->r); + pixel[1] = NormalizedToByte( pixels->g); + pixel[2] = NormalizedToByte( pixels->b); + pixel[3] = NormalizedToByte( pixels->a); + pixel += 4; + ++pixels; + } + pixelRow += image.GetRowBytes(); + } + } + else if (format == kTexFormatRGB24) + { + UInt8* pixelRow = image.GetRowPtr(y) + x * 3; + for( int iy = 0; iy < blockHeight; ++iy ) + { + UInt8* pixel = pixelRow; + for( int ix = 0; ix < blockWidth; ++ix ) + { + pixel[0] = NormalizedToByte(pixels->r); + pixel[1] = NormalizedToByte(pixels->g); + pixel[2] = NormalizedToByte(pixels->b); + pixel += 3; + ++pixels; + } + pixelRow += image.GetRowBytes(); + } + } + else if (format == kTexFormatAlpha8) + { + UInt8* pixelRow = image.GetRowPtr(y) + x; + for( int iy = 0; iy < blockHeight; ++iy ) + { + UInt8* pixel = pixelRow; + for( int ix = 0; ix < blockWidth; ++ix ) + { + pixel[0] = RoundfToIntPos( clamp01(pixels->a) * 255.0f ); + pixel += 1; + ++pixels; + } + pixelRow += image.GetRowBytes(); + } + } + else if (format == kTexFormatRGB565) + { + UInt8* pixelRow = image.GetRowPtr(y) + x; + for( int iy = 0; iy < blockHeight; ++iy ) + { + UInt16* pixel = (UInt16 *)pixelRow; + for( int ix = 0; ix < blockWidth; ++ix ) + { + UInt16 r = (UInt16)(RoundfToIntPos(clamp01(pixels->r) * 31.0f)); + UInt16 g = (UInt16)(RoundfToIntPos(clamp01(pixels->g) * 63.0f)); + UInt16 b = (UInt16)(RoundfToIntPos(clamp01(pixels->b) * 31.0f)); + pixel[0] = r<<11 | g<<5 | b; + pixel += 1; + ++pixels; + } + pixelRow += image.GetRowBytes(); + } + } + else + { + ErrorString(kUnsupportedSetPixelOpFormatMessage); + return false; + } + + return true; +} + + + +// -------------------------------------------------------------------------- + + +#if ENABLE_UNIT_TESTS + +#include "External/UnitTest++/src/UnitTest++.h" + + +SUITE (ImageOpsTests) +{ + +TEST (RepeatInt) +{ + // valdemar: seems like a CW compiler bug, error: (10139) division by 0 +#if !UNITY_WII + // handle zero width + CHECK_EQUAL (0, RepeatInt (7,0)); +#endif + // handle positive args + CHECK_EQUAL (7, RepeatInt (7,13)); + CHECK_EQUAL (0, RepeatInt (13,13)); + CHECK_EQUAL (1, RepeatInt (170,13)); + // handle negative args + CHECK_EQUAL (12, RepeatInt (-1,13)); + CHECK_EQUAL (0, RepeatInt (-13,13)); +} + +TEST (TextureWrap) +{ + // valdemar: seems like a CW compiler bug, error: (10139) division by 0 +#if !UNITY_WII + // handle zero width + CHECK_EQUAL (0, TextureWrap (7,0,kTexWrapClamp)); + CHECK_EQUAL (0, TextureWrap (7,0,kTexWrapRepeat)); +#endif + // repeat mode + CHECK_EQUAL (7, TextureWrap (7,13,kTexWrapRepeat)); + CHECK_EQUAL (1, TextureWrap (170,13,kTexWrapRepeat)); + CHECK_EQUAL (12, TextureWrap (-1,13,kTexWrapRepeat)); + // clamp mode + CHECK_EQUAL (7, TextureWrap (7,13,kTexWrapClamp)); + CHECK_EQUAL (0, TextureWrap (-1,13,kTexWrapClamp)); + CHECK_EQUAL (12, TextureWrap (13,13,kTexWrapClamp)); +} + +TEST (SetGetImagePixelARGB) +{ + UInt8 data[4][4]; + memset (data, 13, sizeof(data)); + ImageReference image (2, 2, 8, kTexFormatARGB32, data); + SetImagePixel (image, 0, 0, kTexWrapRepeat, ColorRGBAf(1.0f,0.5f,0.3f,0.2f)); // sets [0] + CHECK (data[0][1]==255 && data[0][2]==128 && data[0][3]==77 && data[0][0]==51); + SetImagePixel (image, 3, 8, kTexWrapRepeat, ColorRGBAf(0.1f,0.2f,0.3f,0.4f)); // sets [1] due to repeat + CHECK (data[1][1]==26 && data[1][2]==51 && data[1][3]==77 && data[1][0]==102); + SetImagePixel (image, -3, 1, kTexWrapClamp, ColorRGBAf(0.3f,0.4f,0.5f,0.6f)); // sets [2] due to clamp + CHECK (data[2][1]==77 && data[2][2]==102 && data[2][3]==128 && data[2][0]==153); + + CHECK (data[3][1]==13 && data[3][2]==13 && data[3][3]==13 && data[3][0]==13); // [3] left untouched + + CHECK(ColorRGBA32(ColorRGBAf(1.0f,0.5f,0.3f,0.2f)) == GetImagePixel (&data[0][0], 2, 2, image.GetFormat(), kTexWrapRepeat, 2, 2)); // gets [0] due to repeat + CHECK(ColorRGBA32(ColorRGBAf(0.1f,0.2f,0.3f,0.4f)) == GetImagePixel (&data[0][0], 2, 2, image.GetFormat(), kTexWrapRepeat, 5, -2)); // gets [1] due to repeat + CHECK(ColorRGBA32(ColorRGBAf(0.3f,0.4f,0.5f,0.6f)) == GetImagePixel (&data[0][0], 2, 2, image.GetFormat(), kTexWrapClamp, -1, 1)); // gets [2] due to clamp +} + +TEST (SetGetImagePixelRGBA) +{ + UInt8 data[4][4]; + memset (data, 13, sizeof(data)); + ImageReference image (2, 2, 8, kTexFormatRGBA32, data); + SetImagePixel (image, 0, 0, kTexWrapRepeat, ColorRGBAf(1.0f,0.5f,0.3f,0.2f)); // sets [0] + CHECK (data[0][0]==255 && data[0][1]==128 && data[0][2]==77 && data[0][3]==51); + SetImagePixel (image, 3, 8, kTexWrapRepeat, ColorRGBAf(0.1f,0.2f,0.3f,0.4f)); // sets [1] due to repeat + CHECK (data[1][0]==26 && data[1][1]==51 && data[1][2]==77 && data[1][3]==102); + SetImagePixel (image, -3, 1, kTexWrapClamp, ColorRGBAf(0.3f,0.4f,0.5f,0.6f)); // sets [2] due to clamp + CHECK (data[2][0]==77 && data[2][1]==102 && data[2][2]==128 && data[2][3]==153); + + CHECK (data[3][1]==13 && data[3][2]==13 && data[3][3]==13 && data[3][0]==13); // [3] left untouched + + CHECK(ColorRGBA32(ColorRGBAf(1.0f,0.5f,0.3f,0.2f)) == GetImagePixel (&data[0][0], 2, 2, image.GetFormat(), kTexWrapRepeat, 2, 2)); // gets [0] due to repeat + CHECK(ColorRGBA32(ColorRGBAf(0.1f,0.2f,0.3f,0.4f)) == GetImagePixel (&data[0][0], 2, 2, image.GetFormat(), kTexWrapRepeat, 5, -2)); // gets [1] due to repeat + CHECK(ColorRGBA32(ColorRGBAf(0.3f,0.4f,0.5f,0.6f)) == GetImagePixel (&data[0][0], 2, 2, image.GetFormat(), kTexWrapClamp, -1, 1)); // gets [2] due to clamp +} + +TEST (SetGetImagePixelRGB) +{ + UInt8 data[4][3]; + memset (data, 13, sizeof(data)); + ImageReference image (2, 2, 6, kTexFormatRGB24, data); + SetImagePixel (image, 0, 0, kTexWrapClamp, ColorRGBAf(1.0f,0.5f,0.3f,0.2f)); // sets [0] + CHECK (data[0][0]==255 && data[0][1]==128 && data[0][2]==77); + SetImagePixel (image, 1, 0, kTexWrapClamp, ColorRGBAf(0.1f,0.2f,0.3f,0.4f)); // sets [1] + CHECK (data[1][0]==26 && data[1][1]==51 && data[1][2]==77); + SetImagePixel (image, 0, 1, kTexWrapClamp, ColorRGBAf(0.3f,0.4f,0.5f,0.6f)); // sets [2] + CHECK (data[2][0]==77 && data[2][1]==102 && data[2][2]==128); + + CHECK (data[3][0]==13 && data[3][1]==13 && data[3][2]==13); // [3] left untouched + + CHECK(ColorRGBA32(ColorRGBAf(1.0f,0.5f,0.3f,1)) == GetImagePixel (&data[0][0], 2, 2, image.GetFormat(), kTexWrapRepeat, 2, 2)); // gets [0] due to repeat + CHECK(ColorRGBA32(ColorRGBAf(0.1f,0.2f,0.3f,1)) == GetImagePixel (&data[0][0], 2, 2, image.GetFormat(), kTexWrapRepeat, 5, -2)); // gets [1] due to repeat + CHECK(ColorRGBA32(ColorRGBAf(0.3f,0.4f,0.5f,1)) == GetImagePixel (&data[0][0], 2, 2, image.GetFormat(), kTexWrapClamp, -1, 1)); // gets [2] due to clamp +} + +TEST (SetGetImagePixelAlpha) +{ + UInt8 data[4]; + memset (data, 13, sizeof(data)); + ImageReference image (2, 2, 2, kTexFormatAlpha8, data); + SetImagePixel (image, -3, -2, kTexWrapClamp, ColorRGBAf(1.0f,0.5f,0.3f,0.2f)); // sets [0] due to clamp + CHECK (data[0]==51); + SetImagePixel (image, 1, -4, kTexWrapRepeat, ColorRGBAf(0.1f,0.2f,0.3f,0.4f)); // sets [1] due to repeat + CHECK (data[1]==102); + SetImagePixel (image, -4, 7, kTexWrapRepeat, ColorRGBAf(0.3f,0.4f,0.5f,0.6f)); // sets [2] due to repeat + CHECK (data[2]==153); + + CHECK (data[3]==13); // [3] left untouched + + CHECK(ColorRGBA32(ColorRGBAf(1,1,1,0.2f)) == GetImagePixel (&data[0], 2, 2, image.GetFormat(), kTexWrapRepeat, 2, 2)); // gets [0] due to repeat + CHECK(ColorRGBA32(ColorRGBAf(1,1,1,0.4f)) == GetImagePixel (&data[0], 2, 2, image.GetFormat(), kTexWrapRepeat, 5, -2)); // gets [1] due to repeat + CHECK(ColorRGBA32(ColorRGBAf(1,1,1,0.6f)) == GetImagePixel (&data[0], 2, 2, image.GetFormat(), kTexWrapClamp, -1, 1)); // gets [2] due to clamp +} + +TEST (SetGetImagePixelRGB565) +{ + UInt16 data[4]; + memset (data, 0xab, sizeof(data)); + ImageReference image (2, 2, 4, kTexFormatRGB565, data); + SetImagePixel (image, 0, 0, kTexWrapClamp, ColorRGBAf(1.0f,0.0f,0.0f,0.2f)); // sets [0] + CHECK (data[0]==0xf800); + SetImagePixel (image, 1, 0, kTexWrapClamp, ColorRGBAf(0.0f,1.0f,0.0f,0.4f)); // sets [1] + CHECK (data[1]==0x07e0); + SetImagePixel (image, 0, 1, kTexWrapClamp, ColorRGBAf(0.0f,0.0f,1.0f,0.6f)); // sets [2] + CHECK (data[2]==0x001f); + CHECK (data[3]==0xabab); // [3] still left untouched + + ColorRGBAf gray(14.0f/31.0f, 31.0f/63.0f, 16.0f/31.0f, 1.0f); + SetImagePixel (image, 1, 1, kTexWrapClamp, gray); // sets [3] + CHECK (data[3]==0x73f0); + + UInt8* srcData = reinterpret_cast<UInt8*>(&data[0]); + CHECK(ColorRGBA32(ColorRGBAf(1.0f,0.0f,0.0f,1)) == GetImagePixel (srcData, 2, 2, image.GetFormat(), kTexWrapRepeat, 2, 2)); // gets [0] due to repeat + CHECK(ColorRGBA32(ColorRGBAf(0.0f,1.0f,0.0f,1)) == GetImagePixel (srcData, 2, 2, image.GetFormat(), kTexWrapRepeat, 5, -2)); // gets [1] due to repeat + CHECK(ColorRGBA32(ColorRGBAf(0.0f,0.0f,1.0f,1)) == GetImagePixel (srcData, 2, 2, image.GetFormat(), kTexWrapClamp, -1, 1)); // gets [2] due to clamp + CHECK(ColorRGBA32(gray) == GetImagePixel (srcData, 2, 2, image.GetFormat(), kTexWrapClamp, 2, 2)); // gets [3] due to clamp +} + +TEST (SetImagePixelBlockARGB) +{ + UInt8 data[16][16][4]; + memset (data, 13, sizeof(data)); + ImageReference image (16, 16, 16*4, kTexFormatARGB32, data); + + ColorRGBAf color (1,0,1,0); + SetImagePixelBlock (&data[0][0][0], 16, 16, kTexFormatARGB32, 15,15,1,1, 1, &color); + CHECK (data[15][15][1]==255 && data[15][15][2]==0 && data[15][15][3]==255 && data[15][15][0]==0); +} + +#if UNITY_EDITOR +TEST (BlitBilinearARGBToFloat) +{ + Image src (128, 256, kTexFormatARGB32); + Image dst (16, 16, kTexFormatARGB32); + // ProphecySDK's bilinear blitter from integer source to floating point destination + // when sizes are different allocates temporary intermediate image, and blit there + // can result in access violation for the last pixel. This unit test would crash then, especially + // on Macs. ProphecySDK modified to allocate one pixel more for temporary images, just like + // we do for ours. + dst.ReformatImage (src, 128, 128, kTexFormatARGBFloat, Image::BLIT_BILINEAR_SCALE); +} +#endif + +TEST (CreateMipMap2x2) +{ + ColorRGBA32 data[4+1+1]; // 2x2, 1x1, one extra for out-of-bounds check + memset (data, 13, sizeof(data)); + data[0] = ColorRGBA32(255,255,255,255); + data[1] = ColorRGBA32(255,255,255,0); + data[2] = ColorRGBA32(255,255,0,0); + data[3] = ColorRGBA32(255,0,0,0); + CreateMipMap ((UInt8*)data, 2, 2, 1, kTexFormatARGB32); + + // next mip level of 1x1 size + CHECK(ColorRGBA32(255,191,127,63) == data[4]); + + // data after that should be untouched + CHECK(ColorRGBA32(13,13,13,13) == data[5]); +} +TEST (CreateMipMap4x1) +{ + ColorRGBA32 data[4+2+1+1]; // 4x1, 2x1, 1x1, one extra for out-of-bounds check + memset (data, 13, sizeof(data)); + data[0] = ColorRGBA32(255,255,255,255); + data[1] = ColorRGBA32(255,255,255,0); + data[2] = ColorRGBA32(255,255,0,0); + data[3] = ColorRGBA32(255,0,0,0); + CreateMipMap ((UInt8*)data, 4, 1, 1, kTexFormatARGB32); + + // next mip level of 2x1 size + CHECK(ColorRGBA32(255,255,255,127) == data[4]); + CHECK(ColorRGBA32(255,127,0,0) == data[5]); + + // next mip level of 1x1 size + CHECK(ColorRGBA32(255,191,127,63) == data[6]); + + // data after that should be untouched + CHECK(ColorRGBA32(13,13,13,13) == data[7]); +} +TEST (CreateMipMap4x1x2) +{ + ColorRGBA32 data[8+2+1+1]; // 4x1x2, 2x1x1, 1x1x1, one extra for out-of-bounds check + memset (data, 13, sizeof(data)); + data[0] = ColorRGBA32(255,255,255,255); + data[1] = ColorRGBA32(255,255,255,0); + data[2] = ColorRGBA32(255,255,0,0); + data[3] = ColorRGBA32(255,0,0,0); + data[4] = ColorRGBA32(128,128,128,128); + data[5] = ColorRGBA32(128,128,128,0); + data[6] = ColorRGBA32(128,128,0,0); + data[7] = ColorRGBA32(128,0,0,0); + CreateMipMap ((UInt8*)data, 4, 1, 2, kTexFormatARGB32); + + // next mip level, 2x1x1 size + CHECK(ColorRGBA32(191,191,191,95) == data[8]); + CHECK(ColorRGBA32(191,95,0,0) == data[9]); + + // next mip level, 1x1x1 size + CHECK(ColorRGBA32(191,143,95,47) == data[10]); + + // data after that should be untouched + CHECK(ColorRGBA32(13,13,13,13) == data[11]); +} +TEST (CreateMipMap4x1x3) +{ + ColorRGBA32 data[12+2+1+1]; // 4x1x2, 2x1x1, 1x1x1, one extra for out-of-bounds check + memset (data, 13, sizeof(data)); + data[0] = ColorRGBA32(255,255,255,255); + data[1] = ColorRGBA32(255,255,255,0); + data[2] = ColorRGBA32(255,255,0,0); + data[3] = ColorRGBA32(255,0,0,0); + data[4] = ColorRGBA32(128,128,128,128); + data[5] = ColorRGBA32(128,128,128,0); + data[6] = ColorRGBA32(128,128,0,0); + data[7] = ColorRGBA32(128,0,0,0); + data[8] = ColorRGBA32(64,64,64,64); + data[9] = ColorRGBA32(64,64,64,0); + data[10] = ColorRGBA32(64,64,0,0); + data[11] = ColorRGBA32(64,0,0,0); + CreateMipMap ((UInt8*)data, 4, 1, 3, kTexFormatARGB32); + + // next mip level, 2x1x1 size + CHECK(ColorRGBA32(191,191,191,95) == data[12]); + CHECK(ColorRGBA32(191,95,0,0) == data[13]); + + // next mip level, 1x1x1 size + CHECK(ColorRGBA32(191,143,95,47) == data[14]); + + // data after that should be untouched + CHECK(ColorRGBA32(13,13,13,13) == data[15]); +} + + +} // SUITE + +#endif // ENABLE_UNIT_TESTS diff --git a/Runtime/Graphics/Image.h b/Runtime/Graphics/Image.h new file mode 100644 index 0000000..ef75c9e --- /dev/null +++ b/Runtime/Graphics/Image.h @@ -0,0 +1,178 @@ +#ifndef IMAGE_H +#define IMAGE_H + +#include "TextureFormat.h" +#include "Runtime/Serialize/SerializeUtility.h" +#include "Runtime/Allocator/MemoryMacros.h" + +class Image; +class ColorRGBA32; +class ColorRGBAf; +class ImageReference; +namespace prcore { class PixelFormat; } + +prcore::PixelFormat GetProphecyPixelFormat (TextureFormat format); + + +// Inside image, leaves sizeX x sizeY portion untouched, fills the rest by +// repeating border pixels. +void PadImageBorder( ImageReference& image, int sizeX, int sizeY ); + +// Copies source compressed image into (possibly larger) destination compressed image. +void BlitCopyCompressedImage( TextureFormat format, const UInt8* src, int srcWidth, int srcHeight, UInt8* dst, int dstWidth, int dstHeight, bool fillRest ); +void BlitCopyCompressedDXT1ToDXT5( const UInt8* src, int srcWidth, int srcHeight, UInt8* dst, int dstWidth, int dstHeight ); + +// Generates a mipmap chain +void CreateMipMap (UInt8* inData, int width, int height, int depth, TextureFormat format); + +// Calculate the amount of mipmaps that can be created from an image of given size. +// (The original image is counted as mipmap also) +int CalculateMipMapCount3D (int width, int height, int depth); + +int CalculateImageSize (int width, int height, TextureFormat format); +int CalculateImageMipMapSize (int width, int height, TextureFormat format); +int CalculateMipMapOffset (int width, int height, TextureFormat format, int miplevel); + +void Premultiply( ImageReference& image ); + +// Range of RGBM lightmaps is [0;8] +enum { kRGBMMaxRange = 8 }; +void DecodeRGBM (int width, int height, UInt8* data, int pitch, const prcore::PixelFormat& pf); +void SetAlphaChannel (int width, int height, UInt8* data, int pitch, const prcore::PixelFormat& pf, UInt8 alpha); +void SetAlphaToRedChannel (int width, int height, UInt8* data, int pitch, const prcore::PixelFormat& pf); +void XenonToNormalSRGBTexture (int width, int height, UInt8* data, int pitch, const prcore::PixelFormat& pf); + +#if UNITY_XENON +enum { kImageDataAlignment = 4096 }; +#else +enum { kImageDataAlignment = kDefaultMemoryAlignment }; +#endif + +class ImageReference +{ +protected: + UInt32 m_Format; + SInt32 m_Width; + SInt32 m_Height; + SInt32 m_RowBytes; + UInt8* m_Image; + +public: + + enum ClearMode + { + CLEAR_COLOR = 1, + CLEAR_ALPHA = 2, + CLEAR_COLOR_ALPHA = CLEAR_COLOR | CLEAR_ALPHA + }; + + enum BlitMode + { + BLIT_COPY, + BLIT_SCALE, + BLIT_BILINEAR_SCALE, + }; + + ImageReference () { m_Image = NULL; m_Width = 0; m_Height = 0; m_Format = 0; } + ImageReference (int width, int height, int rowbytes, TextureFormat format, void* image); + ImageReference (int width, int height, TextureFormat format); + + // Returns true if the image is exactly the same by camping width, height, and image data + friend bool operator == (const ImageReference& lhs, const ImageReference& rhs); + + // Returns a subpart of the image + ImageReference ClipImage (int x, int y, int width, int height) const; + + UInt8* GetImageData () const { return m_Image; } + int GetRowBytes () const { return m_RowBytes; } + UInt8* GetRowPtr (int y) const { DebugAssertIf(y >= m_Height || y < 0); return m_Image + m_RowBytes * y; } + int GetWidth () const { return m_Width; } + int GetHeight () const { return m_Height; } + TextureFormat GetFormat() const { return (TextureFormat)m_Format; } + + void BlitImage (const ImageReference& source, BlitMode mode = BLIT_COPY); + void BlitImage (int x, int y, const ImageReference& source); + + void ClearImage (const ColorRGBA32& color, ClearMode mode = CLEAR_COLOR_ALPHA); + void FlipImageY (); + #if UNITY_EDITOR + void FlipImageX (); + #endif + + bool IsValidImage () const; + + bool NeedsReformat(int width, int height, TextureFormat format) const; +}; + +class Image : public ImageReference +{ +public: + + DECLARE_SERIALIZE (Image) + + Image (int width, int height, TextureFormat format); + Image (int width, int height, int rowbytes, TextureFormat format, void* image); + Image () { } + + // TODO : should be removed, because they implicitly have worse performance - use SetImage instead + Image (const Image& image) : ImageReference() { SetImage (image); } + Image (const ImageReference& image) { SetImage (image); } + + // TODO : should be removed, because they implicitly have worse performance - use SetImage instead + void operator = (const ImageReference& image) { SetImage (image); } + void operator = (const Image& image) { SetImage (image); } + + ~Image () { UNITY_FREE(kMemNewDelete, m_Image); } + + // Reformats given image into itself + void ReformatImage (const ImageReference& image, int width, int height, TextureFormat format, BlitMode mode = BLIT_COPY); + // Reformats itself + void ReformatImage (int width, int height, TextureFormat format, BlitMode mode = BLIT_COPY); + + // To initialize an image use: someImage = ImageReference (width, height, format); + + // shrinkAllowed - controls if memory should be reallocated when Image needs less memory, + // if shrinkAllowed is set to false it won't reallocate memory if Image needs less memory + void SetImage(const ImageReference& src, bool shrinkAllowed = true); + void SetImage(SInt32 width, SInt32 height, UInt32 format, bool shrinkAllowed); +}; + +bool CheckImageFormatValid (int width, int height, TextureFormat format); + + +extern const char* kUnsupportedGetPixelOpFormatMessage; +extern const char* kUnsupportedSetPixelOpFormatMessage; +void SetImagePixel (ImageReference& image, int x, int y, TextureWrapMode wrap, const ColorRGBAf& color); +ColorRGBA32 GetImagePixel (UInt8* data, int width, int height, TextureFormat format, TextureWrapMode wrap, int x, int y); +ColorRGBAf GetImagePixelBilinear (UInt8* data, int width, int height, TextureFormat format, TextureWrapMode wrap, float u, float v); +bool GetImagePixelBlock (UInt8* data, int dataWidth, int dataHeight, TextureFormat format, int x, int y, int blockWidth, int blockHeight, ColorRGBAf* outColors); +bool SetImagePixelBlock (UInt8* data, int dataWidth, int dataHeight, TextureFormat format, int x, int y, int blockWidth, int blockHeight, int pixelCount, const ColorRGBAf* pixels); + +void SwizzleARGB32ToBGRA32 (UInt8* bytes, int imageSize); +void SwizzleRGBA32ToBGRA32 (UInt8* bytes, int imageSize); +void SwizzleRGB24ToBGR24 (UInt8* bytes, int imageSize); +void SwizzleBGRAToRGBA32 (UInt8* bytes, int imageSize); + +template<class TransferFunction> inline +void Image::Transfer (TransferFunction& transfer) +{ + TRANSFER (m_Format); + TRANSFER (m_Width); + TRANSFER (m_Height); + TRANSFER (m_RowBytes); + + unsigned completeImageSize = m_RowBytes * std::max<int>(m_Height, 0); + + transfer.TransferTypeless (&completeImageSize, "image data"); + if (transfer.IsReading ()) + { + UNITY_FREE(kMemNewDelete, m_Image); + m_Image = NULL; + if (completeImageSize != 0 && CheckImageFormatValid (m_Width, m_Height, m_Format)) + m_Image = (UInt8*)UNITY_MALLOC_ALIGNED(kMemNewDelete, completeImageSize, kImageDataAlignment); + } + + transfer.TransferTypelessData (completeImageSize, m_Image); +} + +#endif diff --git a/Runtime/Graphics/ImageConversion.cpp b/Runtime/Graphics/ImageConversion.cpp new file mode 100644 index 0000000..fe0b462 --- /dev/null +++ b/Runtime/Graphics/ImageConversion.cpp @@ -0,0 +1,621 @@ +#include "UnityPrefix.h" +#include "ImageConversion.h" +#include "Runtime/Utilities/File.h" +#include "Texture2D.h" +#include "Image.h" +#include "Runtime/Utilities/BitUtility.h" +#include "Runtime/Math/Color.h" +#include "DXTCompression.h" +#if ENABLE_PNG_JPG +#include "External/ProphecySDK/src/extlib/pnglib/png.h" +#include "External/ProphecySDK/src/extlib/jpglib/jpeglib.h" +#include "Runtime/Export/JPEGMemsrc.h" +#endif +#include "Runtime/Shaders/GraphicsCaps.h" +#include "Runtime/GfxDevice/GfxDeviceConfigure.h" +#include <setjmp.h> +#include "Runtime/Network/PlayerCommunicator/PlayerConnection.h" + +using namespace std; + +// -------------------------------------------------------------------------- +// PNG + +#if ENABLE_PNG_JPG +static void PngWriteToMemoryFunc( png_structp png, png_bytep data, png_size_t size ) +{ + MemoryBuffer* buffer = (MemoryBuffer*)png->io_ptr; + buffer->insert( buffer->end(), data, data+size ); +} +static void PngWriteFlushFunc( png_structp png ) +{ +} + + +bool ConvertImageToPNGBuffer( const ImageReference& inputImage, MemoryBuffer& buffer ) +{ + png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING,(png_voidp)NULL, NULL, NULL); + if (!png_ptr) + return false; + png_infop info_ptr = png_create_info_struct(png_ptr); + if (!info_ptr) + return false; + + if (setjmp(png_jmpbuf(png_ptr))) + return false; + + buffer.reserve( 4096 ); + png_set_write_fn( png_ptr, &buffer, PngWriteToMemoryFunc, PngWriteFlushFunc ); + + png_set_compression_level(png_ptr, Z_BEST_SPEED); + + int format = kTexFormatRGBA32; + if (inputImage.GetFormat() == kTexFormatRGB24 || inputImage.GetFormat() == kTexFormatRGB565) + format = kTexFormatRGB24; + + Image image( inputImage.GetWidth(), inputImage.GetHeight(), format ); + image.BlitImage( inputImage ); + + png_set_IHDR(png_ptr, info_ptr, + inputImage.GetWidth(), + inputImage.GetHeight(), + 8, + format == kTexFormatRGB24 ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_RGB_ALPHA, + PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_DEFAULT, + PNG_FILTER_TYPE_DEFAULT); + + png_write_info(png_ptr, info_ptr); + for (int i = 0; i < image.GetHeight(); i++) + png_write_row(png_ptr, image.GetRowPtr(image.GetHeight() - i - 1)); + + png_write_end(png_ptr, info_ptr); + + png_destroy_write_struct(&png_ptr, &info_ptr); + + return !buffer.empty(); +} + + +#if CAPTURE_SCREENSHOT_AVAILABLE + +bool ConvertImageToPNGFile (const ImageReference& inputImage, const string& path) +{ + MemoryBuffer buffer; + if( !ConvertImageToPNGBuffer( inputImage, buffer ) ) + return false; + + #if UNITY_PEPPER + // no file access in pepper. write screenshot to stdout. + // we need to write hex, as string reading in mono may perform + // string encoding translation on stdout, breaking the binary data. + printf_console("SCREENSHOT_MARKER=%sSCREENSHOTSTART_MARKER", path.c_str()); + for (int i=0; i<buffer.size(); i++) + printf_console("%02x", buffer[i]); + printf_console("SCREENSHOTEND_MARKER\n"); + return true; + #else +#if ENABLE_PLAYERCONNECTION + TransferFileOverPlayerConnection(path, &buffer[0], buffer.size()); +#endif + return WriteBytesToFile (&buffer[0], buffer.size(), path); + #endif +} + +#endif + +struct PngMemoryReadContext { + const unsigned char* inputPointer; + size_t inputSizeLeft; +}; + +static void PngReadFromMemoryFunc( png_structp png_ptr, png_bytep data, png_size_t len ) +{ + PngMemoryReadContext* context = (PngMemoryReadContext*)png_ptr->io_ptr; + // check for overflow + if( len > context->inputSizeLeft ) + len = context->inputSizeLeft; + + memcpy( data, context->inputPointer, len ); + context->inputPointer += len; + context->inputSizeLeft -= len; +} + + +static void PngReadWarningFunc( png_struct* png_ptr, png_const_charp warning_msg ) +{ +} + +bool ImageSizeBoundCheck(unsigned int width, unsigned int height) { + int tmp, tmp2; + if (width + 3 < width || + height + 3 < height) { + return 0; + } + + tmp = width * height; + if (width != 0 && height != tmp / width) { + return 0; + } + + tmp2 = tmp * 16; + if (tmp != tmp2/16) { + return 0; + } + return 1; +} + +static bool LoadPngIntoTexture( Texture2D& texture, const void* data, size_t size, bool compressTexture, UInt8** outRGBABaseLevelForDXTMips ) +{ + PngMemoryReadContext context; + context.inputPointer = static_cast<const unsigned char*>( data ); + context.inputSizeLeft = size; + + if( !data ) + return false; + + // check png header + if( size < 8 || !png_check_sig( const_cast<unsigned char*>(context.inputPointer), 8 ) ) + return false; + + png_structp png_ptr; + png_infop info_ptr; + png_uint_32 width, height; + int bit_depth, color_type, interlace_type; + + double image_gamma = 0.45; + int number_passes = 0; + + png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING,NULL,NULL,&PngReadWarningFunc); + if( png_ptr == NULL ) + return false; + + info_ptr = png_create_info_struct(png_ptr); + if( info_ptr == NULL ) + { + png_destroy_read_struct(&png_ptr,(png_infopp)NULL,(png_infopp)NULL); + return false; + } + + if( setjmp(png_ptr->jmpbuf) ) + { + png_destroy_read_struct(&png_ptr,&info_ptr,(png_infopp)NULL); + return false; + } + + png_set_read_fn( png_ptr, &context, &PngReadFromMemoryFunc ); + png_read_info( png_ptr, info_ptr ); + + png_get_IHDR( png_ptr, info_ptr, &width, &height, &bit_depth, &color_type, &interlace_type, NULL, NULL ); + + png_set_strip_16(png_ptr); // strip 16 bit channels to 8 bit + png_set_packing(png_ptr); // separate palettized channels + + // palette -> rgb + if( color_type == PNG_COLOR_TYPE_PALETTE ) + { + png_set_expand(png_ptr); + } + + // grayscale -> 8 bits + if( !(color_type & PNG_COLOR_MASK_COLOR) && bit_depth < 8 ) png_set_expand(png_ptr); + + // if exists, expand tRNS to alpha channel + if( png_get_valid(png_ptr,info_ptr,PNG_INFO_tRNS) ) png_set_expand(png_ptr); + + // expand gray to RGB + if( color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA ) + png_set_gray_to_rgb(png_ptr); + + // we need to get ARGB format for raw textures, and RGBA if we will compress it + if( !compressTexture ) + png_set_swap_alpha(png_ptr); + + png_set_filler( png_ptr, 0xFF, PNG_FILLER_BEFORE ); // force alpha byte + + // Only apply gamma correction if the image has gamma information. In this case + // assume our display gamma is 2.0 (a compromise between Mac and PC...). + double screen_gamma = 2.0f; + image_gamma = 0.0; + if ( png_get_gAMA(png_ptr,info_ptr,&image_gamma) ) + { + png_set_gamma(png_ptr,screen_gamma,image_gamma); + } + + number_passes = png_set_interlace_handling(png_ptr); + png_read_update_info(png_ptr,info_ptr); // update gamma, etc. + + // If texture size or format differs, reformat the texture. + TextureFormat wantedFormat = compressTexture ? kTexFormatDXT5 : kTexFormatARGB32; + bool mipMaps = texture.HasMipMap(); + if( width != texture.GetDataWidth () || height != texture.GetDataHeight () || kTexFormatARGB32 != texture.GetTextureFormat()) + texture.InitTexture( width, height, wantedFormat, mipMaps ? Texture2D::kMipmapMask : Texture2D::kNoMipmap, 1 ); + + ImageReference ref; + AssertIf( !outRGBABaseLevelForDXTMips ); + if( compressTexture ) { + int imageByteSize; + if (mipMaps) { + int miplevel = CalculateMipMapCount3D(width, height, 1); + int iter; + unsigned int completeSize = 0; + unsigned int prevcompleteSize = 0; + + if (!ImageSizeBoundCheck(width, height)) { + longjmp(png_ptr->jmpbuf, 1); + } + + for (iter = 0; iter < miplevel; iter++) { + prevcompleteSize = completeSize; + completeSize += CalculateImageSize (std::max (width >> iter, (png_uint_32)1), std::max (height >> iter, (png_uint_32)1), kTexFormatRGBA32); + if (completeSize < prevcompleteSize) { + longjmp(png_ptr->jmpbuf, 1); + } + } + imageByteSize = CalculateImageMipMapSize( width, height, kTexFormatRGBA32 ); + } else { + if (!ImageSizeBoundCheck(width, height)) { + longjmp(png_ptr->jmpbuf, 1); + } + imageByteSize = CalculateImageSize( width, height, kTexFormatRGBA32 ); + } + + *outRGBABaseLevelForDXTMips = new UInt8[imageByteSize]; + ref = ImageReference( width, height, width*4, kTexFormatRGBA32, *outRGBABaseLevelForDXTMips ); + } else { + if( !texture.GetWriteImageReference(&ref, 0, 0) ) { + png_destroy_read_struct( &png_ptr,&info_ptr,(png_infopp)NULL ); + return false; + } + } + + size_t len = sizeof(png_bytep) * height; + + /* boundcheck for integer overflow */ + if (height != len / sizeof(png_bytep) ) { + png_destroy_read_struct( &png_ptr,&info_ptr,(png_infopp)NULL ); + return false; + } + + png_bytep* row_pointers = new png_bytep[height]; + for( png_uint_32 row = 0; row<height; ++row ) + { + row_pointers[row] = ref.GetRowPtr(height-1-row); + } + + for( int pass = 0; pass < number_passes; pass++ ) + { + png_read_rows( png_ptr,row_pointers,NULL,height ); + } + + // cleanup + png_read_end( png_ptr,info_ptr ); + png_destroy_read_struct( &png_ptr,&info_ptr,(png_infopp)NULL ); + delete[] row_pointers; + + return true; +} + +// -------------------------------------------------------------------------- +// JPG + +/* Custom error manager. As the default one in libjpeg crashes Unity on error */ +struct unity_jpeg_error_mgr { + struct jpeg_error_mgr pub; /* "public" fields */ + jmp_buf setjmp_buffer; /* for return to caller */ +}; +typedef struct unity_jpeg_error_mgr * unity_jpeg_error_ptr; + +/* + * Here's the routine that will replace the standard error_exit method: + */ +METHODDEF(void) +unity_jpeg_error_exit (j_common_ptr cinfo) +{ + /* cinfo->err really points to a unity_jpeg_error_mgr struct, so coerce pointer */ + unity_jpeg_error_ptr myerr = (unity_jpeg_error_ptr) cinfo->err; + /* Always display the message. */ + /* We could postpone this until after returning, if we chose. */ + (*cinfo->err->output_message) (cinfo); + /* Return control to the setjmp point */ + longjmp(myerr->setjmp_buffer, 1); +} + +static void HandleError (struct jpeg_decompress_struct& cinfo, Texture2D* tex) +{ + jpeg_destroy_decompress (&cinfo); +} + +static int LoadJpegIntoTexture(Texture2D& tex, const UInt8* jpegData, size_t jpegDataSz, bool compressTexture, UInt8** outRGBABaseLevelForDXTMips) +{ +#if UNITY_WII + AssertIf ("ERROR: LoadJpegIntoTexture is not supported!"); + return 0; +#else + struct jpeg_decompress_struct cinfo; + + /* We use our private extension JPEG error handler. + * Note that this struct must live as long as the main JPEG parameter + * struct, to avoid dangling-pointer problems. + */ + struct unity_jpeg_error_mgr jerr; + + JSAMPARRAY in; + int row_stride; + unsigned char *out; + + // set up the decompression. + cinfo.err = jpeg_std_error(&jerr.pub); + jerr.pub.error_exit = unity_jpeg_error_exit; + + /* Establish the setjmp return context for my_error_exit to use. */ + if (setjmp(jerr.setjmp_buffer)) { + /* If we get here, the JPEG code has signaled an error. + * We need to clean up the JPEG object, close the input file, and return. + */ + HandleError(cinfo, &tex); + return 0; + } + + jpeg_create_decompress (&cinfo); + + // inititalize the source + jpeg_memory_src (&cinfo, (unsigned char*)jpegData, jpegDataSz); + + // initialize decompression + (void) jpeg_read_header (&cinfo, TRUE); + (void) jpeg_start_decompress (&cinfo); + + // set up the width and height for return + int width = cinfo.image_width; + int height = cinfo.image_height; + + // initialize the input buffer - we'll use the in-built memory management routines in the + // JPEG library because it will automatically free the used memory for us when we destroy + // the decompression structure. cool. + row_stride = cinfo.output_width * cinfo.output_components; + if (cinfo.output_width != 0 && cinfo.output_components != row_stride / cinfo.output_width) { + HandleError(cinfo, &tex); + return 0; + } + in = (*cinfo.mem->alloc_sarray) ((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1); + + // we only support three channel RGB or one channel grayscale formats + + if (cinfo.output_components != 3 && cinfo.output_components != 1) + { + HandleError(cinfo, &tex); + return 0; + } + + // If texture size or format differs, reformat the texture. + TextureFormat wantedFormat = compressTexture ? kTexFormatDXT1 : kTexFormatRGB24; + bool mipMaps = tex.HasMipMap(); + if (width != tex.GetDataWidth () || height != tex.GetDataHeight () || wantedFormat != tex.GetTextureFormat()) + tex.InitTexture (width, height, wantedFormat, mipMaps ? Texture2D::kMipmapMask : Texture2D::kNoMipmap, 1); + + ImageReference ref; + AssertIf( !outRGBABaseLevelForDXTMips ); + if( compressTexture ) { + int imageByteSize; + if (mipMaps) { + int miplevel = CalculateMipMapCount3D(width, height, 1); + int iter; + unsigned int completeSize = 0; + unsigned int prevcompleteSize = 0; + + if (!ImageSizeBoundCheck(width, height)) { + HandleError(cinfo, &tex); + return 0; + } + + for (iter = 0; iter < miplevel; iter++) { + prevcompleteSize = completeSize; + completeSize += CalculateImageSize (std::max (width >> iter, 1), std::max (height >> iter, 1), kTexFormatRGBA32); + if (completeSize < prevcompleteSize) { + HandleError(cinfo, &tex); + return 0; + } + } + imageByteSize = CalculateImageMipMapSize( width, height, kTexFormatRGBA32 ); + } else { + if (!ImageSizeBoundCheck(width, height)) { + HandleError(cinfo, &tex); + return 0; + } + imageByteSize = CalculateImageSize( width, height, kTexFormatRGBA32 ); + } + *outRGBABaseLevelForDXTMips = new UInt8[imageByteSize]; + ref = ImageReference( width, height, width*4, kTexFormatRGBA32, *outRGBABaseLevelForDXTMips ); + } else { + if( !tex.GetWriteImageReference(&ref, 0, 0) ) { + jpeg_destroy_decompress (&cinfo); + return 0; + } + } + + UInt8* dst; + int scanline = height-1; + int result = 1; + + if (scanline < 0) { + HandleError(cinfo, &tex); + return 0; + } + + // three channel output + if( cinfo.output_components == 3 ) + { + while (cinfo.output_scanline < cinfo.output_height) + { + jpeg_read_scanlines(&cinfo, in, 1); + dst = ref.GetRowPtr(scanline); + out = in[0]; + if( !compressTexture ) { + for( int i = 0; i < row_stride; i += 3, dst += 3 ) + { + dst[0] = out[i+2]; + dst[1] = out[i+1]; + dst[2] = out[i]; + } + } else { + for( int i = 0; i < row_stride; i += 3, dst += 4 ) + { + dst[0] = out[i+2]; + dst[1] = out[i+1]; + dst[2] = out[i]; + dst[3] = 255; + } + } + --scanline; + } + } + // one channel output + else if( cinfo.output_components == 1 ) + { + while (cinfo.output_scanline < cinfo.output_height) + { + jpeg_read_scanlines(&cinfo, in, 1); + dst = ref.GetRowPtr(scanline); + out = in[0]; + if( !compressTexture ) { + for( int i = 0; i < row_stride; ++i, dst += 3 ) + { + UInt8 v = out[i]; + dst[0] = v; + dst[1] = v; + dst[2] = v; + } + } else { + for( int i = 0; i < row_stride; ++i, dst += 4 ) + { + UInt8 v = out[i]; + dst[0] = v; + dst[1] = v; + dst[2] = v; + dst[4] = 255; + } + } + --scanline; + } + } + else + { + AssertString("unsupported number of output components in JPEG"); + result = 0; + } + + // finish decompression and destroy the jpeg + (void) jpeg_finish_decompress (&cinfo); + jpeg_destroy_decompress (&cinfo); + + + return result; +#endif +} + +#endif + +// -------------------------------------------------------------------------- +// Generic byte->texture loading + + +// Simple image to load into the texture on error: "?" +static const char* kDummyErrorImage = +"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff" +"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff" +"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff" +"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\x00\x00\xff\xff\xff\xff\xff\xff" +"\xff\xff\xff\xff\x00\x00\xff\x00\x00\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\x00\x00\xff\xff\xff" +"\xff\xff\xff\xff\x00\x00\xff\x00\x00\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\x00\x00\xff\xff\xff" +"\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\x00\x00\xff\x00\x00\xff\x00\x00\xff\xff\xff\xff\xff\xff"; + +#if UNITY_FLASH +//Leaky temporary workaround to deal with not having setjmp/longjmp on Flash. This is only to be able to return correctly, but doesn't clean up anything...beware! + +__attribute__ ((noinline)) static int LoadJpegIntoTextureFlash(Texture2D& tex, const UInt8* jpegData, size_t jpegDataSz, bool compressTexture, UInt8** outRGBABaseLevelForDXTMips) +{ + __asm __volatile__("try{"); + int val = LoadJpegIntoTexture(tex,jpegData,jpegDataSz,compressTexture,outRGBABaseLevelForDXTMips); + __asm __volatile__("}catch(e:Error){r_g0 = 0; return;}"); + return val; +} + +__attribute__ ((noinline)) static bool LoadPngIntoTextureFlash( Texture2D& texture, const void* data, size_t size, bool compressTexture, UInt8** outRGBABaseLevelForDXTMips ) +{ + __asm __volatile__("try{"); + bool val = LoadPngIntoTexture(texture,data,size,compressTexture,outRGBABaseLevelForDXTMips); + __asm __volatile__("}catch(e:Error){r_g0 = 0; return;}"); + return val; +} + +#define LoadJpegIntoTexture LoadJpegIntoTextureFlash +#define LoadPngIntoTexture LoadPngIntoTextureFlash +#endif + +bool LoadMemoryBufferIntoTexture( Texture2D& tex, const UInt8* data, size_t size, LoadImageCompression compression, bool markNonReadable ) +{ + UInt8* rgbaBaseLevel = NULL; + if( !gGraphicsCaps.hasS3TCCompression ) + compression = kLoadImageUncompressed; + +#if ENABLE_PNG_JPG + if( !LoadJpegIntoTexture( tex, data, size, compression!=kLoadImageUncompressed, &rgbaBaseLevel ) ) + { + delete[] rgbaBaseLevel; rgbaBaseLevel = NULL; + if( !LoadPngIntoTexture( tex, data, size, compression!=kLoadImageUncompressed, &rgbaBaseLevel ) ) + { + // Store a dummy image into the texture + tex.InitTexture( 8, 8, kTexFormatRGB24, Texture2D::kNoMipmap, 1 ); + unsigned char* textureData = (unsigned char*)tex.GetRawImageData(); + memcpy( textureData, kDummyErrorImage, 8*8*3 ); + } + } +#endif + // Compress the image if needed + bool isDXTCompressed = IsCompressedDXTTextureFormat(tex.GetTextureFormat()); + if( isDXTCompressed ) { + #if !GFX_SUPPORTS_DXT_COMPRESSION + delete[] rgbaBaseLevel; + return false; + #else + + AssertIf( !rgbaBaseLevel ); + // do final image compression + int width = tex.GetDataWidth(); + int height = tex.GetDataHeight(); + TextureFormat texFormat = tex.GetTextureFormat(); + AssertIf( texFormat != kTexFormatDXT1 && texFormat != kTexFormatDXT5 ); + FastCompressImage( width, height, rgbaBaseLevel, tex.GetRawImageData(), texFormat == kTexFormatDXT5, compression==kLoadImageDXTCompressDithered ); + + bool mipMaps = tex.HasMipMap(); + if( mipMaps ) { + // compute mip representations from the base level + CreateMipMap (rgbaBaseLevel, width, height, 1, kTexFormatRGBA32); + // DXT compress them + int mipCount = tex.CountDataMipmaps(); + for( int mip = 1; mip < mipCount; ++mip ) + { + const UInt8* srcMipData = rgbaBaseLevel + CalculateMipMapOffset(width, height, kTexFormatRGBA32, mip); + UInt8* dstMipData = tex.GetRawImageData() + CalculateMipMapOffset(width, height, texFormat, mip); + FastCompressImage( std::max(width>>mip,1), std::max(height>>mip,1), srcMipData, dstMipData, texFormat == kTexFormatDXT5, compression==kLoadImageDXTCompressDithered ); + } + } + #endif // GFX_SUPPORTS_DXT_COMPRESSION + } + delete[] rgbaBaseLevel; + + if(markNonReadable) + { + tex.SetIsReadable(false); + tex.SetIsUnreloadable(true); + } + + if( !IsCompressedDXTTextureFormat(tex.GetTextureFormat()) ) + tex.UpdateImageData(); + else + tex.UpdateImageDataDontTouchMipmap(); + + return true; +} diff --git a/Runtime/Graphics/ImageConversion.h b/Runtime/Graphics/ImageConversion.h new file mode 100644 index 0000000..31ed2e0 --- /dev/null +++ b/Runtime/Graphics/ImageConversion.h @@ -0,0 +1,28 @@ +#ifndef _IMAGECONVERSION_H +#define _IMAGECONVERSION_H + +#include "Runtime/Utilities/dynamic_array.h" + +class ImageReference; +class Texture2D; +typedef dynamic_array<UInt8> MemoryBuffer; + +bool ConvertImageToPNGBuffer( const ImageReference& image, MemoryBuffer& buffer ); + +enum LoadImageCompression { + kLoadImageUncompressed = 0, + kLoadImageDXTCompress, + kLoadImageDXTCompressDithered, +}; +bool LoadMemoryBufferIntoTexture( Texture2D& tex, const UInt8* data, size_t size, LoadImageCompression compression, bool markNonReadable=false ); + + +// hack: this is used only by capture screenshot. Compile code out if it's not +// available +#include "Runtime/Misc/CaptureScreenshot.h" +#if CAPTURE_SCREENSHOT_AVAILABLE +bool ConvertImageToPNGFile( const ImageReference& image, const std::string& path ); +#endif + + +#endif diff --git a/Runtime/Graphics/LightProbeGroup.cpp b/Runtime/Graphics/LightProbeGroup.cpp new file mode 100644 index 0000000..1b19bcb --- /dev/null +++ b/Runtime/Graphics/LightProbeGroup.cpp @@ -0,0 +1,50 @@ +#include "UnityPrefix.h" +#include "LightProbeGroup.h" +#include "Configuration/UnityConfigure.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" + +using namespace std; + +#if UNITY_EDITOR +static LightProbeGroupList gAllLightProbeGroups; +#endif + +LightProbeGroup::LightProbeGroup (MemLabelId label, ObjectCreationMode mode) +: Super(label, mode) +#if UNITY_EDITOR + , m_LightProbeGroupNode (this) +#endif +{ +} + +LightProbeGroup::~LightProbeGroup () +{ +} + +IMPLEMENT_CLASS (LightProbeGroup) +IMPLEMENT_OBJECT_SERIALIZE (LightProbeGroup) + +template<class T> inline +void LightProbeGroup::Transfer (T& transfer) +{ + Super::Transfer (transfer); + + TRANSFER_EDITOR_ONLY (m_SourcePositions); +} + +#if UNITY_EDITOR +void LightProbeGroup::AddToManager () +{ + gAllLightProbeGroups.push_back (m_LightProbeGroupNode); +} + +void LightProbeGroup::RemoveFromManager () +{ + m_LightProbeGroupNode.RemoveFromList (); +} + +LightProbeGroupList& GetLightProbeGroups () +{ + return gAllLightProbeGroups; +} +#endif diff --git a/Runtime/Graphics/LightProbeGroup.h b/Runtime/Graphics/LightProbeGroup.h new file mode 100644 index 0000000..3ca61a3 --- /dev/null +++ b/Runtime/Graphics/LightProbeGroup.h @@ -0,0 +1,41 @@ +#ifndef LIGHTPROBEGROUP_H +#define LIGHTPROBEGROUP_H + +#include "Runtime/GameCode/Behaviour.h" +#include "Runtime/Math/Vector3.h" +#include "Runtime/Utilities/LinkedList.h" +#include "Runtime/Utilities/dynamic_array.h" + +class LightProbeGroup : public Behaviour +{ +public: + REGISTER_DERIVED_CLASS (LightProbeGroup, Behaviour) + DECLARE_OBJECT_SERIALIZE (LightProbeGroup) + + LightProbeGroup (MemLabelId label, ObjectCreationMode mode); + +#if UNITY_EDITOR + void SetPositions(Vector3f* data, int size) { m_SourcePositions.assign(data, data + size); SetDirty(); } + Vector3f* GetPositions() { return m_SourcePositions.size() > 0 ? &m_SourcePositions[0] : NULL; } + int GetPositionsSize() { return m_SourcePositions.size(); } + + virtual void AddToManager (); + virtual void RemoveFromManager (); +#else + virtual void AddToManager () {} + virtual void RemoveFromManager () {} +#endif + +private: +#if UNITY_EDITOR + dynamic_array<Vector3f> m_SourcePositions; + ListNode<LightProbeGroup> m_LightProbeGroupNode; +#endif +}; + +#if UNITY_EDITOR +typedef List< ListNode<LightProbeGroup> > LightProbeGroupList; +LightProbeGroupList& GetLightProbeGroups (); +#endif + +#endif diff --git a/Runtime/Graphics/LightmapSettings.cpp b/Runtime/Graphics/LightmapSettings.cpp new file mode 100644 index 0000000..a2ff547 --- /dev/null +++ b/Runtime/Graphics/LightmapSettings.cpp @@ -0,0 +1,153 @@ +#include "UnityPrefix.h" +#include "LightmapSettings.h" +#include "Runtime/BaseClasses/ManagerContext.h" +#include "Runtime/Misc/BuildSettings.h" +#include "Runtime/Scripting/ScriptingUtility.h" +#include "Runtime/Scripting/Scripting.h" + +const char* const LightmapSettings::kLightmapsModeNames[] = +{ + "Single Lightmaps", + "Dual Lightmaps", + "Directional Lightmaps", + "RNM" +}; + +template<class T> +void LightmapData::Transfer (T& transfer) +{ + TRANSFER(m_Lightmap); + TRANSFER(m_IndirectLightmap); +} + +template<class T> +void LightmapSettings::Transfer (T& transfer) +{ + Super::Transfer(transfer); + TRANSFER(m_LightProbes); + TRANSFER(m_Lightmaps); + TRANSFER(m_LightmapsMode); + TRANSFER(m_BakedColorSpace); + TRANSFER(m_UseDualLightmapsInForward); + transfer.Align (); + TRANSFER_EDITOR_ONLY(m_LightmapEditorSettings); +} + +LightmapSettings::LightmapSettings(MemLabelId label, ObjectCreationMode mode) +: Super(label, mode) +, m_LightmapTextures(NULL) +, m_LightmapTextureCount(0) +, m_LightmapsMode(kDualLightmapsMode) +, m_UseDualLightmapsInForward(false) +, m_BakedColorSpace(0) +{ +} + +LightmapSettings::~LightmapSettings () +{ + delete[] m_LightmapTextures; +} + +void LightmapSettings::AwakeFromLoad(AwakeFromLoadMode awakeMode) +{ + Super::AwakeFromLoad(awakeMode); + Rebuild (); +} + +void LightmapSettings::Rebuild () +{ + delete[] m_LightmapTextures; + + const size_t lightmapCount = m_Lightmaps.size(); + + m_LightmapTextures = new TextureTriple[lightmapCount]; + m_LightmapTextureCount = lightmapCount; + + for (size_t i = 0; i < lightmapCount; ++i) + { + Texture2D* tex = m_Lightmaps[i].m_Lightmap; + Texture2D* texInd = m_Lightmaps[i].m_IndirectLightmap; + Texture2D* texThird = m_Lightmaps[i].m_ThirdLightmap; + TextureTriple p; + p.first = tex ? tex->GetTextureID() : TextureID(); + p.second = texInd ? texInd->GetTextureID() : TextureID(); + p.third = texThird ? texThird->GetTextureID() : TextureID(); + m_LightmapTextures[i] = p; + } +} + +void LightmapSettings::ClearLightmaps() +{ + m_Lightmaps.clear(); + Rebuild(); + SetDirty(); +} + +void LightmapSettings::SetLightmaps (const std::vector<LightmapData>& data) +{ + m_Lightmaps = data; + Rebuild(); + SetDirty(); +} + +void LightmapSettings::SetLightProbes (LightProbes* lightProbes) +{ + if (lightProbes && !GetBuildSettings().hasAdvancedVersion) + { + ErrorString ("Light probes require Unity Pro."); + return; + } + + m_LightProbes = lightProbes; + SetDirty(); +} + +LightProbes* LightmapSettings::GetLightProbes () +{ + return m_LightProbes; +} + +bool LightmapSettings::GetUseDualLightmapsInForward () const +{ + if (IS_CONTENT_NEWER_OR_SAME (kUnityVersion3_5_a1)) + return (m_LightmapsMode == kDualLightmapsMode) && m_UseDualLightmapsInForward; + else + return m_UseDualLightmapsInForward; +} + +void LightmapSettings::AppendLightmaps (const std::vector<LightmapData>& data) +{ + int originalSize = m_Lightmaps.size(); + int dataSize = data.size(); + + if (originalSize + data.size() > LightmapSettings::kMaxLightmaps) + { + int newDataSize = max(0, LightmapSettings::kMaxLightmaps - originalSize); + ErrorString(Format( + "Can't append %i lightmaps, since that would exceed the %i lightmaps limit. " + "Appending only %i lightmaps. Objects that use lightmaps past that limit won't get proper lightmaps.", + dataSize, LightmapSettings::kMaxLightmaps, newDataSize)); + dataSize = newDataSize; + } + if ( dataSize <= 0 ) return; + m_Lightmaps.resize(originalSize + dataSize); + std::copy(data.begin(), data.begin() + dataSize, m_Lightmaps.begin() + originalSize); + + Rebuild(); + SetDirty(); +} + +#if ENABLE_SCRIPTING +void LightmapDataToMono (const LightmapData &src, LightmapDataMono &dest) { + dest.m_Lightmap = Scripting::ScriptingWrapperFor (src.m_Lightmap); + dest.m_IndirectLightmap = Scripting::ScriptingWrapperFor (src.m_IndirectLightmap); +} +void LightmapDataToCpp (LightmapDataMono &src, LightmapData &dest) { + dest.m_Lightmap = ScriptingObjectToObject<Texture2D> (src.m_Lightmap); + dest.m_IndirectLightmap = ScriptingObjectToObject<Texture2D> (src.m_IndirectLightmap); +} +#endif + +IMPLEMENT_CLASS (LightmapSettings) +IMPLEMENT_OBJECT_SERIALIZE (LightmapSettings) +GET_MANAGER (LightmapSettings) diff --git a/Runtime/Graphics/LightmapSettings.h b/Runtime/Graphics/LightmapSettings.h new file mode 100644 index 0000000..c8e8a65 --- /dev/null +++ b/Runtime/Graphics/LightmapSettings.h @@ -0,0 +1,101 @@ +#pragma once + +#include "Runtime/Math/Color.h" +#include "Texture2D.h" +#include "Runtime/BaseClasses/GameManager.h" +#include "Runtime/Utilities/NonCopyable.h" +#include "Runtime/Utilities/triple.h" +#if UNITY_EDITOR +#include "Editor/Src/LightmapEditorSettings.h" +#endif +#include "Runtime/Camera/LightProbes.h" + +class LightmapData +{ +public: + PPtr<Texture2D> m_Lightmap; + PPtr<Texture2D> m_IndirectLightmap; + PPtr<Texture2D> m_ThirdLightmap; + + DECLARE_SERIALIZE(LightmapData) +}; + +struct LightmapDataMono { + ScriptingObjectPtr m_Lightmap; + ScriptingObjectPtr m_IndirectLightmap; +}; +void LightmapDataToMono (const LightmapData &src, LightmapDataMono &dest); +void LightmapDataToCpp (LightmapDataMono &src, LightmapData &dest); + +class LightmapSettings : public LevelGameManager +{ +public: + REGISTER_DERIVED_CLASS (LightmapSettings, LevelGameManager) + DECLARE_OBJECT_SERIALIZE(LightmapSettings) + + // Lightmap index is stored as UInt8, + // 2 indices are reserved. + static const int kMaxLightmaps = 254; + + LightmapSettings(MemLabelId label, ObjectCreationMode mode); + + typedef triple<TextureID> TextureTriple; + TextureTriple GetLightmapTexture (UInt32 index) const + { + if (index < m_LightmapTextureCount) + return m_LightmapTextures[index]; + else + return triple<TextureID>(TextureID(),TextureID(), TextureID()); + } + + const std::vector<LightmapData>& GetLightmaps () { return m_Lightmaps; } + void ClearLightmaps(); + void SetLightmaps (const std::vector<LightmapData>& data); + void AppendLightmaps (const std::vector<LightmapData>& data); + + void SetBakedColorSpace(ColorSpace colorSpace) { m_BakedColorSpace = (int)colorSpace; } + ColorSpace GetBakedColorSpace() { return (ColorSpace)m_BakedColorSpace; } + + enum LightmapsMode { + kSingleLightmapsMode = 0, + kDualLightmapsMode = 1, + kDirectionalLightmapsMode = 2, + kRNMMode = 3, + kLightmapsModeCount // keep this last + }; + + static const char* const kLightmapsModeNames[kLightmapsModeCount]; + + GET_SET(int, LightmapsMode, m_LightmapsMode); + bool GetUseDualLightmapsInForward () const; + + #if UNITY_EDITOR + LightmapEditorSettings& GetLightmapEditorSettings() { return m_LightmapEditorSettings; }; + #endif + + void AwakeFromLoad(AwakeFromLoadMode mode); + + void SetLightProbes (LightProbes* lightProbes); + LightProbes* GetLightProbes (); + +private: + void Rebuild(); + +private: + TextureTriple* m_LightmapTextures; + int m_LightmapTextureCount; + + PPtr<LightProbes> m_LightProbes; + + std::vector<LightmapData> m_Lightmaps; + int m_LightmapsMode; // LightmapsMode + int m_BakedColorSpace; + bool m_UseDualLightmapsInForward; + + #if UNITY_EDITOR + LightmapEditorSettings m_LightmapEditorSettings; + #endif +}; + +LightmapSettings& GetLightmapSettings (); + diff --git a/Runtime/Graphics/LowerResBlitTexture.h b/Runtime/Graphics/LowerResBlitTexture.h new file mode 100644 index 0000000..b96af45 --- /dev/null +++ b/Runtime/Graphics/LowerResBlitTexture.h @@ -0,0 +1,61 @@ +#ifndef LowerResBlitTexture_h +#define LowerResBlitTexture_h + +#include "Runtime/Graphics/Texture.h" +#include "Runtime/GfxDevice/TextureIdMap.h" + +// This is used by the OSX/Linux/iOS standalones for scaling up the viewport to the screen resolution. +class LowerResBlitTexture +: public Texture +{ +public: + + LowerResBlitTexture(MemLabelId label, ObjectCreationMode mode) : Texture(label, mode) {} + + unsigned w, h; + + void Create(unsigned tex, unsigned w_, unsigned h_) + { + #if ENABLE_TEXTUREID_MAP + TextureIdMap::UpdateTexture(m_TexID, tex); + #else + m_TexID = TextureID(tex); + #endif + + + + w = w_; h = h_; + m_TexelSizeX = 1.0f / w_; + m_TexelSizeY = 1.0f / h_; + } + + virtual TextureDimension GetDimension () const { return kTexDim2D; } + virtual bool ExtractImage (ImageReference* /*image*/, int /*imageIndex*/) const { return false; } + virtual int GetStorageMemorySize() const { return 0; } + + virtual int GetDataWidth() const { return w; } + virtual int GetDataHeight() const { return h; } + + virtual bool HasMipMap () const { return false; } + virtual int CountMipmaps () const { return 1; } + + virtual void UnloadFromGfxDevice(bool forceUnloadAll) {} + virtual void UploadToGfxDevice() {} + virtual void ApplySettings () {} +}; + + +static LowerResBlitTexture* CreateBlitTexture() +{ + static LowerResBlitTexture* _BlitTex = 0; + if(_BlitTex == 0) + { + _BlitTex = CreateObjectFromCode<LowerResBlitTexture>(); + _BlitTex->SetHideFlags(Object::kHideAndDontSave); + } + + return _BlitTex; +} + + +#endif diff --git a/Runtime/Graphics/MatrixStack.cpp b/Runtime/Graphics/MatrixStack.cpp new file mode 100644 index 0000000..58e6f12 --- /dev/null +++ b/Runtime/Graphics/MatrixStack.cpp @@ -0,0 +1,72 @@ +#include "UnityPrefix.h" +#include "MatrixStack.h" +#include "Runtime/Math/Matrix4x4.h" + +void MatrixStack::Reset() +{ + m_Depth = 1; + m_Matrices[0].SetIdentity(); +} + +Matrix4x4f& MatrixStack::GetMatrix4x4f( int index ) +{ + Assert(index >= 0 && index < m_Depth); + return m_Matrices[index]; +} + +void MatrixStack::Push() +{ + if( m_Depth >= kStackDepth ) + { + ErrorString( "Matrix stack full depth reached" ); + return; + } + ++m_Depth; + CopyMatrix (m_Matrices[m_Depth-2].GetPtr(), m_Matrices[m_Depth-1].GetPtr()); +} + +void MatrixStack::Push (const float matrix[16]) +{ + if( m_Depth >= kStackDepth ) + { + ErrorString( "Matrix stack full depth reached" ); + return; + } + ++m_Depth; + CopyMatrix (matrix, m_Matrices[m_Depth-2].GetPtr()); +} + +void MatrixStack::Pop() +{ + if( m_Depth < 2 ) + { + ErrorString( "Matrix stack empty" ); + return; + } + --m_Depth; +} + +void MatrixStack::SetMatrix (const float matrix[16]) +{ + CopyMatrix (matrix, m_Matrices[m_Depth-1].GetPtr()); +} + +void MatrixStack::SetCurrentIdentity() +{ + m_Matrices[m_Depth-1].SetIdentity(); +} + +void MatrixStack::MultMatrix( const float matrix[16] ) +{ + const Matrix4x4f& a = *reinterpret_cast<const Matrix4x4f*>(matrix); + Matrix4x4f& b = GetMatrix4x4f( m_Depth-1 ); + Matrix4x4f c; + MultiplyMatrices4x4 (&b, &a, &c); + CopyMatrix (c.GetPtr(), b.GetPtr()); +} + +const Matrix4x4f& MatrixStack::GetMatrix() const +{ + Assert(m_Depth >= 1 && m_Depth <= kStackDepth); + return m_Matrices[m_Depth-1]; +} diff --git a/Runtime/Graphics/MatrixStack.h b/Runtime/Graphics/MatrixStack.h new file mode 100644 index 0000000..fe11a18 --- /dev/null +++ b/Runtime/Graphics/MatrixStack.h @@ -0,0 +1,32 @@ +#pragma once + +#include "Runtime/Math/Matrix4x4.h" + + +class MatrixStack +{ +public: + enum { kStackDepth = 16 }; + +public: + MatrixStack() { Reset(); } + + void Reset(); + + void SetMatrix( const float matrix[16] ); + void SetCurrentIdentity(); + void MultMatrix( const float matrix[16] ); + const Matrix4x4f& GetMatrix() const; + + void Push(const float* matrix); + void Push(); + void Pop(); + + int GetCurrentDepth() const { return m_Depth; } + +private: + Matrix4x4f& GetMatrix4x4f (int index); + + Matrix4x4f m_Matrices[kStackDepth]; + int m_Depth; +}; diff --git a/Runtime/Graphics/ParticleSystem/Modules/ClampVelocityModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/ClampVelocityModule.cpp new file mode 100644 index 0000000..362fbff --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/ClampVelocityModule.cpp @@ -0,0 +1,102 @@ +#include "UnityPrefix.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "ClampVelocityModule.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemUtils.h" +#include "Runtime/Misc/BuildSettings.h" + +inline float DampenOutsideLimit (float v, float limit, float dampen) +{ + float sgn = Sign (v); + float abs = Abs (v); + if (abs > limit) + abs = Lerp(abs, limit, dampen); + return abs * sgn; +} + +ClampVelocityModule::ClampVelocityModule () : ParticleSystemModule(false) +, m_SeparateAxis (false) +, m_InWorldSpace (false) +, m_Dampen (1.0f) +{ +} + +template<ParticleSystemCurveEvalMode mode> +void MagnitudeUpdateTpl(const MinMaxCurve& magnitude, ParticleSystemParticles& ps, size_t fromIndex, size_t toIndex, float dampen) +{ + for (size_t q = fromIndex; q < toIndex; ++q) + { + float limit = Evaluate<mode> (magnitude, NormalizedTime(ps, q), GenerateRandom(ps.randomSeed[q] + kParticleSystemClampVelocityCurveId)); + Vector3f vel = ps.velocity[q] + ps.animatedVelocity[q]; + vel = NormalizeSafe (vel) * DampenOutsideLimit (Magnitude (vel), limit, dampen); + ps.velocity[q] = vel - ps.animatedVelocity[q]; + } +} + +void ClampVelocityModule::Update (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex) +{ + if (m_SeparateAxis) + { + Matrix4x4f matrix; + Matrix4x4f invMatrix; + bool transform; + if(IS_CONTENT_NEWER_OR_SAME (kUnityVersion4_0_a1)) + transform = GetTransformationMatrices(matrix, invMatrix, !roState.useLocalSpace, m_InWorldSpace, state.localToWorld); + else + transform = false; // Revert to old broken behavior + + for (size_t q = fromIndex; q < toIndex; ++q) + { + Vector3f random; + GenerateRandom3(random, ps.randomSeed[q] + kParticleSystemClampVelocityCurveId); + const float time = NormalizedTime(ps, q); + + Vector3f vel = ps.velocity[q] + ps.animatedVelocity[q]; + if(transform) + vel = matrix.MultiplyVector3 (vel); + const Vector3f limit (Evaluate (m_X, time, random.x), Evaluate (m_Y, time, random.y), Evaluate (m_Z, time, random.z)); + vel.x = DampenOutsideLimit (vel.x, limit.x, m_Dampen); + vel.y = DampenOutsideLimit (vel.y, limit.y, m_Dampen); + vel.z = DampenOutsideLimit (vel.z, limit.z, m_Dampen); + vel = vel - ps.animatedVelocity[q]; + if(transform) + vel = invMatrix.MultiplyVector3 (vel); + ps.velocity[q] = vel; + } + } + else + { + if(m_Magnitude.minMaxState == kMMCScalar) + MagnitudeUpdateTpl<kEMScalar> (m_Magnitude, ps, fromIndex, toIndex, m_Dampen); + else if(m_Magnitude.IsOptimized() && m_Magnitude.UsesMinMax()) + MagnitudeUpdateTpl<kEMOptimizedMinMax> (m_Magnitude, ps, fromIndex, toIndex, m_Dampen); + else if(m_Magnitude.IsOptimized()) + MagnitudeUpdateTpl<kEMOptimized> (m_Magnitude, ps, fromIndex, toIndex, m_Dampen); + else + MagnitudeUpdateTpl<kEMSlow> (m_Magnitude, ps, fromIndex, toIndex, m_Dampen); + } +} + +void ClampVelocityModule::CheckConsistency () +{ + m_Dampen = clamp<float> (m_Dampen, 0.0f, 1.0f); + m_X.SetScalar(std::max<float> (0.0f, m_X.GetScalar())); + m_Y.SetScalar(std::max<float> (0.0f, m_Y.GetScalar())); + m_Z.SetScalar(std::max<float> (0.0f, m_Z.GetScalar())); + m_Magnitude.SetScalar(std::max<float> (0.0f, m_Magnitude.GetScalar())); +} + + +template<class TransferFunction> +void ClampVelocityModule::Transfer (TransferFunction& transfer) +{ + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_X, "x"); + transfer.Transfer (m_Y, "y"); + transfer.Transfer (m_Z, "z"); + transfer.Transfer (m_Magnitude, "magnitude"); + transfer.Transfer (m_SeparateAxis, "separateAxis"); + transfer.Transfer (m_InWorldSpace, "inWorldSpace"); transfer.Align (); + transfer.Transfer (m_Dampen, "dampen"); +} +INSTANTIATE_TEMPLATE_TRANSFER(ClampVelocityModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/ClampVelocityModule.h b/Runtime/Graphics/ParticleSystem/Modules/ClampVelocityModule.h new file mode 100644 index 0000000..e315d61 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/ClampVelocityModule.h @@ -0,0 +1,29 @@ +#ifndef SHURIKENMODULECLAMPVELOCITY_H +#define SHURIKENMODULECLAMPVELOCITY_H + +#include "ParticleSystemModule.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemCurves.h" + +class ClampVelocityModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (ClampVelocityModule) + ClampVelocityModule (); + + void Update (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex); + void CheckConsistency (); + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + MinMaxCurve m_X; + MinMaxCurve m_Y; + MinMaxCurve m_Z; + MinMaxCurve m_Magnitude; + bool m_InWorldSpace; + bool m_SeparateAxis; + float m_Dampen; +}; + +#endif // SHURIKENMODULECLAMPVELOCITY_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/CollisionModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/CollisionModule.cpp new file mode 100644 index 0000000..9e16bde --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/CollisionModule.cpp @@ -0,0 +1,603 @@ +#include "UnityPrefix.h" +#include <float.h> +#include "CollisionModule.h" + +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Camera/Camera.h" +#include "Runtime/Geometry/Intersection.h" +#include "Runtime/Geometry/Plane.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemCurves.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemParticle.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemUtils.h" +#include "Runtime/Graphics/Transform.h" +#include "Runtime/Input/TimeManager.h" +#include "Runtime/Interfaces/IRaycast.h" +#include "Runtime/Math/Matrix4x4.h" +#include "Runtime/Misc/BuildSettings.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" + +#include "Editor/Src/Utility/DebugPrimitives.h" + +struct ParticleSystemCollisionParameters +{ + float bounceFactor; + float energyLossOnCollision; + float minKillSpeedSqr; + float particleRadius; + float dampen; + + PlaneColliderCache* planeColliderCache; + IRaycast* raycaster; + size_t rayBudget; + size_t nextParticleToTrace; + float voxelSize; +}; + +struct ColliderInfo +{ + Plane m_CollisionPlane; + bool m_Traced; + int m_ColliderInstanceID; + int m_RigidBodyOrColliderInstanceID; +}; + +struct CollisionInfo +{ + CollisionInfo():m_NumWorldCollisions(0),m_NumCachedCollisions(0),m_NumPlaneCollisions(0),m_Colliders(NULL) {} + + size_t m_NumWorldCollisions; + size_t m_NumCachedCollisions; + size_t m_NumPlaneCollisions; + ColliderInfo* m_Colliders; + + size_t AllCollisions() const { return m_NumWorldCollisions+m_NumCachedCollisions+m_NumPlaneCollisions; } +}; + +/// @TODO: Why does Vector3f has a constructor? WTF. +inline void CalculateCollisionResponse(const ParticleSystemReadOnlyState& roState, + ParticleSystemState& state, + ParticleSystemParticles& ps, + const size_t q, + const ParticleSystemCollisionParameters& params, + const Vector3f& position, + const Vector3f& velocity, + const HitInfo& hitInfo) +{ + // Reflect + dampen + Vector3f positionOffset = ReflectVector (position - hitInfo.intersection, hitInfo.normal) * params.dampen; + Vector3f newVelocity = ReflectVector(velocity, hitInfo.normal) * params.dampen; + + // Apply bounce + positionOffset -= hitInfo.normal * (Dot(positionOffset, hitInfo.normal)) * params.bounceFactor; + newVelocity -= hitInfo.normal * Dot(newVelocity, hitInfo.normal) * params.bounceFactor; + + ps.position[q] = hitInfo.intersection + positionOffset; + ps.velocity[q] = newVelocity - ps.animatedVelocity[q]; + + for(int s = 0; s < state.numCachedSubDataCollision; s++) + { + ParticleSystemEmissionState emissionState; + RecordEmit(emissionState, state.cachedSubDataCollision[s], roState, state, ps, kParticleSystemSubTypeCollision, s, q, 0.0f, 0.0001f, 1.0f); + } + ps.lifetime[q] -= params.energyLossOnCollision * ps.startLifetime[q]; + + if (ps.GetUsesCollisionEvents () && !(hitInfo.colliderInstanceID == 0)) + { + Vector3f wcIntersection = hitInfo.intersection; + Vector3f wcNormal = hitInfo.normal; + Vector3f wcVelocity = velocity; + if ( roState.useLocalSpace ) + { + wcIntersection = state.localToWorld.MultiplyPoint3 (wcIntersection); + wcNormal = state.localToWorld.MultiplyVector3 (wcNormal); + wcVelocity = state.localToWorld.MultiplyVector3 (wcVelocity); + } + ps.collisionEvents.AddEvent (ParticleCollisionEvent (wcIntersection, wcNormal, wcVelocity, hitInfo.colliderInstanceID, hitInfo.rigidBodyOrColliderInstanceID)); + //printf_console (Format ("Intersected '%s' -> id: %d -> go id: %d.\n", hitInfo.collider->GetName (), hitInfo.collider->GetInstanceID (), hitInfo.collider->GetGameObject ().GetInstanceID ()).c_str ()); +} +} + +CollisionModule::CollisionModule () : ParticleSystemModule(false) +, m_Type(0) +, m_Dampen(0.0f) +, m_Bounce(1.0f) +, m_EnergyLossOnCollision(0.0f) +, m_MinKillSpeed(0.0f) +, m_ParticleRadius(0.01f) +, m_Quality(0) +, m_VoxelSize(0.5f) +, m_CollisionMessages(false) +{ + m_CollidesWith.m_Bits = 0xFFFFFFFF; +} + +void CollisionModule::AllocateAndCache(const ParticleSystemReadOnlyState& roState, ParticleSystemState& state) +{ + Assert(!state.cachedCollisionPlanes); + + Matrix4x4f matrix = Matrix4x4f::identity; + if(roState.useLocalSpace) + matrix = state.worldToLocal; + + state.numCachedCollisionPlanes = 0; + for (unsigned i=0; i<kMaxNumPrimitives; ++i) + { + if (m_Primitives[i].IsNull ()) + continue; + state.numCachedCollisionPlanes++; + } + + state.cachedCollisionPlanes = ALLOC_TEMP_MANUAL(Plane, state.numCachedCollisionPlanes); + + int planeCount = 0; + + for (unsigned i=0; i<kMaxNumPrimitives; ++i) + { + if (m_Primitives[i].IsNull ()) + continue; + + const Transform* transform = m_Primitives[i]->QueryComponent(Transform); + const Vector3f position = matrix.MultiplyPoint3(transform->GetPosition()); + + Assert(planeCount <= state.numCachedCollisionPlanes); + const Vector3f normal = matrix.MultiplyVector3(RotateVectorByQuat (transform->GetRotation (), Vector3f::yAxis)); + state.cachedCollisionPlanes[planeCount].SetNormalAndPosition (normal, position); + state.cachedCollisionPlanes[planeCount].NormalizeRobust(); + planeCount++; + } +} + +void CollisionModule::FreeCache(ParticleSystemState& state) +{ + if(state.cachedCollisionPlanes) + { + FREE_TEMP_MANUAL(state.cachedCollisionPlanes); + state.cachedCollisionPlanes = 0; + state.numCachedCollisionPlanes = 0; + } +} + +// read the plane cache for a given range +size_t ReadCache(const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, const ParticleSystemParticles& ps, const ParticleSystemCollisionParameters& params, CollisionInfo& collision, const size_t fromIndex, const size_t& toIndex, const float dt) +{ + size_t numIntersections = 0; + for (size_t q = fromIndex; q < toIndex; ++q) + { + // initialise plane to value that guarantees no intersection + collision.m_Colliders[q].m_CollisionPlane.distance = FLT_MAX; + collision.m_Colliders[q].m_Traced = false; + collision.m_Colliders[q].m_ColliderInstanceID = -1; + collision.m_Colliders[q].m_RigidBodyOrColliderInstanceID = -1; + + // build start/end points + Vector3f to = ps.position[q]; + const Vector3f v = ps.velocity[q] + ps.animatedVelocity[q]; + const Vector3f d = v * dt; + Vector3f from = to - d; + // convert to WC + if ( roState.useLocalSpace ) + { + from = state.localToWorld.MultiplyPoint3(from); + to = state.localToWorld.MultiplyPoint3(to); + } + + // lookup the cache + Plane plane; + int colliderInstanceID; + int rigidBodyOrColliderInstanceID; + if ( params.planeColliderCache->Find (from, to-from, plane, colliderInstanceID, rigidBodyOrColliderInstanceID, params.voxelSize) ) + { + collision.m_Colliders[q].m_CollisionPlane = plane; + collision.m_Colliders[q].m_ColliderInstanceID = colliderInstanceID; + collision.m_Colliders[q].m_RigidBodyOrColliderInstanceID = rigidBodyOrColliderInstanceID; + numIntersections++; + } + } + return numIntersections; +} + +// Perform ray casts. Ray casts are done in WC and results are transformed back into sim space if necessary. +// There are three sets of indices: +// [fromIndex ; params.nextParticleToTrace[ for these we do caching if the cache is available +// [params.nextParticleToTrace ; params.nextParticleToTrace + params.rayBudget[ for these we trace fresh rays +// [params.nextParticleToTrace + params.rayBudget ; toIndex[ for these we do caching if the cache is available +CollisionInfo WorldCollision(const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, const ParticleSystemParticles& ps, const ParticleSystemCollisionParameters& params, const size_t fromIndex, const int filter, const float dt) +{ + CollisionInfo collisionCounter; + const bool approximate = ( params.planeColliderCache ? true : false ); + size_t numIntersections = 0; + + const size_t toIndex = ps.array_size(); + + // pre range that is cached + const size_t traceRangeFrom = approximate ? params.nextParticleToTrace : fromIndex; + const size_t traceRangeTo = approximate ? std::min(params.nextParticleToTrace+params.rayBudget, toIndex) : toIndex; + + const size_t fromIndex0 = fromIndex; + const size_t toIndex0 = traceRangeFrom; + // range to actually ray trace + const size_t fromIndex1 = traceRangeFrom; + const size_t toIndex1 = traceRangeTo; + // post range that is cached + const size_t fromIndex2 = traceRangeTo; + const size_t toIndex2 = toIndex; + + collisionCounter.m_Colliders = ALLOC_TEMP_MANUAL(ColliderInfo, ps.array_size()); + collisionCounter.m_NumCachedCollisions = (toIndex0-fromIndex0)+(toIndex2-fromIndex2); + collisionCounter.m_NumWorldCollisions = toIndex1-fromIndex1; + + if ( toIndex1-fromIndex1 > 0 ) + { + // batch trace selected range + dynamic_array< BatchedRaycast > rayBatch(toIndex1-fromIndex1,kMemTempAlloc); + dynamic_array< BatchedRaycastResult > rayResults(toIndex1-fromIndex1,kMemTempAlloc); + // build request array + size_t i = 0; + for (size_t q = fromIndex1; q < toIndex1; ++q, ++i) + { + // build start/end points + const Vector3f to = ps.position[q]; + const Vector3f v = ps.velocity[q] + ps.animatedVelocity[q]; + const Vector3f d = v * dt; + const Vector3f from = to - d; + + if ( approximate ) + { + // extend ray to trace across the entire voxel (and then some) + const float voxSize = std::max(params.voxelSize,params.voxelSize*kVoxelHeightMultiplier); + const Vector3f displacement = to-from; + + ///@TODO: handle magnitude 0. Should we skip the raycast??? + const Vector3f direction = NormalizeFast(displacement); + const Vector3f extendedTo = from + direction * voxSize; + + // insert into batch + rayBatch[i] = BatchedRaycast(q,from,extendedTo); + } + else + rayBatch[i] = BatchedRaycast(q,from,to); + + // initialise plane to value that guarantees no intersection + collisionCounter.m_Colliders[q].m_CollisionPlane.distance = FLT_MAX; + collisionCounter.m_Colliders[q].m_Traced = (!approximate); + collisionCounter.m_Colliders[q].m_ColliderInstanceID = -1; + collisionCounter.m_Colliders[q].m_RigidBodyOrColliderInstanceID = -1; + } + // convert to WC + if ( roState.useLocalSpace ) + { + const Matrix4x4f m = state.localToWorld; + for (size_t i = 0; i < rayBatch.size(); ++i) + { + rayBatch[i].from = m.MultiplyPoint3(rayBatch[i].from); + rayBatch[i].to = m.MultiplyPoint3(rayBatch[i].to); + } + } + // trace the rays + const size_t numIx = params.raycaster->BatchIntersect( rayBatch, rayResults, filter, approximate ); + + // convert back to local space + if ( roState.useLocalSpace ) + { + const Matrix4x4f m = state.worldToLocal; + for (size_t i = 0; i < numIx; ++i) + { + // the plane intersection was computed in WC, transform intersection to local space. + rayResults[i].hitInfo.intersection = m.MultiplyPoint3( rayResults[i].hitInfo.intersection ); + rayResults[i].hitInfo.normal = m.MultiplyVector3( rayResults[i].hitInfo.normal ); + } + } + + // store planes in the particles that intersected something + for (size_t i = 0; i < numIx; ++i) + { + const size_t q = rayBatch[rayResults[i].index].index; + collisionCounter.m_Colliders[q].m_CollisionPlane.normal = rayResults[i].hitInfo.normal; + collisionCounter.m_Colliders[q].m_CollisionPlane.distance = -Dot(rayResults[i].hitInfo.intersection, rayResults[i].hitInfo.normal); + collisionCounter.m_Colliders[q].m_ColliderInstanceID = rayResults[i].hitInfo.colliderInstanceID; + collisionCounter.m_Colliders[q].m_RigidBodyOrColliderInstanceID = rayResults[i].hitInfo.rigidBodyOrColliderInstanceID; + } + + // store intersections in cache + if (approximate) + { + for (size_t i = 0; i < numIx; ++i) + { + const size_t r = rayResults[i].index; + const size_t q = rayBatch[rayResults[i].index].index; + params.planeColliderCache->Replace (rayBatch[r].from, rayBatch[r].to-rayBatch[r].from, Plane(collisionCounter.m_Colliders[q].m_CollisionPlane), collisionCounter.m_Colliders[q].m_ColliderInstanceID, collisionCounter.m_Colliders[q].m_RigidBodyOrColliderInstanceID, params.voxelSize); + } + } + numIntersections += numIx; + } + + // process pre cache range + if ( toIndex0-fromIndex0 > 0 ) + { + numIntersections += ReadCache(roState, state, ps, params, collisionCounter, fromIndex0, toIndex0, dt); + } + // process post cache range + if ( toIndex2-fromIndex2 > 0 ) + { + numIntersections += ReadCache(roState, state, ps, params, collisionCounter, fromIndex2, toIndex2, dt); + } + + return collisionCounter; +} + + +// Plane collide all particles in simulation space (remember the cached planes are defined in sim space). +CollisionInfo PlaneCollision(const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, const ParticleSystemParticles& ps, const ParticleSystemCollisionParameters& params, const int fromIndex, const float dt) +{ + CollisionInfo collisionInfo; + collisionInfo.m_Colliders = ALLOC_TEMP_MANUAL(ColliderInfo, ps.array_size()); + + const bool newBehaviour = IS_CONTENT_NEWER_OR_SAME(kUnityVersion4_0_a1); + + size_t toIndex = ps.array_size(); + for (size_t q = fromIndex; q < toIndex; ++q) + { + // initialise to value that guarantees no intersection + collisionInfo.m_Colliders[q].m_CollisionPlane.distance = FLT_MAX; + + const Vector3f position = ps.position[q]; + const Vector3f velocity = ps.velocity[q] + ps.animatedVelocity[q]; + const Vector3f displacement = velocity * dt; + const Vector3f origin = position - displacement; + + // walk the planes + for (unsigned i=0; i<state.numCachedCollisionPlanes; ++i) + { + const Plane plane = state.cachedCollisionPlanes[i]; + + if ( newBehaviour ) + { + // new behaviour: + // plane collisions are single sided only, all particles 'behind' the plane will be forced onto the plane + const float dist = plane.GetDistanceToPoint(position); + if (dist > params.particleRadius) + continue; + } + else + { + // old (but fixed) behaviour, particles can collide with both front and back plane + const float d0 = plane.GetDistanceToPoint(origin); + const float d1 = plane.GetDistanceToPoint(position); + const bool sameSide = ( (d0 > 0.0f && d1 > 0.0f) || (d0 <= 0.0f && d1 <= 0.0f) ); + const float aD0 = Abs(d0); + const float aD1 = Abs(d1); + if ( sameSide && aD0 > params.particleRadius && aD1 > params.particleRadius ) + continue; + } + + collisionInfo.m_Colliders[q].m_CollisionPlane = plane; + collisionInfo.m_Colliders[q].m_ColliderInstanceID = 0; + collisionInfo.m_Colliders[q].m_RigidBodyOrColliderInstanceID = 0; + collisionInfo.m_NumPlaneCollisions++; + break; + } + } + + return collisionInfo; +} + +// Compute the collision plane to use for each particle +CollisionInfo UpdateCollisionPlanes(bool worldCollision, UInt32 collisionFilter, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, const ParticleSystemCollisionParameters& params, ParticleSystemParticles& ps, size_t fromIndex, float dt) +{ + if( worldCollision ) + { + // Check if we support raycasting + if (params.raycaster == NULL) + return CollisionInfo(); + + ////@TODO: dtPerParticle + return WorldCollision(roState, state, ps, params, fromIndex, collisionFilter, dt); + } + else + { + ////@TODO: dtPerParticle + return PlaneCollision(roState, state, ps, params, fromIndex, dt); + } +} + +// Collide the particles against their selected planes and update collision response - this is done purely in simulation space +static const float kEpsilon = 0.000001f; +static const float kRayEpsilon = 0.00001f; +void PerformPlaneCollisions(bool worldCollision, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, const ParticleSystemCollisionParameters& params, const CollisionInfo& collisionInfo, const int fromIndex, const float dt) +{ + // for world collisions we use and epsilon but for plane intersections we use the particle radius + const float radius = worldCollision ? kRayEpsilon : params.particleRadius; + const bool newBehaviour = IS_CONTENT_NEWER_OR_SAME(kUnityVersion4_0_a1); + //const bool approximate = ( params.planeColliderCache ? true : false ); + + // collide with plane + size_t toIndex = ps.array_size(); + + for (size_t q = fromIndex; q < toIndex; q++) + { + // check if the actual ray was traced in which case we want to disable the ray testing. Note: does not apply to approximate mode as we trace longer rays to improve cache quality + const bool tracedParticle = collisionInfo.m_Colliders[q].m_Traced; + const Plane plane = collisionInfo.m_Colliders[q].m_CollisionPlane; + if ( plane.distance != FLT_MAX ) + { + const Vector3f position = ps.position[q]; + const Vector3f velocity = ps.velocity[q] + ps.animatedVelocity[q]; + const Vector3f displacement = velocity * dt; + const Vector3f origin = position - displacement; + + HitInfo hit; + hit.normal = plane.GetNormal(); + hit.colliderInstanceID = collisionInfo.m_Colliders[q].m_ColliderInstanceID; + hit.rigidBodyOrColliderInstanceID = collisionInfo.m_Colliders[q].m_RigidBodyOrColliderInstanceID; + + if ( worldCollision ) + { + // plane ray dot product + const float VdN = Dot( plane.GetNormal(), displacement ); + if ( !tracedParticle && VdN >= 0 ) + continue; // only pick up front face intersections + + // Recreate hit point. + const float t = -( Dot( origin, plane.GetNormal() ) + plane.distance ) / VdN; + if ( !tracedParticle && (t < 0 || t > 1) ) + continue; + + // build intersection description from t value and ray + hit.intersection = origin + displacement * t; + + // Adjust intersection along normal to make sure the particle doesn't fall through geometry when it comes to rest. + // This is also an issue when dampen and bounce is zero in which case CalculateCollisionResponse will set the particle + // position to *exactly* the intersection point, which will then have issues next time the intersection is executed + // where it will try and compare two floating point numbers which are equal and the intersection test will come out + // either way depending on fp accuracy + // + // for world collisions we use and epsilon but for plane intersections we use the particle radius + hit.intersection += radius * hit.normal; + } + else + { + if (newBehaviour) + { + const float dist = plane.GetDistanceToPoint(position); + + if (dist > radius) + continue; + + const float VdN = Dot( plane.GetNormal(), velocity ); + if (VdN == 0.0F || VdN == -0.0F) + continue; + + const float t = -( Dot( position, plane.GetNormal() ) + plane.distance - radius ) / VdN; + + hit.intersection = position + velocity * t; + } + else + { + const float OriginDotPlane = Dot (plane.GetNormal(), origin); + const float signedDistanceToOrigin = OriginDotPlane + plane.distance; + const float PositionDotPlane = Dot (plane.GetNormal(), position); + const float signedDistanceToPosition = PositionDotPlane + plane.distance; + const bool originInside = Abs(signedDistanceToOrigin)<radius; + const bool positionInside = Abs(signedDistanceToPosition)<=radius; + const bool oppositeSide = !( (signedDistanceToOrigin > 0.0f && signedDistanceToPosition > 0.0f) || (signedDistanceToOrigin <= 0.0f && signedDistanceToPosition <= 0.0f) ); + + // if the points are both inside or outside the radius we can bail if they're not on opposite sides outside the radius + if ( originInside==positionInside && !(oppositeSide && !originInside && !positionInside ) ) + continue; + + // compute the side of the face we are on - this determines if we are trying to intersect with the front or back face of the plane + const float signOrigin = signedDistanceToOrigin < 0.0 ? -1.f : 1.f; + const float signedRadius = radius*signOrigin; + + // check the direction of the ray is opposite to the plane normal (the sign flips the normal as appropriate) + const float VdN = Dot( plane.GetNormal(), velocity ); + if (VdN*signOrigin >= 0) + continue; + + // calculate intersection point + const float t = -( signedDistanceToPosition - signedRadius ) / VdN; + hit.intersection = position + velocity * t; + } + } + + // compute the bounce + CalculateCollisionResponse(roState, state, ps, q, params, ps.position[q], ps.velocity[q] + ps.animatedVelocity[q], hit); + + // Kill particle? + const float speedSqr = SqrMagnitude(ps.velocity[q] + ps.animatedVelocity[q]); + if (ps.lifetime[q] < 0.0f || speedSqr < params.minKillSpeedSqr) + { + // when killing a particle the last element from the array is pulled into the position of the killed particle and toIndex is updated accordingly + // we do a q-- in order to make sure the new particle at q is also collided + collisionInfo.m_Colliders[q] = collisionInfo.m_Colliders[toIndex-1]; + KillParticle(roState, state, ps, q, toIndex); + q--; + } + } + } + + // resize array to account for killed particles + ps.array_resize(toIndex); +} + +// Update collision for the particle system. +void CollisionModule::Update (const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, size_t fromIndex, float dt ) +{ + // any work todo? + if (fromIndex == ps.array_size()) + return; + + ps.SetUsesCollisionEvents (m_CollisionMessages); + + // setup params + ParticleSystemCollisionParameters params; + params.bounceFactor = 1.0f - m_Bounce; + params.energyLossOnCollision = m_EnergyLossOnCollision; + params.minKillSpeedSqr = m_MinKillSpeed * m_MinKillSpeed; + params.particleRadius = m_ParticleRadius; + params.dampen = 1.0f - m_Dampen; + params.planeColliderCache = ( IsApproximate() ? &m_ColliderCache : NULL ); + params.raycaster = GetRaycastInterface(); + params.rayBudget = state.rayBudget; + params.voxelSize = m_VoxelSize; + params.nextParticleToTrace = ( state.nextParticleToTrace >= ps.array_size() ? fromIndex : std::max(state.nextParticleToTrace,fromIndex) ); + + // update particle collision planes + const CollisionInfo collisionCounter = UpdateCollisionPlanes(m_Type != kPlaneCollision, m_CollidesWith.m_Bits, roState, state, params, ps, fromIndex, dt); + + // update collider index + state.nextParticleToTrace = params.nextParticleToTrace + params.rayBudget; + + // decrement ray budget + state.rayBudget = (state.rayBudget>collisionCounter.m_NumWorldCollisions ? state.rayBudget-collisionCounter.m_NumWorldCollisions : 0); + + // early out if there were no collisions at all + if ( collisionCounter.AllCollisions() <= 0 ) + { + FREE_TEMP_MANUAL(collisionCounter.m_Colliders); + return; + } + + // perform plane collisions + PerformPlaneCollisions(m_Type != kPlaneCollision, roState, state, ps, params, collisionCounter, fromIndex, dt); + + FREE_TEMP_MANUAL(collisionCounter.m_Colliders); + + if (ps.GetUsesCollisionEvents ()) + { + ps.collisionEvents.SortCollisionEventThreadArray (); + } +} + +void CollisionModule::CheckConsistency () +{ + m_Dampen = clamp<float> (m_Dampen, 0.0f, 1.0f); + m_Bounce = clamp<float> (m_Bounce, 0.0f, 2.0f); + m_EnergyLossOnCollision = clamp<float> (m_EnergyLossOnCollision, 0.0f, 1.0f); + m_ParticleRadius = max<float>(m_ParticleRadius, 0.01f); +} + +template<class TransferFunction> +void CollisionModule::Transfer (TransferFunction& transfer) +{ + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_Type, "type"); + + const char* kPrimitiveNames [kMaxNumPrimitives] = { "plane0", "plane1", "plane2", "plane3", "plane4", "plane5"}; + for(int i = 0; i < kMaxNumPrimitives; i++) + transfer.Transfer (m_Primitives[i], kPrimitiveNames[i]); + + transfer.Transfer (m_Dampen, "dampen"); + transfer.Transfer (m_Bounce, "bounce"); + transfer.Transfer (m_EnergyLossOnCollision, "energyLossOnCollision"); + transfer.Transfer (m_MinKillSpeed, "minKillSpeed"); + transfer.Transfer (m_ParticleRadius, "particleRadius"); + transfer.Align(); + transfer.Transfer (m_CollidesWith, "collidesWith"); + transfer.Transfer (m_Quality, "quality"); + transfer.Align(); + transfer.Transfer (m_VoxelSize, "voxelSize"); + transfer.Transfer (m_CollisionMessages, "collisionMessages"); +} + +INSTANTIATE_TEMPLATE_TRANSFER(CollisionModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/CollisionModule.h b/Runtime/Graphics/ParticleSystem/Modules/CollisionModule.h new file mode 100644 index 0000000..77b5541 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/CollisionModule.h @@ -0,0 +1,62 @@ +#pragma once +#include "ParticleSystemModule.h" +#include "Runtime/BaseClasses/BitField.h" +#include "Runtime/BaseClasses/GameObject.h" +#include "Runtime/Geometry/Plane.h" +#include "Runtime/Utilities/SpatialHash.h" + +struct ParticleSystemParticles; +class Transform; + +class CollisionModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (CollisionModule) + CollisionModule (); + + void AllocateAndCache(const ParticleSystemReadOnlyState& roState, ParticleSystemState& state); + static void FreeCache(ParticleSystemState& state); + +#if UNITY_EDITOR + float GetEnergyLoss() const { return m_EnergyLossOnCollision; } + void SetEnergyLoss(float value) { m_EnergyLossOnCollision = value; }; + float GetMinKillSpeed() const { return m_MinKillSpeed; } + void SetMinKillSpeed(float value) { m_MinKillSpeed = value; }; +#endif + + bool GetUsesCollisionMessages () const {return m_CollisionMessages;} + bool IsWorldCollision() const {return m_Type==kWorldCollision;} + bool IsApproximate() const {return m_Quality>0;} + int GetQuality() const {return m_Quality;} + + void Update (const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, size_t fromIndex, float dt); + + void CheckConsistency (); + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + void ClearPrimitives(); + + enum { kPlaneCollision, kWorldCollision }; + enum { kMaxNumPrimitives = 6 }; + + PlaneColliderCache m_ColliderCache; + + // Serialized + int m_Type; + float m_Dampen; + float m_Bounce; + float m_EnergyLossOnCollision; + float m_MinKillSpeed; + float m_ParticleRadius; + /// Collides the particles with every collider whose layerMask & m_CollidesWith != 0 + BitField m_CollidesWith; + /// Perform approximate world particle collisions + int m_Quality; // selected quality, 0 is high (no approximations), 1 is medium (approximate), 2 is low (approximate) + float m_VoxelSize; + bool m_CollisionMessages; + PPtr<Transform> m_Primitives [kMaxNumPrimitives]; +}; + diff --git a/Runtime/Graphics/ParticleSystem/Modules/ColorByVelocityModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/ColorByVelocityModule.cpp new file mode 100644 index 0000000..0437322 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/ColorByVelocityModule.cpp @@ -0,0 +1,60 @@ +#include "UnityPrefix.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "ColorByVelocityModule.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemUtils.h" + +template<MinMaxGradientEvalMode mode> +void UpdateTpl(const ParticleSystemParticles& ps, ColorRGBA32* colorTemp, const MinMaxGradient& gradient, const OptimizedMinMaxGradient& optGradient, const Vector2f offsetScale, size_t fromIndex, size_t toIndex) +{ + for (size_t q = fromIndex; q < toIndex; ++q) + { + Vector3f vel = ps.velocity[q] + ps.animatedVelocity[q]; + const float time = InverseLerpFast01 (offsetScale, Magnitude(vel)); + const int random = GenerateRandomByte(ps.randomSeed[q] + kParticleSystemColorByVelocityGradientId); + + ColorRGBA32 value; + if(mode == kGEMGradient) + value = EvaluateGradient (optGradient, time); + else if(mode == kGEMGradientMinMax) + value = EvaluateRandomGradient (optGradient, time, random); + else + value = Evaluate (gradient, time, random); + + colorTemp[q] *= value; + } +} + +ColorBySpeedModule::ColorBySpeedModule () : ParticleSystemModule(false) +, m_Range (0.0f, 1.0f) +{} + +void ColorBySpeedModule::Update (const ParticleSystemParticles& ps, ColorRGBA32* colorTemp, size_t fromIndex, size_t toIndex) +{ + DebugAssert(toIndex <= ps.array_size()); + + Vector2f offsetScale = CalculateInverseLerpOffsetScale (m_Range); + OptimizedMinMaxGradient gradient; + m_Gradient.InitializeOptimized(gradient); + if(m_Gradient.minMaxState == kMMGGradient) + UpdateTpl<kGEMGradient>(ps, colorTemp, m_Gradient, gradient, offsetScale, fromIndex, toIndex); + else if(m_Gradient.minMaxState == kMMGRandomBetweenTwoGradients) + UpdateTpl<kGEMGradientMinMax>(ps, colorTemp, m_Gradient, gradient, offsetScale, fromIndex, toIndex); + else + UpdateTpl<kGEMSlow>(ps, colorTemp, m_Gradient, gradient, offsetScale, fromIndex, toIndex); +} + +void ColorBySpeedModule::CheckConsistency () +{ + const float MyEpsilon = 0.001f; + m_Range.x = std::min (m_Range.x, m_Range.y - MyEpsilon); +} + +template<class TransferFunction> +void ColorBySpeedModule::Transfer (TransferFunction& transfer) +{ + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_Gradient, "gradient"); + transfer.Transfer (m_Range, "range"); +} +INSTANTIATE_TEMPLATE_TRANSFER(ColorBySpeedModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/ColorByVelocityModule.h b/Runtime/Graphics/ParticleSystem/Modules/ColorByVelocityModule.h new file mode 100644 index 0000000..f140e68 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/ColorByVelocityModule.h @@ -0,0 +1,27 @@ +#ifndef SHURIKENMODULECOLORBYVELOCITY_H +#define SHURIKENMODULECOLORBYVELOCITY_H + +#include "ParticleSystemModule.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemGradients.h" +#include "Runtime/Math/Vector2.h" + +class ColorBySpeedModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (ColorBySpeedModule) + ColorBySpeedModule (); + + void Update (const ParticleSystemParticles& ps, ColorRGBA32* colorTemp, size_t fromIndex, size_t toIndex); + void CheckConsistency (); + + inline MinMaxGradient& GetGradient() { return m_Gradient; }; + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + MinMaxGradient m_Gradient; + Vector2f m_Range; +}; + +#endif // SHURIKENMODULECOLORBYVELOCITY_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/ColorModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/ColorModule.cpp new file mode 100644 index 0000000..c9e78c0 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/ColorModule.cpp @@ -0,0 +1,51 @@ +#include "UnityPrefix.h" +#include "ColorModule.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "Runtime/Math/Random/Random.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemUtils.h" + +template<MinMaxGradientEvalMode mode> +void UpdateTpl(const ParticleSystemParticles& ps, ColorRGBA32* colorTemp, const MinMaxGradient& gradient, const OptimizedMinMaxGradient& optGradient, size_t fromIndex, size_t toIndex) +{ + for (size_t q = fromIndex; q < toIndex; ++q) + { + const float time = NormalizedTime(ps, q); + const int random = GenerateRandomByte(ps.randomSeed[q] + kParticleSystemColorGradientId); + + ColorRGBA32 value; + if(mode == kGEMGradient) + value = EvaluateGradient (optGradient, time); + else if(mode == kGEMGradientMinMax) + value = EvaluateRandomGradient (optGradient, time, random); + else + value = Evaluate (gradient, time, random); + + colorTemp[q] *= value; + } +} + +ColorModule::ColorModule () : ParticleSystemModule(false) +{} + +void ColorModule::Update (const ParticleSystemParticles& ps, ColorRGBA32* colorTemp, size_t fromIndex, size_t toIndex) +{ + DebugAssert(toIndex <= ps.array_size()); + + OptimizedMinMaxGradient gradient; + m_Gradient.InitializeOptimized(gradient); + if(m_Gradient.minMaxState == kMMGGradient) + UpdateTpl<kGEMGradient>(ps, colorTemp, m_Gradient, gradient, fromIndex, toIndex); + else if(m_Gradient.minMaxState == kMMGRandomBetweenTwoGradients) + UpdateTpl<kGEMGradientMinMax>(ps, colorTemp, m_Gradient, gradient, fromIndex, toIndex); + else + UpdateTpl<kGEMSlow>(ps, colorTemp, m_Gradient, gradient, fromIndex, toIndex); +} + +template<class TransferFunction> +void ColorModule::Transfer (TransferFunction& transfer) +{ + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_Gradient, "gradient"); +} +INSTANTIATE_TEMPLATE_TRANSFER(ColorModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/ColorModule.h b/Runtime/Graphics/ParticleSystem/Modules/ColorModule.h new file mode 100644 index 0000000..1fe7250 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/ColorModule.h @@ -0,0 +1,25 @@ +#ifndef SHURIKENMODULECOLOR_H +#define SHURIKENMODULECOLOR_H + +#include "ParticleSystemModule.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemGradients.h" + +class ColorModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (ColorModule) + ColorModule (); + + void Update (const ParticleSystemParticles& ps, ColorRGBA32* colorTemp, size_t fromIndex, size_t toIndex); + void CheckConsistency() {}; + + inline MinMaxGradient& GetGradient() { return m_Gradient; }; + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + MinMaxGradient m_Gradient; +}; + +#endif // SHURIKENMODULECOLOR_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/EmissionModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/EmissionModule.cpp new file mode 100644 index 0000000..111b06f --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/EmissionModule.cpp @@ -0,0 +1,124 @@ +#include "UnityPrefix.h" +#include "EmissionModule.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "Runtime/Math/Vector2.h" + +static int AccumulateBursts (const ParticleSystemEmissionData& emissionData, float t0, float t1) +{ + int burstParticles = 0; + const size_t count = emissionData.burstCount; + for (size_t q = 0; q < count; ++q) + { + if (emissionData.burstTime[q] >= t0 && emissionData.burstTime[q] < t1) + burstParticles += emissionData.burstParticleCount[q]; + } + return burstParticles; +} + +static float AccumulateContinuous(const ParticleSystemEmissionData& emissionData, const float length, const float toT, const float dt) +{ + DebugAssert (length > 0.0001f); + DebugAssert (toT >= 0.0f); + DebugAssert (toT <= length); + float normalizedT = toT / length; + return std::max<float> (0.0f, Evaluate (emissionData.rate, normalizedT)) * dt; +}; + +EmissionModule::EmissionModule () : ParticleSystemModule(true) +{ + m_EmissionData.burstCount = 0; + m_EmissionData.type = kEmissionTypeTime; + for(int i = 0; i < ParticleSystemEmissionData::kMaxNumBursts; i++) + { + m_EmissionData.burstParticleCount[i] = 30; + m_EmissionData.burstTime[i] = 0.0f; + } +} + +void EmissionModule::Emit (ParticleSystemEmissionState& emissionState, size_t& amountOfParticlesToEmit, size_t& numContinuous, const ParticleSystemEmissionData& emissionData, const Vector3f velocity, float fromT, float toT, float dt, float length) +{ + const float epsilon = 0.0001f; + if(kEmissionTypeTime == emissionData.type) + { + float rate = 0.0f; + float t0 = std::max<float> (0.0f, fromT); + float t1 = std::max<float> (0.0f, toT); + if (t1 < t0) // handle loop + { + rate += AccumulateContinuous (emissionData, length, t1, t1); // from start to current time + t1 = length; // from last time to end + } + rate += AccumulateContinuous (emissionData, length, t1, t1 - t0); // from start to current time + + const float newParticles = rate; + if(newParticles >= epsilon) + emissionState.m_ParticleSpacing = 1.0f / newParticles; + else + emissionState.m_ParticleSpacing = 1.0f; + + emissionState.m_ToEmitAccumulator += newParticles; + amountOfParticlesToEmit = (int)emissionState.m_ToEmitAccumulator; + emissionState.m_ToEmitAccumulator -= (float)amountOfParticlesToEmit; + + // Continuous emits + numContinuous = amountOfParticlesToEmit; + + // Bursts + t0 = std::max<float> (0.0f, fromT); + t1 = std::max<float> (0.0f, toT); + if (t1 < t0) // handle loop + { + amountOfParticlesToEmit += AccumulateBursts (emissionData, 0.0f, t1); // from start to current time + t1 = length + epsilon; // from last time to end + } + amountOfParticlesToEmit += AccumulateBursts (emissionData, t0, t1); // from start to current time + } + else + { + float newParticles = AccumulateContinuous (emissionData, length, toT, dt) * Magnitude (velocity); // from start to current time + if(newParticles >= epsilon) + emissionState.m_ParticleSpacing = 1.0f / newParticles; + else + emissionState.m_ParticleSpacing = 1.0f; + + emissionState.m_ToEmitAccumulator += newParticles; + amountOfParticlesToEmit = (int)emissionState.m_ToEmitAccumulator; + emissionState.m_ToEmitAccumulator -= (float)amountOfParticlesToEmit; + + // Continuous emits + numContinuous = amountOfParticlesToEmit; + } +} + +void EmissionModule::CheckConsistency () +{ + m_EmissionData.rate.SetScalar(std::max<float> (0.0f, m_EmissionData.rate.GetScalar())); + + const size_t count = m_EmissionData.burstCount; + for (size_t q = 0; q < count; ++q) + { + m_EmissionData.burstTime[q] = std::max<float> (0.0f, m_EmissionData.burstTime[q]); + } +} + +template<class TransferFunction> +void EmissionModule::Transfer (TransferFunction& transfer) +{ + ParticleSystemModule::Transfer (transfer); + + transfer.Transfer (m_EmissionData.type, "m_Type"); + transfer.Transfer (m_EmissionData.rate, "rate"); + + const char* kCountNames [ParticleSystemEmissionData::kMaxNumBursts] = { "cnt0", "cnt1", "cnt2", "cnt3", }; + const char* kTimeNames [ParticleSystemEmissionData::kMaxNumBursts] = { "time0", "time1", "time2", "time3", }; + for(int i = 0; i < ParticleSystemEmissionData::kMaxNumBursts; i++) + transfer.Transfer (m_EmissionData.burstParticleCount[i], kCountNames[i]); + + for(int i = 0; i < ParticleSystemEmissionData::kMaxNumBursts; i++) + transfer.Transfer (m_EmissionData.burstTime[i], kTimeNames[i]); + + transfer.Transfer (m_EmissionData.burstCount, "m_BurstCount"); transfer.Align(); +} + +INSTANTIATE_TEMPLATE_TRANSFER(EmissionModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/EmissionModule.h b/Runtime/Graphics/ParticleSystem/Modules/EmissionModule.h new file mode 100644 index 0000000..75ad977 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/EmissionModule.h @@ -0,0 +1,34 @@ +#ifndef SHURIKENMODULEEMISSION_H +#define SHURIKENMODULEEMISSION_H + +#include "ParticleSystemModule.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemCurves.h" + +class Vector2f; + +class EmissionModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (EmissionModule) + EmissionModule (); + + enum { kEmissionTypeTime, kEmissionTypeDistance }; + + static void Emit (ParticleSystemEmissionState& emissionState, size_t& amountOfParticlesToEmit, size_t& numContinuous, const ParticleSystemEmissionData& emissionData, const Vector3f velocity, float fromT, float toT, float dt, float length); + void CheckConsistency (); + + int CalculateMaximumEmitCountEstimate(float deltaTime) const; + + const ParticleSystemEmissionData& GetEmissionDataRef() { return m_EmissionData; } + const ParticleSystemEmissionData& GetEmissionDataRef() const { return m_EmissionData; } + void GetEmissionDataCopy(ParticleSystemEmissionData* emissionData) { *emissionData = m_EmissionData; }; + ParticleSystemEmissionData& GetEmissionData() { return m_EmissionData; } + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + ParticleSystemEmissionData m_EmissionData; +}; + +#endif // SHURIKENMODULEEMISSION_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/ExternalForcesModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/ExternalForcesModule.cpp new file mode 100644 index 0000000..0b86e79 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/ExternalForcesModule.cpp @@ -0,0 +1,118 @@ +#include "UnityPrefix.h" +#include "ExternalForcesModule.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "../ParticleSystemUtils.h" +#include "Runtime/Graphics/Transform.h" +#include "Runtime/Geometry/Intersection.h" +#include "Runtime/Geometry/Sphere.h" +#include "Runtime/Terrain/Wind.h" +#include "Runtime/Input/TimeManager.h" + + +void ApplyRadialForce(ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex, const Vector3f position, const float radius, const float force, const float dt) +{ + for(int q = fromIndex; q < toIndex; q++) + { + Vector3f toForce = position - ps.position[q]; + float distanceToForce = Magnitude(toForce); + toForce = NormalizeSafe(toForce); + float forceFactor = clamp(distanceToForce/radius, 0.0f, 1.0f); + forceFactor = 1.0f - forceFactor * forceFactor; + Vector3f delta = toForce * force * forceFactor * dt; + ps.velocity[q] += delta; + } +} + +void ApplyDirectionalForce(ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex, const Vector3f position, const Vector3f direction, const float radius, const float force, const float dt) +{ + for(int q = fromIndex; q < toIndex; q++) + { + Vector3f delta = direction * force * dt; + ps.velocity[q] += delta; + } +} + +ExternalForcesModule::ExternalForcesModule () : ParticleSystemModule(false) +, m_Multiplier (1.0f) +{ +} + +void ExternalForcesModule::Update (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex, float dt) +{ + Matrix4x4f matrix = Matrix4x4f::identity; + if(roState.useLocalSpace) + Matrix4x4f::Invert_General3D(state.localToWorld, matrix); + + AABB aabb = state.minMaxAABB; + for(int i = 0; i < state.numCachedForces; i++) + { + const ParticleSystemExternalCachedForce& cachedForce = state.cachedForces[i]; + const Vector3f position = matrix.MultiplyPoint3(cachedForce.position); + const Vector3f direction = matrix.MultiplyVector3(cachedForce.direction); + const float radius = cachedForce.radius; + const float force = cachedForce.forceMain * m_Multiplier; + + const WindZone::WindZoneMode forceType = (WindZone::WindZoneMode)cachedForce.forceType; + if(WindZone::Spherical == forceType) + { + Sphere sphere (position, radius); + if(!IntersectAABBSphere (aabb, sphere)) + continue; + ApplyRadialForce(ps, fromIndex, toIndex, position, radius, force, dt); + } + else if(WindZone::Directional == forceType) + ApplyDirectionalForce(ps, fromIndex, toIndex, position, direction, radius, force, dt); + } +} + +// TODO: Perform culling here, instead of caching all of them +void ExternalForcesModule::AllocateAndCache(const ParticleSystemReadOnlyState& roState, ParticleSystemState& state) +{ + float time = GetTimeManager ().GetTimeSinceLevelLoad (); + WindManager::WindZoneList& windZones = WindManager::GetInstance().GetList(); + + state.numCachedForces = 0; + for (WindManager::WindZoneList::iterator it = windZones.begin (); it != windZones.end (); ++it) + state.numCachedForces++; + + // Allocate + state.cachedForces = ALLOC_TEMP_MANUAL(ParticleSystemExternalCachedForce, state.numCachedForces); + + // Cache + int i = 0; + for (WindManager::WindZoneList::iterator it = windZones.begin (); it != windZones.end (); ++it) + { + const WindZone& zone = **it; + ParticleSystemExternalCachedForce& cachedForce = state.cachedForces[i++]; + cachedForce.position = zone.GetComponent (Transform).GetPosition(); + cachedForce.direction = zone.GetComponent (Transform).GetLocalToWorldMatrix().GetAxisZ(); + cachedForce.forceType = zone.GetMode(); + cachedForce.radius = zone.GetRadius(); + + float phase = time * kPI * zone.GetWindPulseFrequency(); + float pulse = (cos (phase) + cos (phase * 0.375f) + cos (phase * 0.05f)) * 0.333f; + pulse = 1.0f + (pulse * zone.GetWindPulseMagnitude()); + cachedForce.forceMain = zone.GetWindMain() * pulse; + // Maybe implement using turbulence and time based phasing? + // ForceTurbulenceMultiply (maybe) // @TODO: Figure out what to do about turbulence. Do perlin field? Expensive but maybe cool! If use it: only do it when turbulence force is set to something + + //cachedForce.force = 1.0f; + } +} + +void ExternalForcesModule::FreeCache(ParticleSystemState& state) +{ + if(state.cachedForces) + FREE_TEMP_MANUAL(state.cachedForces); + state.cachedForces = NULL; + state.numCachedForces = 0; +} + +template<class TransferFunction> +void ExternalForcesModule::Transfer (TransferFunction& transfer) +{ + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_Multiplier, "multiplier"); +} +INSTANTIATE_TEMPLATE_TRANSFER(ExternalForcesModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/ExternalForcesModule.h b/Runtime/Graphics/ParticleSystem/Modules/ExternalForcesModule.h new file mode 100644 index 0000000..2abc949 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/ExternalForcesModule.h @@ -0,0 +1,25 @@ +#ifndef SHURIKENMODULEEXTERNALFORCES_H +#define SHURIKENMODULEEXTERNALFORCES_H + +#include "ParticleSystemModule.h" + +class ExternalForcesModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (ExternalForcesModule) + ExternalForcesModule (); + + void Update (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex, float dt); + void CheckConsistency() {}; + + static void AllocateAndCache(const ParticleSystemReadOnlyState& roState, ParticleSystemState& state); + static void FreeCache(ParticleSystemState& state); + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + float m_Multiplier; +}; + +#endif // SHURIKENMODULEEXTERNALFORCES_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/ForceModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/ForceModule.cpp new file mode 100644 index 0000000..d72da93 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/ForceModule.cpp @@ -0,0 +1,164 @@ +#include "UnityPrefix.h" +#include "ForceModule.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "../ParticleSystemUtils.h" +#include "Runtime/Math/Random/Random.h" + +template<ParticleSystemCurveEvalMode mode> +void UpdateTpl(const MinMaxCurve& x, const MinMaxCurve& y, const MinMaxCurve& z, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex, bool transform, const Matrix4x4f& matrix, float dt) +{ + for (size_t q = fromIndex; q < toIndex; ++q) + { + Vector3f random; + GenerateRandom3(random, ps.randomSeed[q] + kParticleSystemForceCurveId); + const float time = NormalizedTime(ps, q); + Vector3f f = Vector3f (Evaluate<mode> (x, time, random.x), Evaluate<mode> (y, time, random.y), Evaluate<mode> (z, time, random.z)); + if(transform) + f = matrix.MultiplyVector3 (f); + ps.velocity[q] += f * dt; + } +} + +template<bool isOptimized> +void UpdateProceduralTpl(const DualMinMax3DPolyCurves& pos, const DualMinMax3DPolyCurves& vel, ParticleSystemParticles& ps, const Matrix4x4f& matrix, bool transform) +{ + const size_t count = ps.array_size(); + for (int q=0; q<count; q++) + { + Vector3f random; + GenerateRandom3(random, ps.randomSeed[q] + kParticleSystemForceCurveId); + float time = NormalizedTime(ps, q); + float range = ps.startLifetime[q]; + + Vector3f delta; + Vector3f velocity; + if(isOptimized) + { + delta = Vector3f (EvaluateDoubleIntegrated(pos.optX, time, random.x), EvaluateDoubleIntegrated(pos.optY, time, random.y), EvaluateDoubleIntegrated(pos.optZ, time, random.z)); + velocity = Vector3f (EvaluateIntegrated(vel.optX, time, random.x), EvaluateIntegrated(vel.optY, time, random.y), EvaluateIntegrated(vel.optZ, time, random.z)); + } + else + { + delta = Vector3f (EvaluateDoubleIntegrated(pos.x, time, random.x), EvaluateDoubleIntegrated(pos.y, time, random.y), EvaluateDoubleIntegrated(pos.z, time, random.z)); + velocity = Vector3f (EvaluateIntegrated(vel.x, time, random.x), EvaluateIntegrated(vel.y, time, random.y), EvaluateIntegrated(vel.z, time, random.z)); + } + + // Sqr range + delta *= range * range; + velocity *= range; + + if(transform) + { + delta = matrix.MultiplyVector3 (delta); + velocity = matrix.MultiplyVector3 (velocity); + } + + ps.position[q] += delta; + ps.velocity[q] += velocity; + } +} + +ForceModule::ForceModule () : ParticleSystemModule(false) +, m_RandomizePerFrame (false) +, m_InWorldSpace(false) +{} + +void ForceModule::Update (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex, float dt) +{ + Matrix4x4f matrix; + bool transform = GetTransformationMatrix(matrix, !roState.useLocalSpace, m_InWorldSpace, state.localToWorld); + + if (m_RandomizePerFrame) + { + for (size_t q = fromIndex; q < toIndex; ++q) + { + const float t = NormalizedTime (ps, q); + const float randomX = Random01 (m_Random); + const float randomY = Random01 (m_Random); + const float randomZ = Random01 (m_Random); + Vector3f f (Evaluate (m_X, t, randomX), Evaluate (m_Y, t, randomY), Evaluate (m_Z, t, randomZ)); + if(transform) + f = matrix.MultiplyVector3 (f); + ps.velocity[q] += f * dt; + } + } + else + { + bool usesScalar = (m_X.minMaxState == kMMCScalar) && (m_Y.minMaxState == kMMCScalar) && (m_Z.minMaxState == kMMCScalar); + bool isOptimized = m_X.IsOptimized() && m_Y.IsOptimized() && m_Z.IsOptimized(); + bool usesMinMax = m_X.UsesMinMax() && m_Y.UsesMinMax() && m_Z.UsesMinMax(); + if(usesScalar) + UpdateTpl<kEMScalar>(m_X, m_Y, m_Z, ps, fromIndex, toIndex, transform, matrix, dt); + else if(isOptimized && usesMinMax) + UpdateTpl<kEMOptimizedMinMax>(m_X, m_Y, m_Z, ps, fromIndex, toIndex, transform, matrix, dt); + else if(isOptimized) + UpdateTpl<kEMOptimized>(m_X, m_Y, m_Z, ps, fromIndex, toIndex, transform, matrix, dt); + else + UpdateTpl<kEMSlow>(m_X, m_Y, m_Z, ps, fromIndex, toIndex, transform, matrix, dt); + } +} + +void ForceModule::UpdateProcedural (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps) +{ + Assert(!m_RandomizePerFrame); + + Matrix4x4f matrix; + bool transform = GetTransformationMatrix(matrix, !roState.useLocalSpace, m_InWorldSpace, state.localToWorld); + + DualMinMax3DPolyCurves posCurves; + DualMinMax3DPolyCurves velCurves; + if(m_X.IsOptimized() && m_Y.IsOptimized() && m_Z.IsOptimized()) + { + posCurves.optX = m_X.polyCurves; posCurves.optX.DoubleIntegrate(); + posCurves.optY = m_Y.polyCurves; posCurves.optY.DoubleIntegrate(); + posCurves.optZ = m_Z.polyCurves; posCurves.optZ.DoubleIntegrate(); + velCurves.optX = m_X.polyCurves; velCurves.optX.Integrate(); + velCurves.optY = m_Y.polyCurves; velCurves.optY.Integrate(); + velCurves.optZ = m_Z.polyCurves; velCurves.optZ.Integrate(); + UpdateProceduralTpl<true>(posCurves, velCurves, ps, matrix, transform); + } + else + { + DebugAssert(CurvesSupportProcedural (m_X.editorCurves, m_X.minMaxState)); + DebugAssert(CurvesSupportProcedural (m_Y.editorCurves, m_Y.minMaxState)); + DebugAssert(CurvesSupportProcedural (m_Z.editorCurves, m_Z.minMaxState)); + BuildCurves(posCurves.x, m_X.editorCurves, m_X.GetScalar(), m_X.minMaxState); posCurves.x.DoubleIntegrate(); + BuildCurves(posCurves.y, m_Y.editorCurves, m_Y.GetScalar(), m_Y.minMaxState); posCurves.y.DoubleIntegrate(); + BuildCurves(posCurves.z, m_Z.editorCurves, m_Z.GetScalar(), m_Z.minMaxState); posCurves.z.DoubleIntegrate(); + BuildCurves(velCurves.x, m_X.editorCurves, m_X.GetScalar(), m_X.minMaxState); velCurves.x.Integrate(); + BuildCurves(velCurves.y, m_Y.editorCurves, m_Y.GetScalar(), m_Y.minMaxState); velCurves.y.Integrate(); + BuildCurves(velCurves.z, m_Z.editorCurves, m_Z.GetScalar(), m_Z.minMaxState); velCurves.z.Integrate(); + UpdateProceduralTpl<false>(posCurves, velCurves, ps, matrix, transform); + } +} +void ForceModule::CalculateProceduralBounds(MinMaxAABB& bounds, const Matrix4x4f& localToWorld, float maxLifeTime) +{ + Vector2f xRange = m_X.FindMinMaxDoubleIntegrated(); + Vector2f yRange = m_Y.FindMinMaxDoubleIntegrated(); + Vector2f zRange = m_Z.FindMinMaxDoubleIntegrated(); + bounds.m_Min = Vector3f(xRange.x, yRange.x, zRange.x) * maxLifeTime * maxLifeTime; + bounds.m_Max = Vector3f(xRange.y, yRange.y, zRange.y) * maxLifeTime * maxLifeTime; + + if(m_InWorldSpace) + { + Matrix4x4f matrix; + Matrix4x4f::Invert_General3D(localToWorld, matrix); + matrix.SetPosition(Vector3f::zero); + AABB aabb = bounds; + TransformAABBSlow(aabb, matrix, aabb); + bounds = aabb; + } +} + +template<class TransferFunction> +void ForceModule::Transfer (TransferFunction& transfer) +{ + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_X, "x"); + transfer.Transfer (m_Y, "y"); + transfer.Transfer (m_Z, "z"); + transfer.Transfer (m_InWorldSpace, "inWorldSpace"); + transfer.Transfer (m_RandomizePerFrame, "randomizePerFrame"); transfer.Align (); +} +INSTANTIATE_TEMPLATE_TRANSFER(ForceModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/ForceModule.h b/Runtime/Graphics/ParticleSystem/Modules/ForceModule.h new file mode 100644 index 0000000..9ad54ec --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/ForceModule.h @@ -0,0 +1,36 @@ +#ifndef SHURIKENMODULEFORCE_H +#define SHURIKENMODULEFORCE_H + +#include "ParticleSystemModule.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemCurves.h" +#include "Runtime/Math/Random/rand.h" + +class ForceModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (ForceModule) + ForceModule (); + + void Update (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex, float dt); + void UpdateProcedural (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps); + void CalculateProceduralBounds(MinMaxAABB& bounds, const Matrix4x4f& localToWorld, float maxLifeTime); + void CheckConsistency() {}; + + inline MinMaxCurve& GetXCurve() { return m_X; } + inline MinMaxCurve& GetYCurve() { return m_Y; } + inline MinMaxCurve& GetZCurve() { return m_Z; } + inline bool GetRandomizePerFrame() { return m_RandomizePerFrame; } + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + MinMaxCurve m_X; + MinMaxCurve m_Y; + MinMaxCurve m_Z; + bool m_InWorldSpace; + bool m_RandomizePerFrame; + Rand m_Random; +}; + +#endif // SHURIKENMODULEFORCE_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/InitialModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/InitialModule.cpp new file mode 100644 index 0000000..680d175 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/InitialModule.cpp @@ -0,0 +1,193 @@ +#include "UnityPrefix.h" +#include "InitialModule.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemUtils.h" +#include "Runtime/Math/Random/Random.h" +#include "Runtime/Interfaces/IPhysics.h" +#include "Runtime/BaseClasses/IsPlaying.h" + +InitialModule::InitialModule () : ParticleSystemModule(true) +, m_GravityModifier(0.0f) +, m_InheritVelocity(0.0f) +, m_MaxNumParticles(1000) +{ +} + +Vector3f InitialModule::GetGravity (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state) const +{ +#if ENABLE_PHYSICS + IPhysics* physicsModule = GetIPhysics(); + if (!physicsModule) + return Vector3f::zero; + + Vector3f gravity = m_GravityModifier * physicsModule->GetGravity (); + if(roState.useLocalSpace) + { + Matrix4x4f worldToLocal; + Matrix4x4f::Invert_General3D(state.localToWorld, worldToLocal); + gravity = worldToLocal.MultiplyVector3(gravity); + } + return gravity; +#else + return Vector3f::zero; +#endif +} + +void InitialModule::Start (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const Matrix4x4f& matrix, size_t fromIndex, float t) +{ + DebugAssert(roState.lengthInSec > 0.0001f); + const float normalizedT = t / roState.lengthInSec; + DebugAssert (normalizedT >= 0.0f); + DebugAssert (normalizedT <= 1.0f); + + Rand& random = GetRandom(); + + Vector3f origin = matrix.GetPosition (); + + const size_t count = ps.array_size (); + for (size_t q = fromIndex; q < count; ++q) + { + UInt32 randUInt32 = random.Get (); + float rand = Rand::GetFloatFromInt (randUInt32); + UInt32 randByte = Rand::GetByteFromInt (randUInt32); + + const ColorRGBA32 col = Evaluate (m_Color, normalizedT, randByte); + float sz = std::max<float> (0.0f, Evaluate (m_Size, normalizedT, rand)); + Vector3f vel = matrix.MultiplyVector3 (Vector3f::zAxis); + float ttl = std::max<float> (0.0f, Evaluate (m_Lifetime, normalizedT, rand)); + float rot = Evaluate (m_Rotation, normalizedT, rand); + + ps.position[q] = origin; + ps.velocity[q] = vel; + ps.animatedVelocity[q] = Vector3f::zero; + ps.lifetime[q] = ttl; + ps.startLifetime[q] = ttl; + ps.size[q] = sz; + ps.rotation[q] = rot; + if(ps.usesRotationalSpeed) + ps.rotationalSpeed[q] = 0.0f; + ps.color[q] = col; + ps.randomSeed[q] = random.Get(); // One more iteration to avoid visible patterns between random spawned parameters and those used in update + if(ps.usesAxisOfRotation) + ps.axisOfRotation[q] = Vector3f::zAxis; + for(int acc = 0; acc < ps.numEmitAccumulators; acc++) + ps.emitAccumulator[acc][q] = 0.0f; + + } +} + +void InitialModule::Update (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex, float dt) const +{ + Vector3f gravityDelta = GetGravity(roState, state) * dt; + if(!CompareApproximately(gravityDelta, Vector3f::zero, 0.0001f)) + for (size_t q = fromIndex; q < toIndex; ++q) + ps.velocity[q] += gravityDelta; + + for (size_t q = fromIndex; q < toIndex; ++q) + ps.animatedVelocity[q] = Vector3f::zero; + + if(ps.usesRotationalSpeed) + for (size_t q = fromIndex; q < toIndex; ++q) + ps.rotationalSpeed[q] = 0.0f; +} + +void InitialModule::GenerateProcedural (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const ParticleSystemEmitReplay& emit) +{ + size_t count = emit.particlesToEmit; + float t = emit.t; + float alreadyPassedTime = emit.aliveTime; + + DebugAssert(roState.lengthInSec > 0.0001f); + const float normalizedT = t / roState.lengthInSec; + DebugAssert (normalizedT >= 0.0f); + DebugAssert (normalizedT <= 1.0f); + + Rand& random = GetRandom(); + + const Matrix4x4f localToWorld = !roState.useLocalSpace ? state.localToWorld : Matrix4x4f::identity; + Vector3f origin = localToWorld.GetPosition (); + for (size_t i = 0; i < count; ++i) + { + UInt32 randUInt32 = random.Get (); + float rand = Rand::GetFloatFromInt (randUInt32); + UInt32 randByte = Rand::GetByteFromInt (randUInt32); + + float frameOffset = (float(i) + emit.emissionOffset) * emit.emissionGap * float(i < emit.numContinuous); + + const ColorRGBA32 col = Evaluate (m_Color, normalizedT, randByte); + float sz = std::max<float> (0.0f, Evaluate (m_Size, normalizedT, rand)); + Vector3f vel = localToWorld.MultiplyVector3 (Vector3f::zAxis); + float ttlStart = std::max<float> (0.0f, Evaluate (m_Lifetime, normalizedT, rand)); + float ttl = ttlStart - alreadyPassedTime - frameOffset; + float rot = Evaluate (m_Rotation, normalizedT, rand); + + if (ttl < 0.0F) + continue; + + size_t q = ps.array_size(); + ps.array_resize(ps.array_size() + 1); + + ps.position[q] = origin; + ps.velocity[q] = vel; + ps.animatedVelocity[q] = Vector3f::zero; + ps.lifetime[q] = ttl; + ps.startLifetime[q] = ttlStart; + ps.size[q] = sz; + ps.rotation[q] = rot; + if(ps.usesRotationalSpeed) + ps.rotationalSpeed[q] = 0.0f; + ps.color[q] = col; + ps.randomSeed[q] = random.Get(); // One more iteration to avoid visible patterns between random spawned parameters and those used in update + if(ps.usesAxisOfRotation) + ps.axisOfRotation[q] = Vector3f::zAxis; + for(int acc = 0; acc < ps.numEmitAccumulators; acc++) + ps.emitAccumulator[acc][q] = 0.0f; + } +} + +void InitialModule::CheckConsistency () +{ + m_Lifetime.SetScalar(clamp<float> (m_Lifetime.GetScalar(), 0.05f, 100000.0f)); + m_Size.SetScalar(std::max<float> (0.0f, m_Size.GetScalar())); + m_MaxNumParticles = std::max<int> (0, m_MaxNumParticles); +} + +void InitialModule::AwakeFromLoad (ParticleSystem* system, const ParticleSystemReadOnlyState& roState) +{ + ResetSeed(roState); +} + +void InitialModule::ResetSeed(const ParticleSystemReadOnlyState& roState) +{ + if(roState.randomSeed == 0) + m_Random.SetSeed(GetGlobalRandomSeed ()); + else + m_Random.SetSeed(roState.randomSeed); +} + +Rand& InitialModule::GetRandom() +{ +#if UNITY_EDITOR + if(!IsWorldPlaying()) + return m_EditorRandom; + else +#endif + return m_Random; +} + +template<class TransferFunction> +void InitialModule::Transfer (TransferFunction& transfer) +{ + SetEnabled(true); // always enabled + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_Lifetime, "startLifetime"); + transfer.Transfer (m_Speed, "startSpeed"); + transfer.Transfer (m_Color, "startColor"); + transfer.Transfer (m_Size, "startSize"); + transfer.Transfer (m_Rotation, "startRotation"); + transfer.Transfer (m_GravityModifier, "gravityModifier"); + transfer.Transfer (m_InheritVelocity, "inheritVelocity"); + transfer.Transfer (m_MaxNumParticles, "maxNumParticles"); +} +INSTANTIATE_TEMPLATE_TRANSFER(InitialModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/InitialModule.h b/Runtime/Graphics/ParticleSystem/Modules/InitialModule.h new file mode 100644 index 0000000..cd602a9 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/InitialModule.h @@ -0,0 +1,66 @@ +#ifndef SHURIKENMODULEINITIAL_H +#define SHURIKENMODULEINITIAL_H + +#include "ParticleSystemModule.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemCurves.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemGradients.h" +#include "Runtime/Math/Random/rand.h" + +struct ParticleSystemState; + +class InitialModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (InitialModule) + InitialModule (); + + void Start (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const Matrix4x4f& matrix, size_t fromIndex, float t); + void Update (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex, float dt) const; + void GenerateProcedural (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const ParticleSystemEmitReplay& emit); + void CheckConsistency (); + void AwakeFromLoad (ParticleSystem* system, const ParticleSystemReadOnlyState& roState); + void ResetSeed(const ParticleSystemReadOnlyState& roState); + + inline MinMaxCurve& GetLifeTimeCurve() { return m_Lifetime; } + inline const MinMaxCurve& GetLifeTimeCurve() const { return m_Lifetime; } + inline MinMaxCurve& GetSpeedCurve() { return m_Speed; } + inline const MinMaxCurve& GetSpeedCurve() const { return m_Speed; } + inline MinMaxCurve& GetSizeCurve() { return m_Size; } + inline const MinMaxCurve& GetSizeCurve() const { return m_Size; } + inline MinMaxCurve& GetRotationCurve() { return m_Rotation; } + inline const MinMaxCurve& GetRotationCurve() const { return m_Rotation; } + inline MinMaxGradient& GetColor() { return m_Color; } + inline const MinMaxGradient& GetColor() const { return m_Color; } + inline void SetGravityModifier(float value) { m_GravityModifier = value; } + inline float GetGravityModifier() const { return m_GravityModifier; } + + inline void SetMaxNumParticles(int value) { m_MaxNumParticles = value; } + inline int GetMaxNumParticles() const { return m_MaxNumParticles; } + inline float GetInheritVelocity() const { return m_InheritVelocity; } + Vector3f GetGravity (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state) const; + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + Rand& GetRandom(); + + MinMaxCurve m_Lifetime; + MinMaxCurve m_Speed; + MinMaxGradient m_Color; + MinMaxCurve m_Size; + MinMaxCurve m_Rotation; + float m_GravityModifier; + float m_InheritVelocity; + int m_MaxNumParticles; + + Rand m_Random; + +#if UNITY_EDITOR +public: + Rand m_EditorRandom; +#endif +}; + + +#endif // SHURIKENMODULEINITIAL_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/ParticleSystemModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/ParticleSystemModule.cpp new file mode 100644 index 0000000..dee4872 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/ParticleSystemModule.cpp @@ -0,0 +1,141 @@ +#include "UnityPrefix.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "ParticleSystemModule.h" + +ParticleSystemReadOnlyState::ParticleSystemReadOnlyState() +: lengthInSec (5.0f) +, startDelay (0.0f) +, speed (1.0f) +, randomSeed (0) +, looping (true) +, prewarm (false) +, playOnAwake (true) +, useLocalSpace (true) +{ +} + +void ParticleSystemReadOnlyState::CheckConsistency() +{ + lengthInSec = std::max(lengthInSec, 0.1f); + lengthInSec = std::min(lengthInSec, 100000.0f); // Very large values can lead to editor locking up due to numerical instability. + startDelay = std::max(startDelay, 0.0f); + speed = std::max(speed, 0.0f); +} + +template<class TransferFunction> +void ParticleSystemReadOnlyState::Transfer (TransferFunction& transfer) +{ + TRANSFER (lengthInSec); + TRANSFER (startDelay); + TRANSFER (speed); + TRANSFER (randomSeed); + TRANSFER (looping); + TRANSFER (prewarm); + TRANSFER (playOnAwake); + transfer.Transfer (useLocalSpace, "moveWithTransform"); +} +INSTANTIATE_TEMPLATE_TRANSFER(ParticleSystemReadOnlyState) + +ParticleSystemState::ParticleSystemState () +: playing (false) +, needRestart (true) +, stopEmitting (false) +, accumulatedDt (0.0f) +, delayT (0.0f) +, t (0.0f) +, maxSize (0.0f) +, isSubEmitter (false) +, recordSubEmits(false) +, cullTime(0.0) +, culled(false) +, numLoops(0) +, invalidateProcedural(false) +, supportsProcedural(true) +, cachedForces(0) +, numCachedForces(0) +, cachedSubDataBirth(0) +, numCachedSubDataBirth(0) +, cachedSubDataCollision(0) +, numCachedSubDataCollision(0) +, cachedSubDataDeath(0) +, numCachedSubDataDeath(0) +, cachedCollisionPlanes(0) +, numCachedCollisionPlanes(0) +, rayBudget(0) +, nextParticleToTrace(0) +{ + ClearSubEmitterCommandBuffer(); + + localToWorld.SetIdentity (); + emitterVelocity = Vector3f::zero; + emitterScale = Vector3f::one; + minMaxAABB = MinMaxAABB (Vector3f::zero, Vector3f::zero); +} + +void ParticleSystemState::Tick (const ParticleSystemReadOnlyState& constState, float dt) +{ + t += dt; + + for(int i = 0; i < subEmitterCommandBuffer.commandCount; i++) + subEmitterCommandBuffer.commands[i].timeAlive += dt; + + if (!constState.looping) + t = std::min<float> (t, constState.lengthInSec); + else + if(t > constState.lengthInSec) + { + t -= constState.lengthInSec; + numLoops++; + } +} + +void ParticleSystemState::ClearSubEmitterCommandBuffer() +{ + if(cachedSubDataBirth) + { + for (int i = 0; i < numCachedSubDataBirth; ++i) + { + (cachedSubDataBirth+i)->~ParticleSystemSubEmitterData(); + } + FREE_TEMP_MANUAL(cachedSubDataBirth); + } + if(cachedSubDataCollision) + { + for (int i = 0; i < numCachedSubDataCollision; ++i) + { + (cachedSubDataCollision+i)->~ParticleSystemSubEmitterData(); + } + FREE_TEMP_MANUAL(cachedSubDataCollision); + } + if(cachedSubDataDeath) + { + for (int i = 0; i < numCachedSubDataDeath; ++i) + { + (cachedSubDataDeath+i)->~ParticleSystemSubEmitterData(); + } + FREE_TEMP_MANUAL(cachedSubDataDeath); + } + if(subEmitterCommandBuffer.commands) + FREE_TEMP_MANUAL(subEmitterCommandBuffer.commands); + + cachedSubDataBirth = cachedSubDataCollision = cachedSubDataDeath = 0; + numCachedSubDataBirth = numCachedSubDataCollision = numCachedSubDataDeath = 0; + subEmitterCommandBuffer.commands = 0; + subEmitterCommandBuffer.commandCount = subEmitterCommandBuffer.maxCommandCount = 0; +} + +template<class TransferFunction> +void ParticleSystemState::Transfer (TransferFunction& transfer) +{ + TRANSFER_DEBUG (t); +} +INSTANTIATE_TEMPLATE_TRANSFER(ParticleSystemState) + + +template<class TransferFunction> +void ParticleSystemModule::Transfer (TransferFunction& transfer) +{ + transfer.Transfer (m_Enabled, "enabled"); transfer.Align (); +} +INSTANTIATE_TEMPLATE_TRANSFER(ParticleSystemModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/ParticleSystemModule.h b/Runtime/Graphics/ParticleSystem/Modules/ParticleSystemModule.h new file mode 100644 index 0000000..b4746de --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/ParticleSystemModule.h @@ -0,0 +1,238 @@ +#ifndef SHURIKENMODULE_H +#define SHURIKENMODULE_H + +#include "../ParticleSystemCommon.h" +#include "../ParticleSystemCurves.h" +#include "Runtime/Math/Matrix4x4.h" +#include "Runtime/Geometry/AABB.h" +#include "Runtime/Utilities/dynamic_array.h" + +#define ENABLE_MULTITHREADED_PARTICLES ENABLE_MULTITHREADED_CODE + +#define DECLARE_MODULE(name) const char* GetName () { return #name; } DEFINE_GET_TYPESTRING(name) + +class ParticleSystem; +class Plane; + +struct ParticleSystemEmissionState +{ + ParticleSystemEmissionState() { Clear(); } + inline void Clear() + { + m_ToEmitAccumulator = 0.0f; + m_ParticleSpacing = 0.0f; + } + float m_ParticleSpacing; + float m_ToEmitAccumulator; +}; + +struct ParticleSystemEmissionData +{ + enum { kMaxNumBursts = 4 }; + + int type; + MinMaxCurve rate; + float burstTime[kMaxNumBursts]; + UInt16 burstParticleCount[kMaxNumBursts]; + UInt8 burstCount; +}; + + +struct ParticleSystemSubEmitterData +{ + ParticleSystemSubEmitterData() + :maxLifetime(0.0f) + ,startDelayInSec(0.0f) + ,lengthInSec(0.0f) + {} + + ParticleSystemEmissionData emissionData; + float maxLifetime; + float startDelayInSec; + float lengthInSec; + ParticleSystem* emitter; +}; + +// @TODO: Find "pretty" place for shared structs and enums? +struct ParticleSystemEmitReplay +{ + float t; + float aliveTime; + float emissionOffset; + float emissionGap; + int particlesToEmit; + size_t numContinuous; + UInt32 randomSeed; + + ParticleSystemEmitReplay (float inT, int inParticlesToEmit, float inEmissionOffset, float inEmissionGap, size_t inNumContinuous, UInt32 inRandomSeed) + : t (inT), particlesToEmit (inParticlesToEmit), aliveTime(0.0F), emissionOffset(inEmissionOffset), emissionGap(inEmissionGap), numContinuous(inNumContinuous), randomSeed(inRandomSeed) + {} +}; + +struct SubEmitterEmitCommand +{ + SubEmitterEmitCommand(ParticleSystemEmissionState inEmissionState, Vector3f inPosition, Vector3f inVelocity, ParticleSystemSubType inSubEmitterType, int inSubEmitterIndex, int inParticlesToEmit, int inParticlesToEmitContinuous, float inParentT, float inDeltaTime) + :emissionState(inEmissionState) + ,position(inPosition) + ,velocity(inVelocity) + ,subEmitterType(inSubEmitterType) + ,subEmitterIndex(inSubEmitterIndex) + ,particlesToEmit(inParticlesToEmit) + ,particlesToEmitContinuous(inParticlesToEmitContinuous) + ,deltaTime(inDeltaTime) + ,parentT(inParentT) + ,timeAlive(0.0f) + { + } + + ParticleSystemEmissionState emissionState; + Vector3f position; + Vector3f velocity; + ParticleSystemSubType subEmitterType; + int subEmitterIndex; + int particlesToEmit; + int particlesToEmitContinuous; + float deltaTime; + float parentT; // Used for StartModules + float timeAlive; +}; + +struct ParticleSystemSubEmitCmdBuffer +{ + ParticleSystemSubEmitCmdBuffer() + :commands(0) + ,commandCount(0) + ,maxCommandCount(0) + {} + + inline void AddCommand(const ParticleSystemEmissionState& emissionState, const Vector3f& initialPosition, const Vector3f& initialVelocity, const ParticleSystemSubType type, const int index, const int particlesToEmit, const int particlesToEmitContinuous, const float parentT, const float dt) + { + commands[commandCount++] = SubEmitterEmitCommand(emissionState, initialPosition, initialVelocity, type, index, particlesToEmit, particlesToEmitContinuous, parentT, dt); + } + inline bool IsFull() { return commandCount >= maxCommandCount; } + + SubEmitterEmitCommand* commands; + int commandCount; + int maxCommandCount; // Mostly to assert/test against trashing memory +}; + +struct ParticleSystemExternalCachedForce +{ + Vector3f position; + Vector3f direction; + int forceType; + float radius; + float forceMain; + float forceTurbulence; // not yet implemented +}; + +// @TODO: Maybe there's a better name for this? ParticleSystemSerializedState? Some shit like that :) +struct ParticleSystemReadOnlyState +{ + ParticleSystemReadOnlyState(); + + void CheckConsistency(); + + float lengthInSec; + float startDelay; + float speed; + UInt32 randomSeed; + bool looping; + bool prewarm; + bool playOnAwake; + bool useLocalSpace; + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); +}; + +// Some of these aren't actually state, but more like context. Separate it? +struct ParticleSystemState +{ + // state + float accumulatedDt; + float delayT; + bool playing; + bool needRestart; + bool stopEmitting; + size_t rayBudget; + size_t nextParticleToTrace; + + bool GetIsSubEmitter() const { return isSubEmitter; } +private: + // When setting this we need to ensure some other things happen as well + bool isSubEmitter; + +public: + + bool recordSubEmits; + + // Procedural mode / culling + bool supportsProcedural; // With the current parameter set, does the emitter support procedural mode? + bool invalidateProcedural; // This is set if anything changes from script at some point when running a system + bool culled; // Is particle system currently culled? + double cullTime; // Absolute time, so we need as double in case it runs for ages + int numLoops; // Number of loops executed + + // per-frame + Matrix4x4f localToWorld; + Matrix4x4f worldToLocal; + Vector3f emitterVelocity; + Vector3f emitterScale; + + MinMaxAABB minMaxAABB; + float maxSize; // Maximum size of particles due to setting from script + float t; + + // Temp alloc stuff + ParticleSystemSubEmitterData* cachedSubDataBirth; + size_t numCachedSubDataBirth; + ParticleSystemSubEmitterData* cachedSubDataCollision; + size_t numCachedSubDataCollision; + ParticleSystemSubEmitterData* cachedSubDataDeath; + size_t numCachedSubDataDeath; + ParticleSystemExternalCachedForce* cachedForces; + size_t numCachedForces; + Plane* cachedCollisionPlanes; + size_t numCachedCollisionPlanes; + ParticleSystemSubEmitCmdBuffer subEmitterCommandBuffer; + + dynamic_array<ParticleSystemEmitReplay> emitReplay; + ParticleSystemEmissionState emissionState; + + ParticleSystemState (); + + void Tick (const ParticleSystemReadOnlyState& constState, float dt); + void ClearSubEmitterCommandBuffer(); + + void SetIsSubEmitter(bool inIsSubEmitter) + { + if(inIsSubEmitter) + { + stopEmitting = true; + invalidateProcedural = true; + } + isSubEmitter = inIsSubEmitter; + } + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); +}; + + +class ParticleSystemModule +{ +public: + DECLARE_SERIALIZE (ParticleSystemModule) + ParticleSystemModule (bool enabled) : m_Enabled (enabled) {} + virtual ~ParticleSystemModule () {} + + inline bool GetEnabled() const { return m_Enabled; } + inline void SetEnabled(bool enabled) { m_Enabled = enabled; } + +private: + // shared data + bool m_Enabled; +}; + +#endif // SHURIKENMODULE_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/RotationByVelocityModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/RotationByVelocityModule.cpp new file mode 100644 index 0000000..c194808 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/RotationByVelocityModule.cpp @@ -0,0 +1,51 @@ +#include "UnityPrefix.h" +#include "RotationByVelocityModule.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemUtils.h" + + +RotationBySpeedModule::RotationBySpeedModule () : ParticleSystemModule(false) +, m_Range (0.0f, 1.0f) +{} + +template<ParticleSystemCurveEvalMode mode> +void UpdateTpl(const MinMaxCurve& curve, ParticleSystemParticles& ps, size_t fromIndex, size_t toIndex, const Vector2f offsetScale) +{ + if (!ps.usesRotationalSpeed) return; + for (size_t q = fromIndex; q < toIndex; ++q) + { + const Vector3f vel = ps.velocity[q] + ps.animatedVelocity[q]; + const float t = InverseLerpFast01 (offsetScale, Magnitude(vel)); + const float random = GenerateRandom(ps.randomSeed[q] + kParticleSystemRotationBySpeedCurveId); + ps.rotationalSpeed[q] += Evaluate<mode> (curve, t, random); + } +} + +void RotationBySpeedModule::Update (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex) +{ + Vector2f offsetScale = CalculateInverseLerpOffsetScale (m_Range); + if (m_Curve.minMaxState == kMMCScalar) + UpdateTpl<kEMScalar>(m_Curve, ps, fromIndex, toIndex, offsetScale); + else if(m_Curve.IsOptimized() && m_Curve.UsesMinMax()) + UpdateTpl<kEMOptimizedMinMax>(m_Curve, ps, fromIndex, toIndex, offsetScale); + else if(m_Curve.IsOptimized()) + UpdateTpl<kEMOptimized>(m_Curve, ps, fromIndex, toIndex, offsetScale); + else + UpdateTpl<kEMSlow>(m_Curve, ps, fromIndex, toIndex, offsetScale); +} + +void RotationBySpeedModule::CheckConsistency () +{ + const float MyEpsilon = 0.001f; + m_Range.y = std::max (m_Range.x + MyEpsilon, m_Range.y); +} + +template<class TransferFunction> +void RotationBySpeedModule::Transfer (TransferFunction& transfer) +{ + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_Curve, "curve"); + transfer.Transfer (m_Range, "range"); +} +INSTANTIATE_TEMPLATE_TRANSFER(RotationBySpeedModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/RotationByVelocityModule.h b/Runtime/Graphics/ParticleSystem/Modules/RotationByVelocityModule.h new file mode 100644 index 0000000..548f9c3 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/RotationByVelocityModule.h @@ -0,0 +1,27 @@ +#ifndef SHURIKENMODULEROTATIONBYVELOCITY_H +#define SHURIKENMODULEROTATIONBYVELOCITY_H + +#include "ParticleSystemModule.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemCurves.h" +#include "Runtime/Math/Vector2.h" + +class RotationBySpeedModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (RotationBySpeedModule) + RotationBySpeedModule (); + + void Update (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex); + void CheckConsistency (); + + inline MinMaxCurve& GetCurve() { return m_Curve; } + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + MinMaxCurve m_Curve; + Vector2f m_Range; +}; + +#endif // SHURIKENMODULEROTATIONBYVELOCITY_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/RotationModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/RotationModule.cpp new file mode 100644 index 0000000..7f331d6 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/RotationModule.cpp @@ -0,0 +1,83 @@ +#include "UnityPrefix.h" +#include "RotationModule.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemUtils.h" +#include "Runtime/Math/Random/Random.h" +#include "Runtime/Math/Vector2.h" + +struct DualMinMaxPolyCurves +{ + MinMaxOptimizedPolyCurves optRot; + MinMaxPolyCurves rot; +}; + +template<ParticleSystemCurveEvalMode mode> +void UpdateTpl(const MinMaxCurve& curve, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex) +{ + if ( !ps.usesRotationalSpeed ) return; + for (size_t q = fromIndex; q < toIndex; ++q) + { + const float time = NormalizedTime(ps, q); + const float random = GenerateRandom(ps.randomSeed[q] + kParticleSystemRotationCurveId); + ps.rotationalSpeed[q] += Evaluate<mode> (curve, time, random); + } +} + +template<bool isOptimized> +void UpdateProceduralTpl(const DualMinMaxPolyCurves& curves, ParticleSystemParticles& ps) +{ + const size_t count = ps.array_size (); + for (int q=0; q<count; q++) + { + float time = NormalizedTime(ps, q); + float random = GenerateRandom(ps.randomSeed[q] + kParticleSystemRotationCurveId); + float range = ps.startLifetime[q]; + float value; + if(isOptimized) + value = EvaluateIntegrated (curves.optRot, time, random); + else + value = EvaluateIntegrated (curves.rot, time, random); + ps.rotation[q] += value * range; + } +} + +RotationModule::RotationModule() : ParticleSystemModule(false) +{} + +void RotationModule::Update (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex) +{ + if (m_Curve.minMaxState == kMMCScalar) + UpdateTpl<kEMScalar>(m_Curve, ps, fromIndex, toIndex); + else if(m_Curve.IsOptimized() && m_Curve.UsesMinMax()) + UpdateTpl<kEMOptimizedMinMax>(m_Curve, ps, fromIndex, toIndex); + else if(m_Curve.IsOptimized()) + UpdateTpl<kEMOptimized>(m_Curve, ps, fromIndex, toIndex); + else + UpdateTpl<kEMSlow>(m_Curve, ps, fromIndex, toIndex); + +} + +void RotationModule::UpdateProcedural (const ParticleSystemState& state, ParticleSystemParticles& ps) +{ + DualMinMaxPolyCurves curves; + if(m_Curve.IsOptimized()) + { + curves.optRot = m_Curve.polyCurves; curves.optRot.Integrate(); + UpdateProceduralTpl<true>(curves, ps); + } + else + { + DebugAssert(CurvesSupportProcedural (m_Curve.editorCurves, m_Curve.minMaxState)); + BuildCurves(curves.rot, m_Curve.editorCurves, m_Curve.GetScalar(), m_Curve.minMaxState); curves.rot.Integrate(); + UpdateProceduralTpl<false>(curves, ps); + } +} + +template<class TransferFunction> +void RotationModule::Transfer (TransferFunction& transfer) +{ + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_Curve, "curve"); +} +INSTANTIATE_TEMPLATE_TRANSFER(RotationModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/RotationModule.h b/Runtime/Graphics/ParticleSystem/Modules/RotationModule.h new file mode 100644 index 0000000..494414e --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/RotationModule.h @@ -0,0 +1,27 @@ +#ifndef SHURIKENMODULEROTATION_H +#define SHURIKENMODULEROTATION_H + +#include "ParticleSystemModule.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemCurves.h" + +class RotationModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (RotationModule) + + RotationModule(); + + void Update (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex); + void UpdateProcedural (const ParticleSystemState& state, ParticleSystemParticles& ps); + void CheckConsistency() {}; + + inline MinMaxCurve& GetCurve() { return m_Curve; } + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + MinMaxCurve m_Curve; +}; + +#endif // SHURIKENMODULEROTATION_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/ShapeModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/ShapeModule.cpp new file mode 100644 index 0000000..3cdb007 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/ShapeModule.cpp @@ -0,0 +1,650 @@ +#include "UnityPrefix.h" +#include "ShapeModule.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemUtils.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystem.h" +#include "Runtime/Graphics/TriStripper.h" +#include "Runtime/Math/Vector2.h" +#include "Runtime/Math/Random/Random.h" +#include "Runtime/Geometry/ComputionalGeometry.h" +#include "Runtime/BaseClasses/IsPlaying.h" +#include "Runtime/Utilities/StrideIterator.h" +#include "Runtime/Filters/Mesh/LodMesh.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" + +enum MeshDistributionMode +{ + kDistributionVertex, + kDistributionTriangle, +}; + + +/// This gives a random barycentric coord (on the edge of triangle) +// @TODO: Stupid: Make this in a faster way +inline Vector3f RandomBarycentricCoordEdge (Rand& rand) +{ + float u = rand.GetFloat (); + float v = rand.GetFloat (); + if (u + v > 1.0F) + { + u = 1.0F - u; + v = 1.0F - v; + } + float w = 1.0F - u - v; + + int edge = RangedRandom(rand, 0, 2); + if(0 == edge) + { + v += 0.5f * u; + w += 0.5f * u; + u = 0.0f; + } + else if(1 == edge) + { + u += 0.5f * v; + w += 0.5f * v; + v = 0.0f; + } + else + { + u += 0.5f * w; + v += 0.5f * w; + w = 0.0f; + } + + return Vector3f (u, v, w); +} + + +// TODO: It could make sense to initialize in separated loops. i.e. separate position and velcoity vectors +inline void EmitterStoreData(const Matrix4x4f& localToWorld, const Vector3f& scale, ParticleSystemParticles& ps, size_t q, Vector3f& pos, Vector3f& n, Rand& random, bool randomDirection) +{ + if(randomDirection) + n = RandomUnitVector (random); + + n = NormalizeSafe(n); + + pos = Scale(pos, scale); + + Vector3f vel = Magnitude (ps.velocity[q]) * n; + vel = localToWorld.MultiplyVector3 (vel); + + // @TODO: Sooo... why multiply point and then undo the result of it? Why not just MultiplyVector? + pos = localToWorld.MultiplyPoint3 (pos) - localToWorld.GetPosition(); + ps.position[q] += pos; + ps.velocity[q] = vel; + +#if 0 // WIP code for converting to spherical + Vector3f sp = ps.position[q]; + ps.position[q].x = Sqrt(sp.x*sp.x + sp.y*sp.y + sp.z*sp.z); + ps.position[q].y = acosf(sp.z/ps.position[q].x); + ps.position[q].z = acosf(sp.y/ps.position[q].x); +#endif + + if(ps.usesAxisOfRotation) + { + Vector3f tan = Cross (-n, Vector3f::zAxis); + if (SqrMagnitude (tan) <= 0.01) + tan = Cross (-pos, Vector3f::zAxis); + if (SqrMagnitude (tan) <= 0.01) + tan = Vector3f::yAxis; + ps.axisOfRotation[q] = Normalize (tan); + } +} + +inline void EmitterStoreData(const Matrix4x4f& localToWorld, const Vector3f& scale, ParticleSystemParticles& ps, size_t q, Vector3f& pos, Vector3f& n, ColorRGBA32& color, Rand& random, bool randomDirection) +{ + EmitterStoreData(localToWorld, scale, ps, q, pos, n, random, randomDirection); + ps.color[q] *= color; +} + + +template<MeshDistributionMode distributionMode> +void GetPositionMesh (Vector3f& pos, + Vector3f& n, + ColorRGBA32& color, + const ParticleSystemEmitterMeshVertex* vertexData, + const int vertexCount, + const MeshTriangleData* triangleData, + const UInt32 numPrimitives, + float totalTriangleArea, + Rand& random, + bool edge) +{ + // position/normal of particle is vertex/vertex normal from mesh + if(kDistributionVertex == distributionMode) + { + int vertexIndex = RangedRandom (random, 0, vertexCount); + pos = vertexData[vertexIndex].position; + n = vertexData[vertexIndex].normal; + color = vertexData[vertexIndex].color; + } + else if(kDistributionTriangle == distributionMode) + { + float randomArea = RangedRandom(random, 0.0f, totalTriangleArea); + float accArea = 0.0f; + UInt32 triangleIndex = 0; + + for(UInt32 i = 0; i < numPrimitives; i++) + { + const MeshTriangleData& data = triangleData[i]; + accArea += data.area; + if(accArea >= randomArea) + { + triangleIndex = i; + break; + } + } + + const MeshTriangleData& data = triangleData[triangleIndex]; + UInt16 a = data.indices[0]; + UInt16 b = data.indices[1]; + UInt16 c = data.indices[2]; + + Vector3f barycenter; + if(edge) + barycenter = RandomBarycentricCoordEdge (random); + else + barycenter = RandomBarycentricCoord (random); + + // Interpolate vertex with barycentric coordinate + pos = barycenter.x * vertexData[a].position + barycenter.y * vertexData[b].position + barycenter.z * vertexData[c].position; + n = barycenter.x * vertexData[a].normal + barycenter.y * vertexData[b].normal + barycenter.z * vertexData[c].normal; + + // TODO: Don't convert to floats!!! + ColorRGBAf color1 = vertexData[a].color; + ColorRGBAf color2 = vertexData[b].color; + ColorRGBAf color3 = vertexData[c].color; + color = barycenter.x * color1 + barycenter.y * color2 + barycenter.z * color3; + } +} + +static bool CompareMeshTriangleData (const MeshTriangleData& a, const MeshTriangleData& b) +{ + return (a.area > b.area); +} + +static float BuildMeshAreaTable(MeshTriangleData* triData, const StrideIterator<Vector3f> vertices, const UInt16* indices, int numTriangles) +{ + float result = 0.0f; + for(int i = 0; i < numTriangles; i++) + { + const UInt16 a = indices[i * 3 + 0]; + const UInt16 b = indices[i * 3 + 1]; + const UInt16 c = indices[i * 3 + 2]; + float area = TriangleArea3D (vertices[a], vertices[b], vertices[c]); + result += area; + + triData[i].indices[0] = a; + triData[i].indices[1] = b; + triData[i].indices[2] = c; + triData[i].area = area; + } + + return result; +} + +// ------------------------------------------------------------------------------------------ + +ShapeModule::ShapeModule () : ParticleSystemModule(true) +, m_Type (kCone) +, m_RandomDirection (false) +, m_Angle(25.0f) +, m_Radius(1.0f) +, m_Length(5.0f) +, m_BoxX(1.0f) +, m_BoxY(1.0f) +, m_BoxZ(1.0f) +, m_PlacementMode(kVertex) +, m_CachedMesh(NULL) +, m_MeshNode (NULL) +{ +} + +void ShapeModule::Start (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const Matrix4x4f& matrix, size_t fromIndex, float t) +{ + DebugAssert(roState.lengthInSec > 0.0001f); + const float normalizedT = t / roState.lengthInSec; + DebugAssert (normalizedT >= 0.0f); + DebugAssert (normalizedT <= 1.0f); + + Rand& random = GetRandom(); + + if (m_Type == kMesh) + { + if(!m_CachedMesh) + return; + + if(!m_CachedVertexData.size()) + return; + + if(!m_CachedTriangleData.size()) + return; + + const ParticleSystemEmitterMeshVertex* vertexData = &m_CachedVertexData[0]; + const int vertexCount = m_CachedVertexData.size(); + size_t count = ps.array_size (); + switch(m_PlacementMode) + { + case kVertex: + { + for (int q = fromIndex; q < count; ++q) + { + Vector3f pos; + Vector3f n; + ColorRGBA32 color; + GetPositionMesh<kDistributionVertex>(pos, n, color, vertexData, vertexCount, NULL, 0, m_CachedTotalTriangleArea, random, false); + EmitterStoreData(matrix, state.emitterScale, ps, q, pos, n, color, random, m_RandomDirection); + } + break; + } + case kEdge: + { + for (int q = fromIndex; q < count; ++q) + { + Vector3f pos; + Vector3f n; + ColorRGBA32 color; + GetPositionMesh<kDistributionTriangle>(pos, n, color, vertexData, vertexCount, m_CachedTriangleData.begin(), m_CachedTriangleData.size(), m_CachedTotalTriangleArea, random, true); + EmitterStoreData(matrix, state.emitterScale, ps, q, pos, n, color, random, m_RandomDirection); + } + break; + } + case kTriangle: + { + for (int q = fromIndex; q < count; ++q) + { + Vector3f pos; + Vector3f n; + ColorRGBA32 color; + GetPositionMesh<kDistributionTriangle>(pos, n, color, vertexData, vertexCount, m_CachedTriangleData.begin(), m_CachedTriangleData.size(), m_CachedTotalTriangleArea, random, false); + EmitterStoreData(matrix, state.emitterScale, ps, q, pos, n, color, random, m_RandomDirection); + } + break; + } + default: + { + DebugAssert(0 && "PlacementMode Not Supported"); + } + } + } + else + { + const float r = m_Radius; + + float a = Deg2Rad (m_Angle); + float sinA = Sin (a); + float cosA = Cos (a); + float length = m_Length; + + const size_t count = ps.array_size (); + switch(m_Type) + { + case kSphere: + { + for (int q = fromIndex; q < count; ++q) + { + Vector3f pos = RandomPointInsideUnitSphere (random) * r; + Vector3f n = pos; + EmitterStoreData(matrix, state.emitterScale, ps, q, pos, n, random, m_RandomDirection); + } + break; + } + case kSphereShell: + { + for (int q = fromIndex; q < count; ++q) + { + Vector3f pos = RandomUnitVector(random) * r; + Vector3f n = pos; + EmitterStoreData(matrix, state.emitterScale, ps, q, pos, n, random, m_RandomDirection); + } + break; + } + case kHemiSphere: + { + for (int q = fromIndex; q < count; ++q) + { + Vector3f pos = RandomPointInsideUnitSphere (random) * r; + if (pos.z < 0.0f) + pos.z *= -1.0f; + Vector3f n = pos; + EmitterStoreData(matrix, state.emitterScale, ps, q, pos, n, random, m_RandomDirection); + } + break; + } + case kHemiSphereShell: + { + for (int q = fromIndex; q < count; ++q) + { + Vector3f pos = RandomUnitVector (random) * r; + if (pos.z < 0.0f) + pos.z *= -1.0f; + Vector3f n = pos; + EmitterStoreData(matrix, state.emitterScale, ps, q, pos, n, random, m_RandomDirection); + } + break; + } + case kCone: + { + for (int q = fromIndex; q < count; ++q) + { + Vector2f posXY = RandomPointInsideUnitCircle (random); + Vector2f nXY; + if(m_RandomDirection) + nXY = RandomPointInsideUnitCircle (random) * sinA; + else + nXY = Vector2f(posXY.x, posXY.y)* sinA; + Vector3f n (nXY.x, nXY.y, cosA); + Vector3f pos (posXY.x * r, posXY.y * r, 0.0f); + EmitterStoreData(matrix, state.emitterScale, ps, q, pos, n, random, false); + } + break; + } + case kConeShell: + { + for (int q = fromIndex; q < count; ++q) + { + Vector2f posXY = NormalizeSafe(RandomUnitVector2 (random)); + + Vector2f nXY; + if(m_RandomDirection) + nXY = RandomPointInsideUnitCircle (random) * sinA; + else + nXY = Vector2f(posXY.x, posXY.y)* sinA; + Vector3f n (nXY.x, nXY.y, cosA); + Vector3f pos (posXY.x * r, posXY.y * r, 0.0f); + EmitterStoreData(matrix, state.emitterScale, ps, q, pos, n, random, false); + } + break; + } + case kConeVolume: + { + for (int q = fromIndex; q < count; ++q) + { + Vector2f posXY = RandomPointInsideUnitCircle (random); + Vector2f nXY = Vector2f(posXY.x, posXY.y)* sinA; + Vector3f n (nXY.x, nXY.y, cosA); + Vector3f pos (posXY.x * r, posXY.y * r, 0.0f); + pos += length * Random01(random) * NormalizeSafe(n); + EmitterStoreData(matrix, state.emitterScale, ps, q, pos, n, random, m_RandomDirection); + } + break; + } + case kConeVolumeShell: + { + for (int q = fromIndex; q < count; ++q) + { + Vector2f posXY = NormalizeSafe(RandomUnitVector2 (random)); + Vector2f nXY = Vector2f(posXY.x, posXY.y)* sinA; + Vector3f n (nXY.x, nXY.y, cosA); + Vector3f pos = Vector3f(posXY.x * r, posXY.y * r, 0.0f); + pos += length * Random01(random) * NormalizeSafe(n); + EmitterStoreData(matrix, state.emitterScale, ps, q, pos, n, random, m_RandomDirection); + } + break; + } + case kBox: + { + const Vector3f extents (0.5f * m_BoxX, 0.5f * m_BoxY, 0.5f * m_BoxZ); + for (int q = fromIndex; q < count; ++q) + { + Vector3f pos = RandomPointInsideCube (random, extents); + Vector3f n = Vector3f::zAxis; + EmitterStoreData(matrix, state.emitterScale, ps, q, pos, n, random, m_RandomDirection); + } + } + break; + default: + { + DebugAssert(0 && "Shape not supported"); + } + } + } +} + +void ShapeModule::CalculateProceduralBounds(MinMaxAABB& bounds, const Vector3f& emitterScale, Vector2f minMaxBounds) const +{ + DebugAssert(minMaxBounds.x <= minMaxBounds.y); + + switch(m_Type) + { + case kSphere: + case kSphereShell: + bounds.m_Max = Vector3f(m_Radius, m_Radius, m_Radius); + bounds.m_Min = -bounds.m_Max; + break; + case kHemiSphere: + case kHemiSphereShell: + bounds.m_Max = Vector3f(m_Radius, m_Radius, m_Radius); + bounds.m_Min = Vector3f(-m_Radius, -m_Radius, 0.0f); + break; + case kCone: + case kConeShell: + bounds.m_Max = Vector3f(m_Radius, m_Radius, 0.0f); + bounds.m_Min = -bounds.m_Max; + break; + case kConeVolume: + case kConeVolumeShell: + { + const float a = Deg2Rad (m_Angle); + const float coneRadius2 = m_Radius + m_Length * Sin (a); + const float coneLength = m_Length * Cos (a); + bounds.m_Max = Vector3f(coneRadius2, coneRadius2, coneLength); + bounds.m_Min = -Vector3f(coneRadius2, coneRadius2, 0.0f); + break; + } + case kBox: + bounds.m_Max = Vector3f(m_BoxX, m_BoxY, m_BoxZ) * 0.5f; + bounds.m_Min = -bounds.m_Max; + break; + case kMesh: + { + if(m_CachedMesh) + bounds = m_CachedMesh->GetBounds(0); + else + bounds = MinMaxAABB(Vector3f::zero, Vector3f::zero); + break; + } + default: + { + AssertBreak(!"Shape not implemented."); + } + } + + bounds.m_Min = Scale(bounds.m_Min, emitterScale); + bounds.m_Max = Scale(bounds.m_Max, emitterScale); + + MinMaxAABB speedBounds; + + // Cone and cone shell random direction only deviate inside the bound + if(m_RandomDirection && (m_Type != kCone) && (m_Type != kConeShell)) + { + speedBounds.m_Max = Vector3f::one; + speedBounds.m_Min = -Vector3f::one; + minMaxBounds = Abs(minMaxBounds); + } + else + { + switch(m_Type) + { + case kSphere: + case kSphereShell: + case kMesh: + speedBounds.m_Max = Vector3f::one; + speedBounds.m_Min = -Vector3f::one; + break; + case kHemiSphere: + case kHemiSphereShell: + speedBounds.m_Max = Vector3f::one; + speedBounds.m_Min = Vector3f(-1.0f, -1.0f, 0.0f); + break; + case kCone: + case kConeShell: + case kConeVolume: + case kConeVolumeShell: + { + const float a = Deg2Rad (m_Angle); + const float sinA = Sin (a); + speedBounds.m_Max = Vector3f(sinA, sinA, 1.0f); + speedBounds.m_Min = Vector3f(-sinA, -sinA, 0.0f); + break; + } + case kBox: + speedBounds.m_Max = Vector3f::zAxis; + speedBounds.m_Min = Vector3f::zero; + break; + default: + { + AssertBreak(!"Shape not implemented."); + } + } + } + + MinMaxAABB speedBound; + speedBound.m_Min = bounds.m_Min + speedBounds.m_Min * minMaxBounds.y; + speedBound.m_Max = bounds.m_Max + speedBounds.m_Max * minMaxBounds.y; + bounds.Encapsulate(speedBound); + + MinMaxAABB negSpeedBound; + negSpeedBound.m_Min = speedBounds.m_Min * minMaxBounds.x; + negSpeedBound.m_Max = speedBounds.m_Max * minMaxBounds.x; + speedBound.m_Min = min(negSpeedBound.m_Min, negSpeedBound.m_Max); + speedBound.m_Max = max(negSpeedBound.m_Min, negSpeedBound.m_Max); + bounds.Encapsulate(speedBound); +} + +void ShapeModule::CheckConsistency () +{ + m_Type = clamp<int> (m_Type, kSphere, kMax-1); + m_PlacementMode = clamp<int> (m_PlacementMode, kVertex, kModeMax-1); + + m_Angle = clamp(m_Angle, 0.0f, 90.0f); + m_Radius = max(0.01f, m_Radius); + m_Length = max(0.0f, m_Length); + m_BoxX = max(0.0f, m_BoxX); + m_BoxY = max(0.0f, m_BoxY); + m_BoxZ = max(0.0f, m_BoxZ); +} + +void ShapeModule::AwakeFromLoad (ParticleSystem* system, const ParticleSystemReadOnlyState& roState) +{ + m_MeshNode.RemoveFromList(); + m_MeshNode.SetData(system); + m_CachedMesh = m_Mesh; + if (m_CachedMesh != NULL) + m_CachedMesh->AddObjectUser( m_MeshNode ); + DidModifyMeshData(); + + ResetSeed(roState); +} + +void ShapeModule::ResetSeed(const ParticleSystemReadOnlyState& roState) +{ + if(roState.randomSeed == 0) + m_Random.SetSeed(GetGlobalRandomSeed ()); + else + m_Random.SetSeed(roState.randomSeed); +} + +void ShapeModule::DidDeleteMesh (ParticleSystem* system) +{ + m_CachedMesh = NULL; +} + +void ShapeModule::DidModifyMeshData () +{ + if (m_CachedMesh == NULL) + { + m_CachedTriangleData.resize_uninitialized(0); + m_CachedVertexData.resize_uninitialized(0); + m_CachedTotalTriangleArea = 0; + return; + } + + + const StrideIterator<Vector3f> vertexBuffer = m_CachedMesh->GetVertexBegin(); + const UInt16* indexBuffer = m_CachedMesh->GetSubMeshBuffer16(0); + const SubMesh& submesh = m_CachedMesh->GetSubMeshFast (0); + if (submesh.topology == kPrimitiveTriangleStripDeprecated) + { + const int numTriangles = CountTrianglesInStrip(indexBuffer, submesh.indexCount); + const int capacity = numTriangles * 3; + UNITY_TEMP_VECTOR(UInt16) tempIndices(capacity); + Destripify(indexBuffer, submesh.indexCount, &tempIndices[0], capacity); + m_CachedTriangleData.resize_uninitialized(numTriangles); + m_CachedTotalTriangleArea = BuildMeshAreaTable(m_CachedTriangleData.begin(), vertexBuffer, &tempIndices[0], numTriangles); + } + else if (submesh.topology == kPrimitiveTriangles) + { + const int numTriangles = submesh.indexCount/3; + m_CachedTriangleData.resize_uninitialized(numTriangles); + m_CachedTotalTriangleArea = BuildMeshAreaTable(m_CachedTriangleData.begin(), vertexBuffer, indexBuffer, numTriangles); + } + else + { + m_CachedMesh = NULL; + } + + // Optimization: This sorts so big triangles comes before small, which means finding the right triangle is faster + std::sort(m_CachedTriangleData.begin(), m_CachedTriangleData.begin() + m_CachedTriangleData.size(), CompareMeshTriangleData); + + // Cache vertices + const int vertexCount = m_CachedMesh->GetVertexCount(); + const StrideIterator<Vector3f> vertices = m_CachedMesh->GetVertexBegin(); + const StrideIterator<Vector3f> normals = m_CachedMesh->GetNormalBegin(); + const StrideIterator<ColorRGBA32> colors = m_CachedMesh->GetColorBegin(); + m_CachedVertexData.resize_uninitialized(vertexCount); + for(int i = 0; i < vertexCount; i++) + { + m_CachedVertexData[i].position = vertices[i]; + + if(!normals.IsNull()) + m_CachedVertexData[i].normal = normals[i]; + else + m_CachedVertexData[i].normal = Vector3f::zero; + + if(!colors.IsNull()) + m_CachedVertexData[i].color = colors[i]; + else + m_CachedVertexData[i].color = ColorRGBA32(0xffffffff); + } +} + +Rand& ShapeModule::GetRandom() +{ +#if UNITY_EDITOR + if(!IsWorldPlaying()) + return m_EditorRandom; + else +#endif + return m_Random; +} + +template<class TransferFunction> +void ShapeModule::Transfer (TransferFunction& transfer) +{ + transfer.SetVersion(2); + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_Type, "type"); + + // Primitive + transfer.Transfer(m_Radius, "radius"); + transfer.Transfer(m_Angle, "angle"); + transfer.Transfer(m_Length, "length"); + transfer.Transfer(m_BoxX, "boxX"); + transfer.Transfer(m_BoxY, "boxY"); + transfer.Transfer(m_BoxZ, "boxZ"); + + // Mesh + transfer.Transfer (m_PlacementMode, "placementMode"); + TRANSFER (m_Mesh); + + transfer.Transfer (m_RandomDirection, "randomDirection"); transfer.Align(); + + // In Unity 3.5 all cone emitters had random direction set to false, but behaved as if it was true + if(transfer.IsOldVersion(1)) + if(kCone == m_Type) + m_RandomDirection = true; +} + +INSTANTIATE_TEMPLATE_TRANSFER(ShapeModule) + diff --git a/Runtime/Graphics/ParticleSystem/Modules/ShapeModule.h b/Runtime/Graphics/ParticleSystem/Modules/ShapeModule.h new file mode 100644 index 0000000..1f95d33 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/ShapeModule.h @@ -0,0 +1,80 @@ +#ifndef SHURIKENMODULESHAPE_H +#define SHURIKENMODULESHAPE_H + +#include "Runtime/BaseClasses/BaseObject.h" +#include "ParticleSystemModule.h" +#include "Runtime/Math/Random/rand.h" +#include "Runtime/Utilities/LinkedList.h" + +struct MeshTriangleData +{ + float area; + UInt16 indices[3]; +}; + +struct ParticleSystemEmitterMeshVertex +{ + Vector3f position; + Vector3f normal; + ColorRGBA32 color; +}; + +class Mesh; + +class ShapeModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (ShapeModule) + ShapeModule (); + + enum MeshPlacementMode { kVertex, kEdge, kTriangle, kModeMax }; + enum { kSphere, kSphereShell, kHemiSphere, kHemiSphereShell, kCone, kBox, kMesh, kConeShell, kConeVolume, kConeVolumeShell, kMax }; + + void AwakeFromLoad (ParticleSystem* system, const ParticleSystemReadOnlyState& roState); + void ResetSeed(const ParticleSystemReadOnlyState& roState); + void DidModifyMeshData (); + void DidDeleteMesh (ParticleSystem* system); + + PPtr<Mesh> GetMeshEmitterShape () { return m_Mesh; } + + void Start (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const Matrix4x4f& matrix, size_t fromIndex, float t); + void CalculateProceduralBounds(MinMaxAABB& bounds, const Vector3f& emitterScale, Vector2f minMaxBounds) const; + void CheckConsistency (); + + inline void SetShapeType(int type) { m_Type = type; }; + inline void SetRadius(float radius) { m_Radius = radius; }; + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + Rand& GetRandom(); + + int m_Type; + + // Primitive stuff + float m_Radius; + float m_Angle; + float m_Length; + float m_BoxX; + float m_BoxY; + float m_BoxZ; + + // Mesh stuff + int m_PlacementMode; + PPtr<Mesh> m_Mesh; + Mesh* m_CachedMesh; + dynamic_array<ParticleSystemEmitterMeshVertex> m_CachedVertexData; + dynamic_array<MeshTriangleData> m_CachedTriangleData; + float m_CachedTotalTriangleArea; + ListNode<Object> m_MeshNode; + + bool m_RandomDirection; + Rand m_Random; +#if UNITY_EDITOR +public: + Rand m_EditorRandom; +#endif +}; + +#endif // SHURIKENMODULESHAPE_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/SizeByVelocityModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/SizeByVelocityModule.cpp new file mode 100644 index 0000000..21cd5df --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/SizeByVelocityModule.cpp @@ -0,0 +1,50 @@ +#include "UnityPrefix.h" +#include "SizeByVelocityModule.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemUtils.h" + +SizeBySpeedModule::SizeBySpeedModule () : ParticleSystemModule(false) +, m_Range (0.0f, 1.0f) +{} + +template<ParticleSystemCurveEvalMode mode> +void UpdateTpl(const MinMaxCurve& curve, const ParticleSystemParticles& ps, float* tempSize, size_t fromIndex, size_t toIndex, const Vector2f offsetScale) +{ + for (size_t q = fromIndex; q < toIndex; ++q) + { + const Vector3f vel = ps.velocity[q] + ps.animatedVelocity[q]; + const float t = InverseLerpFast01 (offsetScale, Magnitude (vel)); + const float random = GenerateRandom(ps.randomSeed[q] + kParticleSystemSizeBySpeedCurveId); + tempSize[q] *= max<float> (0.0f, Evaluate<mode> (curve, t, random)); + } +} + +void SizeBySpeedModule::Update (const ParticleSystemParticles& ps, float* tempSize, size_t fromIndex, size_t toIndex) +{ + DebugAssert(toIndex <= ps.array_size ()); + Vector2f offsetScale = CalculateInverseLerpOffsetScale(m_Range); + if (m_Curve.minMaxState == kMMCScalar) + UpdateTpl<kEMScalar> (m_Curve, ps, tempSize, fromIndex, toIndex, offsetScale); + else if (m_Curve.IsOptimized() && m_Curve.UsesMinMax()) + UpdateTpl<kEMOptimizedMinMax> (m_Curve, ps, tempSize, fromIndex, toIndex, offsetScale); + else if(m_Curve.IsOptimized()) + UpdateTpl<kEMOptimized> (m_Curve, ps, tempSize, fromIndex, toIndex, offsetScale); + else + UpdateTpl<kEMSlow> (m_Curve, ps, tempSize, fromIndex, toIndex, offsetScale); +} + +void SizeBySpeedModule::CheckConsistency () +{ + const float MyEpsilon = 0.001f; + m_Range.x = std::min (m_Range.x, m_Range.y - MyEpsilon); +} + +template<class TransferFunction> +void SizeBySpeedModule::Transfer (TransferFunction& transfer) +{ + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_Curve, "curve"); + transfer.Transfer (m_Range, "range"); +} +INSTANTIATE_TEMPLATE_TRANSFER(SizeBySpeedModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/SizeByVelocityModule.h b/Runtime/Graphics/ParticleSystem/Modules/SizeByVelocityModule.h new file mode 100644 index 0000000..c8ad729 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/SizeByVelocityModule.h @@ -0,0 +1,27 @@ +#ifndef SHURIKENMODULESIZEBYVELOCITY_H +#define SHURIKENMODULESIZEBYVELOCITY_H + +#include "ParticleSystemModule.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemCurves.h" +#include "Runtime/Math/Vector2.h" + +class SizeBySpeedModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (SizeBySpeedModule) + SizeBySpeedModule (); + + void Update (const ParticleSystemParticles& ps, float* tempSize, size_t fromIndex, size_t toIndex); + void CheckConsistency (); + + inline MinMaxCurve& GetCurve() { return m_Curve; } + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + MinMaxCurve m_Curve; + Vector2f m_Range; +}; + +#endif // SHURIKENMODULESIZEBYVELOCITY_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/SizeModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/SizeModule.cpp new file mode 100644 index 0000000..ad08a4a --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/SizeModule.cpp @@ -0,0 +1,41 @@ +#include "UnityPrefix.h" +#include "SizeModule.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "Runtime/Math/Random/Random.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemUtils.h" + +template<ParticleSystemCurveEvalMode mode> +void UpdateTpl(const MinMaxCurve& curve, const ParticleSystemParticles& ps, float* tempSize, size_t fromIndex, size_t toIndex) +{ + for (size_t q = fromIndex; q < toIndex; ++q) + { + const float time = NormalizedTime(ps, q); + const float random = GenerateRandom(ps.randomSeed[q] + kParticleSystemSizeCurveId); + tempSize[q] *= max<float>(0.0f, Evaluate<mode> (curve, time, random)); + } +} + +SizeModule::SizeModule() : ParticleSystemModule(false) +{} + +void SizeModule::Update (const ParticleSystemParticles& ps, float* tempSize, size_t fromIndex, size_t toIndex) +{ + DebugAssert(toIndex <= ps.array_size ()); + if(m_Curve.minMaxState == kMMCScalar) + UpdateTpl<kEMScalar>(m_Curve, ps, tempSize, fromIndex, toIndex); + else if (m_Curve.IsOptimized() && m_Curve.UsesMinMax ()) + UpdateTpl<kEMOptimizedMinMax>(m_Curve, ps, tempSize, fromIndex, toIndex); + else if(m_Curve.IsOptimized()) + UpdateTpl<kEMOptimized>(m_Curve, ps, tempSize, fromIndex, toIndex); + else + UpdateTpl<kEMSlow>(m_Curve, ps, tempSize, fromIndex, toIndex); +} + +template<class TransferFunction> +void SizeModule::Transfer (TransferFunction& transfer) +{ + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_Curve, "curve"); +} +INSTANTIATE_TEMPLATE_TRANSFER(SizeModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/SizeModule.h b/Runtime/Graphics/ParticleSystem/Modules/SizeModule.h new file mode 100644 index 0000000..3c92634 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/SizeModule.h @@ -0,0 +1,27 @@ +#ifndef SHURIKENMODULESIZE_H +#define SHURIKENMODULESIZE_H + +#include "ParticleSystemModule.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemCurves.h" + +class SizeModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (SizeModule) + + SizeModule(); + + void Update (const ParticleSystemParticles& ps, float* tempSize, size_t fromIndex, size_t toIndex); + + void CheckConsistency () {}; + + inline MinMaxCurve& GetCurve() { return m_Curve; } + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + MinMaxCurve m_Curve; +}; + +#endif // SHURIKENMODULESIZE_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/SubModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/SubModule.cpp new file mode 100644 index 0000000..895c444 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/SubModule.cpp @@ -0,0 +1,148 @@ +#include "UnityPrefix.h" +#include "SubModule.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "../ParticleSystemUtils.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystem.h" // Only because of PPtr comparison + +bool IsUsingSubEmitter(ParticleSystem* emitter) +{ + const void* shurikenSelf = NULL; + return emitter != shurikenSelf; +} + +SubModule::SubModule () : ParticleSystemModule(false) +{ +} + +void SubModule::Update (const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, size_t fromIndex, size_t toIndex, float dt) const +{ + Assert(state.numCachedSubDataBirth <= kParticleSystemMaxNumEmitAccumulators); + for(int i = 0; i < state.numCachedSubDataBirth; i++) + { + const ParticleSystemSubEmitterData& data = state.cachedSubDataBirth[i]; + //const bool culled = (data.maxLifetime < state.accumulatedDt); + const float startDelay = data.startDelayInSec; + const float length = data.lengthInSec; + for (int q = fromIndex; q < toIndex; ++q) + { + const float t = std::max(0.0f, ps.startLifetime[q] - ps.lifetime[q] - dt) - startDelay; + const bool started = (t >= 0.0f); + const bool ended = (t >= length); + if(started && !ended) + { + ParticleSystemEmissionState emissionState; + emissionState.m_ToEmitAccumulator = ps.emitAccumulator[i][q]; + RecordEmit(emissionState, data, roState, state, ps, kParticleSystemSubTypeBirth, i, q, t, dt, length); + ps.emitAccumulator[i][q] = emissionState.m_ToEmitAccumulator; + } + } + } +} + +void SubModule::RemoveDuplicatePtrs (ParticleSystem** shurikens) +{ + for(int i = 0; i < kParticleSystemMaxSubTotal-1; i++) + for(int j = i+1; j < kParticleSystemMaxSubTotal; j++) + if(shurikens[i] && (shurikens[i] == shurikens[j])) + shurikens[i] = NULL; +} + +// This can not be cached between frames since the referenced particle systems might be deleted at any point. +int SubModule::GetSubEmitterPtrs (ParticleSystem** subEmitters) const +{ + for(int i = 0; i < kParticleSystemMaxSubTotal; i++) + subEmitters[i] = NULL; + + int numSubEmitters = 0; + for(int i = 0; i < kParticleSystemMaxSubBirth; i++) + { + ParticleSystem* subParticleSystem = m_SubEmittersBirth[i]; + if (IsUsingSubEmitter(subParticleSystem)) + subEmitters[numSubEmitters++] = subParticleSystem; + } + for(int i = 0; i < kParticleSystemMaxSubCollision; i++) + { + ParticleSystem* subParticleSystem = m_SubEmittersCollision[i]; + if (IsUsingSubEmitter(subParticleSystem)) + subEmitters[numSubEmitters++] = subParticleSystem; + } + for(int i = 0; i < kParticleSystemMaxSubDeath; i++) + { + ParticleSystem* subParticleSystem = m_SubEmittersDeath[i]; + if (IsUsingSubEmitter(subParticleSystem)) + subEmitters[numSubEmitters++] = subParticleSystem; + } + return numSubEmitters; +} + +int SubModule::GetSubEmitterPtrsBirth (ParticleSystem** subEmitters) const +{ + int numSubEmitters = 0; + for(int i = 0; i < kParticleSystemMaxSubBirth; i++) + { + ParticleSystem* subParticleSystem = m_SubEmittersBirth[i]; + if (IsUsingSubEmitter(subParticleSystem)) + subEmitters[numSubEmitters++] = subParticleSystem; + } + return numSubEmitters; +} + +int SubModule::GetSubEmitterPtrsCollision (ParticleSystem** subEmitters) const +{ + int numSubEmitters = 0; + for(int i = 0; i < kParticleSystemMaxSubCollision; i++) + { + ParticleSystem* subParticleSystem = m_SubEmittersCollision[i]; + if (IsUsingSubEmitter(subParticleSystem)) + subEmitters[numSubEmitters++] = subParticleSystem; + } + return numSubEmitters; +} + +int SubModule::GetSubEmitterPtrsDeath(ParticleSystem** subEmitters) const +{ + int numSubEmitters = 0; + for(int i = 0; i < kParticleSystemMaxSubDeath; i++) + { + ParticleSystem* subParticleSystem = m_SubEmittersDeath[i]; + if (IsUsingSubEmitter(subParticleSystem)) + subEmitters[numSubEmitters++] = subParticleSystem; + } + return numSubEmitters; +} + +int SubModule::GetSubEmitterTypeCount(ParticleSystemSubType type) const +{ + int count = 0; + const void* shurikenSelf = NULL; + if(type == kParticleSystemSubTypeBirth) { + for(int i = 0; i < kParticleSystemMaxSubBirth; i++) + if(m_SubEmittersBirth[i] != shurikenSelf) + count++; + } else if(type == kParticleSystemSubTypeCollision) { + for(int i = 0; i < kParticleSystemMaxSubCollision; i++) + if(m_SubEmittersCollision[i] != shurikenSelf) + count++; + } else if(type == kParticleSystemSubTypeDeath) { + for(int i = 0; i < kParticleSystemMaxSubDeath; i++) + if(m_SubEmittersDeath[i] != shurikenSelf) + count++; + } else { + Assert(!"Sub emitter type not implemented."); + } + return count; +} + +template<class TransferFunction> +void SubModule::Transfer (TransferFunction& transfer) +{ + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_SubEmittersBirth[0], "subEmitterBirth"); + transfer.Transfer (m_SubEmittersBirth[1], "subEmitterBirth1"); + transfer.Transfer (m_SubEmittersCollision[0], "subEmitterCollision"); + transfer.Transfer (m_SubEmittersCollision[1], "subEmitterCollision1"); + transfer.Transfer (m_SubEmittersDeath[0], "subEmitterDeath"); + transfer.Transfer (m_SubEmittersDeath[1], "subEmitterDeath1"); +} +INSTANTIATE_TEMPLATE_TRANSFER(SubModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/SubModule.h b/Runtime/Graphics/ParticleSystem/Modules/SubModule.h new file mode 100644 index 0000000..093c8ba --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/SubModule.h @@ -0,0 +1,38 @@ +#ifndef SHURIKENMODULESUB_H +#define SHURIKENMODULESUB_H + +#include "ParticleSystemModule.h" +#include "Runtime/BaseClasses/BaseObject.h" + +class ParticleSystem; +struct ParticleSystemParticles; + + +class SubModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (SubModule) + SubModule (); + + void Update (const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, size_t fromIndex, size_t toIndex, float dt) const; + void CheckConsistency() {}; + + int GetSubEmitterTypeCount(ParticleSystemSubType type) const; + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + + static void RemoveDuplicatePtrs (ParticleSystem** shurikens); + int GetSubEmitterPtrs (ParticleSystem** shurikens) const; + int GetSubEmitterPtrsBirth (ParticleSystem** shurikens) const; + int GetSubEmitterPtrsCollision (ParticleSystem** shurikens) const; + int GetSubEmitterPtrsDeath(ParticleSystem** shurikens) const; + + +private: + PPtr<ParticleSystem> m_SubEmittersBirth[kParticleSystemMaxSubBirth]; + PPtr<ParticleSystem> m_SubEmittersCollision[kParticleSystemMaxSubCollision]; + PPtr<ParticleSystem> m_SubEmittersDeath[kParticleSystemMaxSubDeath]; +}; + +#endif // SHURIKENMODULESUB_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/UVModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/UVModule.cpp new file mode 100644 index 0000000..67d49b3 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/UVModule.cpp @@ -0,0 +1,105 @@ +#include "UnityPrefix.h" +#include "UVModule.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemParticle.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemCurves.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemUtils.h" +#include "Runtime/Math/Random/Random.h" + +template<ParticleSystemCurveEvalMode mode> +void UpdateWholeSheetTpl(float cycles, const MinMaxCurve& curve, const ParticleSystemParticles& ps, float* tempSheetIndex, size_t fromIndex, size_t toIndex) +{ + for (size_t q = fromIndex; q < toIndex; ++q) + tempSheetIndex[q] = Repeat (cycles * Evaluate(curve, NormalizedTime(ps, q), GenerateRandom(ps.randomSeed[q] + kParticleSystemUVCurveId)), 1.0f); +} + +UVModule::UVModule () : ParticleSystemModule(false) +, m_TilesX (1), m_TilesY (1) +, m_AnimationType (kWholeSheet) +, m_RowIndex (0) +, m_Cycles (1.0f) +, m_RandomRow (true) +{} + +void UVModule::Update (const ParticleSystemParticles& ps, float* tempSheetIndex, size_t fromIndex, size_t toIndex) +{ + const float cycles = m_Cycles; + + DebugAssert(toIndex <= ps.array_size ()); + if (m_AnimationType == kSingleRow) // row + { + int rows = m_TilesY; + float animRange = (1.0f / (m_TilesX * rows)) * m_TilesX; + if(m_RandomRow) + { + for (size_t q = fromIndex; q < toIndex; ++q) + { + const float t = cycles * Evaluate(m_Curve, NormalizedTime(ps, q), GenerateRandom(ps.randomSeed[q] + kParticleSystemUVCurveId)); + const float x = Repeat (t, 1.0f); + const float randomValue = GenerateRandom(ps.randomSeed[q] + kParticleSystemUVRowSelectionId); + const float startRow = Floorf (randomValue * rows); + float from = startRow * animRange; + float to = from + animRange; + tempSheetIndex[q] = Lerp (from, to, x); + } + } + else + { + const float startRow = Floorf(m_RowIndex * animRange * rows); + float from = startRow * animRange; + float to = from + animRange; + for (size_t q = fromIndex; q < toIndex; ++q) + { + const float t = cycles * Evaluate(m_Curve, NormalizedTime(ps, q), GenerateRandom(ps.randomSeed[q] + kParticleSystemUVCurveId)); + const float x = Repeat (t, 1.0f); + tempSheetIndex[q] = Lerp (from, to, x); + } + } + } + else if (m_AnimationType == kWholeSheet) // grid || row + { + if(m_Curve.minMaxState == kMMCScalar) + UpdateWholeSheetTpl<kEMScalar>(m_Cycles, m_Curve, ps, tempSheetIndex, fromIndex, toIndex); + else if (m_Curve.IsOptimized() && m_Curve.UsesMinMax ()) + UpdateWholeSheetTpl<kEMOptimizedMinMax>(m_Cycles, m_Curve, ps, tempSheetIndex, fromIndex, toIndex); + else if(m_Curve.IsOptimized()) + UpdateWholeSheetTpl<kEMOptimized>(m_Cycles, m_Curve, ps, tempSheetIndex, fromIndex, toIndex); + else + UpdateWholeSheetTpl<kEMSlow>(m_Cycles, m_Curve, ps, tempSheetIndex, fromIndex, toIndex); + } + else + { + Assert(!"Animation mode not implemented!"); + } +} + +void UVModule::CheckConsistency () +{ + m_AnimationType = clamp<int> (m_AnimationType, 0, kNumAnimationTypes-1); + m_TilesX = std::max<int> (1, m_TilesX); + m_TilesY = std::max<int> (1, m_TilesY); + m_Cycles = std::max<int> (1, (int)m_Cycles); + m_RowIndex = clamp<int> (m_RowIndex, 0, m_TilesY-1); + m_Curve.SetScalar(clamp<float> (m_Curve.GetScalar(), 0.0f, 1.0f)); +} + +void UVModule::GetNumTiles(int& uvTilesX, int& uvTilesY) const +{ + uvTilesX = m_TilesX; + uvTilesY = m_TilesY; +} + +template<class TransferFunction> +void UVModule::Transfer (TransferFunction& transfer) +{ + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_Curve, "frameOverTime"); + transfer.Transfer (m_TilesX, "tilesX"); + transfer.Transfer (m_TilesY, "tilesY"); + transfer.Transfer (m_AnimationType, "animationType"); + transfer.Transfer (m_RowIndex, "rowIndex"); + transfer.Transfer (m_Cycles, "cycles"); + transfer.Transfer (m_RandomRow, "randomRow"); transfer.Align (); +} +INSTANTIATE_TEMPLATE_TRANSFER(UVModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/UVModule.h b/Runtime/Graphics/ParticleSystem/Modules/UVModule.h new file mode 100644 index 0000000..5ed9e7f --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/UVModule.h @@ -0,0 +1,36 @@ +#ifndef SHURIKENMODULEUV_H +#define SHURIKENMODULEUV_H + +#include "ParticleSystemModule.h" +#include "Runtime/Graphics/ParticleSystem/PolynomialCurve.h" + +struct ParticleSystemParticles; + +class UVModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (UVModule) + UVModule (); + + void Update (const ParticleSystemParticles& ps, float* tempSheetIndex, size_t fromIndex, size_t toIndex); + void CheckConsistency (); + + inline MinMaxCurve& GetCurve() { return m_Curve; } + + void GetNumTiles(int& uvTilesX, int& uvTilesY) const; + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + enum { kWholeSheet, kSingleRow, kNumAnimationTypes }; + + MinMaxCurve m_Curve; + int m_TilesX, m_TilesY; + int m_AnimationType; + int m_RowIndex; + float m_Cycles; + bool m_RandomRow; +}; + +#endif // SHURIKENMODULEUV_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/VelocityModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/VelocityModule.cpp new file mode 100644 index 0000000..edb7ed9 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/VelocityModule.cpp @@ -0,0 +1,127 @@ +#include "UnityPrefix.h" +#include "VelocityModule.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "../ParticleSystemUtils.h" +#include "Runtime/Math/Random/Random.h" +#include "Runtime/Math/Matrix4x4.h" + +template<ParticleSystemCurveEvalMode mode> +void UpdateTpl(const MinMaxCurve& x, const MinMaxCurve& y, const MinMaxCurve& z, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex, bool transform, const Matrix4x4f& matrix) +{ + for (size_t q = fromIndex; q < toIndex; ++q) + { + Vector3f random; + GenerateRandom3(random, ps.randomSeed[q] + kParticleSystemVelocityCurveId); + + const float normalizedTime = NormalizedTime (ps, q); + Vector3f vel = Vector3f (Evaluate<mode> (x, normalizedTime, random.x), Evaluate<mode> (y, normalizedTime, random.y), Evaluate<mode> (z, normalizedTime, random.z)); + if(transform) + vel = matrix.MultiplyVector3 (vel); + ps.animatedVelocity[q] += vel; + } +} + +template<bool isOptimized> +void UpdateProceduralTpl(const DualMinMax3DPolyCurves& curves, const MinMaxCurve& x, const MinMaxCurve& y, const MinMaxCurve& z, ParticleSystemParticles& ps, const Matrix4x4f& matrix, bool transform) +{ + const size_t count = ps.array_size (); + for (int q=0;q<count;q++) + { + Vector3f random; + GenerateRandom3(random, ps.randomSeed[q] + kParticleSystemVelocityCurveId); + const float time = NormalizedTime(ps, q); + + Vector3f delta; + if(isOptimized) + delta = Vector3f(EvaluateIntegrated(curves.optX, time, random.x), EvaluateIntegrated(curves.optY, time, random.y), EvaluateIntegrated(curves.optZ, time, random.z)) * ps.startLifetime[q]; + else + delta = Vector3f(EvaluateIntegrated(curves.x, time, random.x), EvaluateIntegrated(curves.y, time, random.y), EvaluateIntegrated(curves.z, time, random.z)) * ps.startLifetime[q]; + + Vector3f velocity (Evaluate(x, time, random.x), Evaluate(y, time, random.y), Evaluate(z, time, random.z)); + if(transform) + { + delta = matrix.MultiplyVector3 (delta); + velocity = matrix.MultiplyVector3 (velocity); + } + ps.position[q] += delta; + ps.animatedVelocity[q] += velocity; + } +} + +VelocityModule::VelocityModule () : ParticleSystemModule(false) +, m_InWorldSpace (false) +{} + +void VelocityModule::Update (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex) +{ + Matrix4x4f matrix; + bool transform = GetTransformationMatrix(matrix, !roState.useLocalSpace, m_InWorldSpace, state.localToWorld); + + bool usesScalar = (m_X.minMaxState == kMMCScalar) && (m_Y.minMaxState == kMMCScalar) && (m_Z.minMaxState == kMMCScalar); + bool isOptimized = m_X.IsOptimized() && m_Y.IsOptimized() && m_Z.IsOptimized(); + bool usesMinMax = m_X.UsesMinMax() && m_Y.UsesMinMax() && m_Z.UsesMinMax(); + if(usesScalar) + UpdateTpl<kEMScalar>(m_X, m_Y, m_Z, ps, fromIndex, toIndex, transform, matrix); + else if(isOptimized && usesMinMax) + UpdateTpl<kEMOptimizedMinMax>(m_X, m_Y, m_Z, ps, fromIndex, toIndex, transform, matrix); + else if(isOptimized) + UpdateTpl<kEMOptimized>(m_X, m_Y, m_Z, ps, fromIndex, toIndex, transform, matrix); + else + UpdateTpl<kEMSlow>(m_X, m_Y, m_Z, ps, fromIndex, toIndex, transform, matrix); + +} + +void VelocityModule::UpdateProcedural (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps) +{ + Matrix4x4f matrix; + bool transform = GetTransformationMatrix(matrix, !roState.useLocalSpace, m_InWorldSpace, state.localToWorld); + + DualMinMax3DPolyCurves curves; + if(m_X.IsOptimized() && m_Y.IsOptimized() && m_Z.IsOptimized()) + { + curves.optX = m_X.polyCurves; curves.optX.Integrate(); + curves.optY = m_Y.polyCurves; curves.optY.Integrate(); + curves.optZ = m_Z.polyCurves; curves.optZ.Integrate(); + UpdateProceduralTpl<true>(curves, m_X, m_Y, m_Z, ps, matrix, transform); + } + else + { + DebugAssert(CurvesSupportProcedural (m_X.editorCurves, m_X.minMaxState)); + DebugAssert(CurvesSupportProcedural (m_Y.editorCurves, m_Y.minMaxState)); + DebugAssert(CurvesSupportProcedural (m_Z.editorCurves, m_Z.minMaxState)); + BuildCurves(curves.x, m_X.editorCurves, m_X.GetScalar(), m_X.minMaxState); curves.x.Integrate(); + BuildCurves(curves.y, m_Y.editorCurves, m_Y.GetScalar(), m_Y.minMaxState); curves.y.Integrate(); + BuildCurves(curves.z, m_Z.editorCurves, m_Z.GetScalar(), m_Z.minMaxState); curves.z.Integrate(); + UpdateProceduralTpl<false>(curves, m_X, m_Y, m_Z, ps, matrix, transform); + } +} + +void VelocityModule::CalculateProceduralBounds(MinMaxAABB& bounds, const Matrix4x4f& localToWorld, float maxLifeTime) +{ + Vector2f xRange = m_X.FindMinMaxIntegrated(); + Vector2f yRange = m_Y.FindMinMaxIntegrated(); + Vector2f zRange = m_Z.FindMinMaxIntegrated(); + bounds.m_Min = Vector3f(xRange.x, yRange.x, zRange.x) * maxLifeTime; + bounds.m_Max = Vector3f(xRange.y, yRange.y, zRange.y) * maxLifeTime; + if(m_InWorldSpace) + { + Matrix4x4f matrix; + Matrix4x4f::Invert_General3D(localToWorld, matrix); + matrix.SetPosition(Vector3f::zero); + AABB aabb = bounds; + TransformAABBSlow(aabb, matrix, aabb); + bounds = aabb; + } +} + +template<class TransferFunction> +void VelocityModule::Transfer (TransferFunction& transfer) +{ + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_X, "x"); + transfer.Transfer (m_Y, "y"); + transfer.Transfer (m_Z, "z"); + transfer.Transfer (m_InWorldSpace, "inWorldSpace"); transfer.Align(); +} +INSTANTIATE_TEMPLATE_TRANSFER(VelocityModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/VelocityModule.h b/Runtime/Graphics/ParticleSystem/Modules/VelocityModule.h new file mode 100644 index 0000000..bf76f28 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/VelocityModule.h @@ -0,0 +1,36 @@ +#ifndef SHURIKENMODULEVELOCITY_H +#define SHURIKENMODULEVELOCITY_H + +#include "ParticleSystemModule.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemCurves.h" +#include "Runtime/Math/Random/rand.h" + +class VelocityModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (VelocityModule) + VelocityModule (); + + void Update (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex); + void UpdateProcedural (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps); + void CalculateProceduralBounds(MinMaxAABB& bounds, const Matrix4x4f& localToWorld, float maxLifeTime); + void CheckConsistency() {}; + + inline MinMaxCurve& GetXCurve() { return m_X; }; + inline MinMaxCurve& GetYCurve() { return m_Y; }; + inline MinMaxCurve& GetZCurve() { return m_Z; }; + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + MinMaxCurve m_X; + MinMaxCurve m_Y; + MinMaxCurve m_Z; + bool m_InWorldSpace; + + Rand m_Random; +}; + + +#endif // SHURIKENMODULEVELOCITY_H diff --git a/Runtime/Graphics/ParticleSystem/ParticleCollisionEvents.cpp b/Runtime/Graphics/ParticleSystem/ParticleCollisionEvents.cpp new file mode 100644 index 0000000..dfefc6a --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleCollisionEvents.cpp @@ -0,0 +1,122 @@ +#include "UnityPrefix.h" +#include "ParticleCollisionEvents.h" + +#include "Runtime/Scripting/ScriptingUtility.h" +#include "Runtime/Threads/Thread.h" + +CollisionEvents::CollisionEvents (): currentCollisionEventThreadArray(0) +{ +} + +void CollisionEvents::Clear () +{ + collisionEvents[0].clear (); + collisionEvents[1].clear (); +} + +bool CollisionEvents::AddEvent (const ParticleCollisionEvent& event) +{ + GetCollisionEventThreadArray ().push_back (event); + return true; +} + +int CollisionEvents::GetCollisionEventCount () const +{ + return GetCollisionEventScriptArray ().size (); +} + +void CollisionEvents::SwapCollisionEventArrays () +{ + Assert (Thread::CurrentThreadIsMainThread ()); + currentCollisionEventThreadArray = (currentCollisionEventThreadArray+1)%2; + collisionEvents[currentCollisionEventThreadArray].clear (); +} + +dynamic_array<ParticleCollisionEvent>& CollisionEvents::GetCollisionEventThreadArray () +{ + return collisionEvents[currentCollisionEventThreadArray]; +} + +const dynamic_array<ParticleCollisionEvent>& CollisionEvents::GetCollisionEventScriptArray () const +{ + const int currentCollisionEventScriptArray = (currentCollisionEventThreadArray+1)%2; + return collisionEvents[currentCollisionEventScriptArray]; +} + +struct SortCollisionEventsByGameObject { + bool operator()( const ParticleCollisionEvent& ra, const ParticleCollisionEvent& rb ) const; +}; + +bool SortCollisionEventsByGameObject::operator()( const ParticleCollisionEvent& ra, const ParticleCollisionEvent& rb ) const +{ + return ra.m_RigidBodyOrColliderInstanceID < rb.m_RigidBodyOrColliderInstanceID; +} + +void CollisionEvents::SortCollisionEventThreadArray () +{ + std::sort (GetCollisionEventThreadArray ().begin (), GetCollisionEventThreadArray ().end (), SortCollisionEventsByGameObject ()); +} + +static GameObject* GetGameObjectFromInstanceID (int instanceId) +{ + Object* temp = Object::IDToPointer (instanceId); + if ( temp ) + { + return (reinterpret_cast<Unity::Component*> (temp))->GetGameObjectPtr (); + } + return NULL; +} + +static int GetGameObjectIDFromInstanceID (int instanceId) +{ + Object* temp = Object::IDToPointer (instanceId); + if ( temp ) + { + return (reinterpret_cast<Unity::Component*> (temp))->GetGameObjectInstanceID (); + } + return 0; +} + +void CollisionEvents::SendCollisionEvents (Unity::Component& particleSystem) const +{ + const dynamic_array<ParticleCollisionEvent>& scriptEventArray = GetCollisionEventScriptArray (); + GameObject* pParticleSystem = &particleSystem.GetGameObject (); + GameObject* collideeGO = NULL; + int currentId = -1; + for (int e = 0; e < scriptEventArray.size (); ++e) + { + if (currentId != scriptEventArray[e].m_RigidBodyOrColliderInstanceID) + { + collideeGO = GetGameObjectFromInstanceID (scriptEventArray[e].m_RigidBodyOrColliderInstanceID); + if (!collideeGO) + continue; + currentId = scriptEventArray[e].m_RigidBodyOrColliderInstanceID; + pParticleSystem->SendMessage (kParticleCollisionEvent, collideeGO, ClassID (GameObject)); // send message to particle system + collideeGO->SendMessage (kParticleCollisionEvent, pParticleSystem, ClassID (GameObject)); // send message to object collided with + } + } +} + +int CollisionEvents::GetCollisionEvents (int instanceId, MonoParticleCollisionEvent* collisionEvents, int size) const +{ + const dynamic_array<ParticleCollisionEvent>& scriptEventArray = GetCollisionEventScriptArray (); + dynamic_array<ParticleCollisionEvent>::const_iterator iter = scriptEventArray.begin (); + for (; iter != scriptEventArray.end (); ++iter) + { + if (instanceId == GetGameObjectIDFromInstanceID (iter->m_RigidBodyOrColliderInstanceID)) + { + int count = 0; + while (iter != scriptEventArray.end () && GetGameObjectIDFromInstanceID (iter->m_RigidBodyOrColliderInstanceID) == instanceId && count < size) + { + collisionEvents[count].m_Intersection = iter->m_Intersection; + collisionEvents[count].m_Normal = iter->m_Normal; + collisionEvents[count].m_Velocity = iter->m_Velocity; + collisionEvents[count].m_ColliderInstanceID = iter->m_ColliderInstanceID; + count++; + iter++; + } + return count; + } + } + return 0; +} diff --git a/Runtime/Graphics/ParticleSystem/ParticleCollisionEvents.h b/Runtime/Graphics/ParticleSystem/ParticleCollisionEvents.h new file mode 100644 index 0000000..98260ee --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleCollisionEvents.h @@ -0,0 +1,42 @@ +#pragma once + +#include "Runtime/BaseClasses/GameObject.h" +#include "Runtime/Math/Vector3.h" +#include "Runtime/Mono/MonoScript.h" +#include "Runtime/Utilities/dynamic_array.h" + + +struct ParticleCollisionEvent +{ + ParticleCollisionEvent (const Vector3f& intersection, const Vector3f& normal, const Vector3f& velocity, int colliderInstanceID, int rigidBodyOrColliderInstanceID); + Vector3f m_Intersection; + Vector3f m_Normal; + Vector3f m_Velocity; + int m_ColliderInstanceID; + int m_RigidBodyOrColliderInstanceID; // This can be a Collider or a RigidBody component +}; + +struct MonoParticleCollisionEvent +{ + Vector3f m_Intersection; + Vector3f m_Normal; + Vector3f m_Velocity; + int m_ColliderInstanceID; +}; + +struct CollisionEvents +{ + CollisionEvents (); + dynamic_array<ParticleCollisionEvent> collisionEvents[2]; + int currentCollisionEventThreadArray; + + void Clear (); + bool AddEvent (const ParticleCollisionEvent& event); + int GetCollisionEventCount () const; + void SwapCollisionEventArrays (); + void SortCollisionEventThreadArray (); + void SendCollisionEvents (Unity::Component& particleSystem) const; + int GetCollisionEvents (int instanceId, MonoParticleCollisionEvent* collisionEvents, int size) const; + dynamic_array<ParticleCollisionEvent>& GetCollisionEventThreadArray (); + const dynamic_array<ParticleCollisionEvent>& GetCollisionEventScriptArray () const; +}; diff --git a/Runtime/Graphics/ParticleSystem/ParticleSystem.cpp b/Runtime/Graphics/ParticleSystem/ParticleSystem.cpp new file mode 100644 index 0000000..47e952f --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleSystem.cpp @@ -0,0 +1,2110 @@ +#include "UnityPrefix.h" +#include "ParticleSystem.h" +#include "ParticleSystemRenderer.h" +#include "ParticleSystemCurves.h" +#include "ParticleSystemUtils.h" +#include "Modules/ParticleSystemModule.h" +#include "Modules/InitialModule.h" +#include "Modules/ShapeModule.h" +#include "Modules/EmissionModule.h" +#include "Modules/SizeModule.h" +#include "Modules/RotationModule.h" +#include "Modules/ColorModule.h" +#include "Modules/UVModule.h" +#include "Modules/VelocityModule.h" +#include "Modules/ForceModule.h" +#include "Modules/ExternalForcesModule.h" +#include "Modules/ClampVelocityModule.h" +#include "Modules/SizeByVelocityModule.h" +#include "Modules/RotationByVelocityModule.h" +#include "Modules/ColorByVelocityModule.h" +#include "Modules/CollisionModule.h" +#include "Modules/SubModule.h" +#include "Runtime/Math/Color.h" +#include "Runtime/Math/Vector3.h" +#include "Runtime/Math/Matrix4x4.h" +#include "Runtime/Math/FloatConversion.h" +#include "Runtime/Math/Random/rand.h" +#include "Runtime/Math/Random/Random.h" +#include "Runtime/BaseClasses/IsPlaying.h" +#include "Runtime/Math/AnimationCurve.h" +#include "Runtime/Input/TimeManager.h" +#include "Runtime/Threads/JobScheduler.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "Runtime/Serialize/TransferFunctions/TransferNameConversions.h" +#include "Runtime/Shaders/GraphicsCaps.h" +#include "Runtime/Profiler/Profiler.h" +#include "Runtime/Threads/Thread.h" +#include "Runtime/Misc/GameObjectUtility.h" +#include "Runtime/Misc/ResourceManager.h" +#include "Runtime/Shaders/Material.h" +#include "Runtime/Misc/QualitySettings.h" +#include "Runtime/Misc/BuildSettings.h" +#include "Runtime/Core/Callbacks/GlobalCallbacks.h" + +#if UNITY_EDITOR +#include "Editor/Src/ParticleSystem/ParticleSystemEditor.h" +#include "Runtime/Mono/MonoManager.h" +#endif + +// P1: +// . Calling ps.Play() in Start(). Does it actually stsrt playing when game starts? No. + +// P2: +// Automatic Culling: +// . Improve procedural AABB +// . Gravity: make it work with transforming into local space the same way velocity and force modules work +// . Get tighter bounds by forces counteracting eachother instead of always expanding +// . For procedural systems, no gain in calculating AABB every frame. Just do it when something changes. +// . Solving for roots analytically +// . http://en.wikipedia.org/wiki/Cubic_function +// . http://en.wikipedia.org/wiki/Quartic_function +// . http://en.wikipedia.org/wiki/Quintic_function + +// External Forces: +// . Currently not using turbulence. Start using that? + +// Other: +// . Batching: Make it work with meshes as well +// . !!! Add runtime tests for playback code. Playing, stopping, simulating etc. Make sure it's 100%. + +struct ParticleSystemManager +{ + ParticleSystemManager() + :needSync(false) + { + activeEmitters.reserve(32); + } + + dynamic_array<ParticleSystem*> activeEmitters; + +#if ENABLE_MULTITHREADED_PARTICLES + JobScheduler::JobGroupID worldCollisionJobGroup; // group for particle systems performing world collisions, PhysX is currently not threadsafe so deleting objects in LateUpdate could cause crashes as that could overlap with collision testing + JobScheduler::JobGroupID jobGroup; // for any other collisions +#endif + bool needSync; +}; + +Material* GetDefaultParticleMaterial () +{ + #if WEBPLUG + if (!IS_CONTENT_NEWER_OR_SAME(kUnityVersion_OldWebResourcesAdded)) + return GetBuiltinOldWebResource<Material> ("Default-Particle.mat"); + #endif + + #if UNITY_EDITOR + return GetBuiltinExtraResource<Material> ("Default-Particle.mat"); + #endif + + // If someone happens to create a new particle system component at runtime, + // just don't assign any material. The default one might not even be included + // into the build. + return NULL; +} + +ParticleSystemManager gParticleSystemManager; + +PROFILER_INFORMATION(gParticleSystemProfile, "ParticleSystem.Update", kProfilerParticles) +PROFILER_INFORMATION(gParticleSystemPrewarm, "ParticleSystem.Prewarm", kProfilerParticles) +PROFILER_INFORMATION(gParticleSystemWait, "ParticleSystem.WaitForUpdateThreads", kProfilerParticles) +PROFILER_INFORMATION(gParticleSystemJobProfile, "ParticleSystem.UpdateJob", kProfilerParticles) + +PROFILER_INFORMATION(gParticleSystemUpdateCollisions, "ParticleSystem.UpdateCollisions", kProfilerParticles) + + +#define MAX_TIME_STEP (0.03f) +static float GetTimeStep(float dt, bool fixedTimeStep) +{ + if(fixedTimeStep) + return GetTimeManager().GetFixedDeltaTime(); + else if(dt > MAX_TIME_STEP) + return dt / Ceilf(dt / MAX_TIME_STEP); + else + return dt; +} + +static void ApplyStartDelay (float& delayT, float& accumulatedDt) +{ + if(delayT > 0.0f) + { + delayT -= accumulatedDt; + accumulatedDt = max(-delayT, 0.0f); + delayT = max(delayT, 0.0f); + } + DebugAssert(delayT >= 0.0f); +} + +static inline void CheckParticleConsistency(ParticleSystemState& state, ParticleSystemParticle& particle) +{ + particle.lifetime = min(particle.lifetime, particle.startLifetime); + state.maxSize = max(state.maxSize, particle.size); +} + +ParticleSystem::ParticleSystem (MemLabelId label, ObjectCreationMode mode) +: Super(label, mode) +, m_RayBudget(0) +, m_EmittersIndex (-1) +#if UNITY_EDITOR +, m_EditorListNode ( this ) +#endif +{ + m_State = new ParticleSystemState (); + m_ReadOnlyState = new ParticleSystemReadOnlyState (); + + m_SizeModule = new SizeModule (); + m_RotationModule = new RotationModule (); + m_ColorModule = new ColorModule (); + m_UVModule = new UVModule (); + m_VelocityModule = new VelocityModule (); + m_ForceModule = new ForceModule (); + m_ExternalForcesModule = new ExternalForcesModule (); + m_ClampVelocityModule = new ClampVelocityModule (); + m_SizeBySpeedModule = new SizeBySpeedModule (); + m_RotationBySpeedModule = new RotationBySpeedModule (); + m_ColorBySpeedModule = new ColorBySpeedModule (); + m_CollisionModule = new CollisionModule (); + m_SubModule = new SubModule (); + +#if UNITY_EDITOR + ParticleSystemEditor::SetupDefaultParticleSystem(*this); + m_EditorRandomSeedIndex = 0; +#endif +} + +ParticleSystem::~ParticleSystem () +{ + delete m_State; + delete m_ReadOnlyState; + + delete m_SizeModule; + delete m_RotationModule; + delete m_ColorModule; + delete m_UVModule; + delete m_VelocityModule; + delete m_ForceModule; + delete m_ExternalForcesModule; + delete m_ClampVelocityModule; + delete m_SizeBySpeedModule; + delete m_RotationBySpeedModule; + delete m_ColorBySpeedModule; + delete m_CollisionModule; + delete m_SubModule; +} + +void ParticleSystem::InitializeClass () +{ +#if UNITY_EDITOR + // This is only necessary to avoid pain during development (Pre 3.5) - can be removed later... + RegisterAllowTypeNameConversion ("MinMaxColorCurve", "MinMaxColor"); + // Only needed due to 3.5 beta content + RegisterAllowNameConversion ("MinMaxCurve", "maxScalar", "scalar"); + RegisterAllowNameConversion ("InitialModule", "lifetime", "startLifetime"); + RegisterAllowNameConversion ("InitialModule", "speed", "startSpeed"); + RegisterAllowNameConversion ("InitialModule", "color", "startColor"); + RegisterAllowNameConversion ("InitialModule", "size", "startSize"); + RegisterAllowNameConversion ("InitialModule", "rotation", "startRotation"); + RegisterAllowNameConversion ("ShapeModule", "m_Radius", "radius"); + RegisterAllowNameConversion ("ShapeModule", "m_Angle", "angle"); + RegisterAllowNameConversion ("ShapeModule", "m_BoxX", "boxX"); + RegisterAllowNameConversion ("ShapeModule", "m_BoxY", "boxY"); + RegisterAllowNameConversion ("ShapeModule", "m_BoxZ", "boxZ"); + + REGISTER_MESSAGE_VOID (ParticleSystem, kTransformChanged, TransformChanged); +#endif + + REGISTER_MESSAGE_VOID(ParticleSystem, kDidDeleteMesh, DidDeleteMesh); + REGISTER_MESSAGE_VOID(ParticleSystem, kDidModifyMesh, DidModifyMesh); +} + +void ParticleSystem::SetRayBudget (int rayBudget) +{ + m_RayBudget = rayBudget; +}; + +int ParticleSystem::GetRayBudget() const +{ + return m_RayBudget; +}; + +void ParticleSystem::DidModifyMesh () +{ + m_ShapeModule.DidModifyMeshData(); +} + +void ParticleSystem::DidDeleteMesh () +{ + m_ShapeModule.DidDeleteMesh(this); +} + +void ParticleSystem::Cull() +{ + if(!IsWorldPlaying()) + return; + + m_State->culled = true; + + Clear(false); + m_State->cullTime = GetCurTime (); + RemoveFromManager(); +} + +void ParticleSystem::RendererBecameVisible() +{ + if(m_State->culled) + { + m_State->culled = false; + if(!m_State->stopEmitting && IsPlaying() && IsWorldPlaying() && CheckSupportsProcedural (*this)) + { + double timeDiff = GetCurTime() - m_State->cullTime; + Simulate(m_State->numLoops * m_ReadOnlyState->lengthInSec + m_State->t + timeDiff, true); + Play(); + } + } +} + +void ParticleSystem::RendererBecameInvisible() +{ + if(!m_State->culled && CheckSupportsProcedural(*this)) + Cull(); +} + +void ParticleSystem::AddToManager() +{ + if(m_EmittersIndex >= 0) + return; + size_t index = gParticleSystemManager.activeEmitters.size(); + gParticleSystemManager.activeEmitters.push_back(this); + m_EmittersIndex = index; +} + +void ParticleSystem::RemoveFromManager() +{ + if(m_EmittersIndex < 0) + return; + const int index = m_EmittersIndex; + gParticleSystemManager.activeEmitters[index]->m_EmittersIndex = -1; + gParticleSystemManager.activeEmitters[index] = gParticleSystemManager.activeEmitters.back(); + if(gParticleSystemManager.activeEmitters[index] != this) // corner case + gParticleSystemManager.activeEmitters[index]->m_EmittersIndex = index; + gParticleSystemManager.activeEmitters.resize_uninitialized(gParticleSystemManager.activeEmitters.size() - 1); +} + +void ParticleSystem::AwakeFromLoad (AwakeFromLoadMode awakeMode) +{ + Super::AwakeFromLoad (awakeMode); + m_InitialModule.AwakeFromLoad(this, *m_ReadOnlyState); + m_ShapeModule.AwakeFromLoad(this, *m_ReadOnlyState); + + if (!IsActive () || (kDefaultAwakeFromLoad == awakeMode)) + return; + + m_State->localToWorld = GetComponent (Transform).GetLocalToWorldMatrixNoScale(); + Matrix4x4f::Invert_General3D(m_State->localToWorld, m_State->worldToLocal); + m_State->maxSize = 0.0f; + m_State->invalidateProcedural = false; + + if (IsWorldPlaying () && m_ReadOnlyState->playOnAwake) + Play (); + + // Does this even happen? + if(GetParticleCount() || IsPlaying()) + AddToManager(); +} + +void ParticleSystem::Deactivate (DeactivateOperation operation) +{ + Super::Deactivate(operation); + SyncJobs(); + RemoveFromManager(); +} + +void ParticleSystem::SyncJobs() +{ + if(gParticleSystemManager.needSync) + { +#if ENABLE_MULTITHREADED_PARTICLES + PROFILER_BEGIN(gParticleSystemWait, NULL); + JobScheduler& scheduler = GetJobScheduler(); + scheduler.WaitForGroup (gParticleSystemManager.jobGroup); + PROFILER_END; + +#endif + const float deltaTimeEpsilon = 0.0001f; + float deltaTime = GetDeltaTime(); + if(deltaTime < deltaTimeEpsilon) + return; + + for(int i = 0; i < gParticleSystemManager.activeEmitters.size(); ++i) + { + ParticleSystem& system = *gParticleSystemManager.activeEmitters[i]; + ParticleSystemState& state = *system.m_State; + system.Update2 (system, *system.m_ReadOnlyState, state, false); + } + + gParticleSystemManager.needSync = false; + } +} + +#if ENABLE_MULTITHREADED_PARTICLES +void* ParticleSystem::UpdateFunction (void* rawData) +{ + Assert (rawData); + ParticleSystem* system = reinterpret_cast<ParticleSystem*>(rawData); + ParticleSystem::Update1 (*system, system->GetParticles((int)ParticleSystem::kParticleBuffer0), system->GetThreadScratchPad().deltaTime, false, false, system->m_RayBudget); + return NULL; +} +#endif + +#define MAX_NUM_SUB_EMIT_CMDS (1024) +void ParticleSystem::Update(ParticleSystem& system, float deltaTime, bool fixedTimeStep, bool useProcedural, int rayBudget) +{ + system.m_State->recordSubEmits = false; + Update0 (system, *system.m_ReadOnlyState, *system.m_State, deltaTime, fixedTimeStep); + Update1 (system, system.GetParticles((int)ParticleSystem::kParticleBuffer0), deltaTime, fixedTimeStep, useProcedural, rayBudget); + Update2 (system, *system.m_ReadOnlyState, *system.m_State, fixedTimeStep); +#if UNITY_EDITOR + ParticleSystemEditor::PerformInterpolationStep(&system); +#endif +} + +size_t ParticleSystem::EmitFromModules (const ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemEmissionState& emissionState, size_t& numContinuous, const Vector3f velocity, float fromT, float toT, float dt) +{ + if(system.m_EmissionModule.GetEnabled()) + return EmitFromData(emissionState, numContinuous, system.m_EmissionModule.GetEmissionDataRef(), velocity, fromT, toT, dt, roState.lengthInSec); + return 0; +} + +size_t ParticleSystem::EmitFromData (ParticleSystemEmissionState& emissionState, size_t& numContinuous, const ParticleSystemEmissionData& emissionData, const Vector3f velocity, float fromT, float toT, float dt, float length) +{ + size_t amountOfParticlesToEmit = 0; + EmissionModule::Emit(emissionState, amountOfParticlesToEmit, numContinuous, emissionData, velocity, fromT, toT, dt, length); + return amountOfParticlesToEmit; +} + +size_t ParticleSystem::LimitParticleCount(size_t requestSize) const +{ + const size_t maxNumParticles = m_InitialModule.GetMaxNumParticles(); + return min<size_t>(requestSize, maxNumParticles); +} + +size_t ParticleSystem::AddNewParticles(ParticleSystemParticles& particles, size_t newParticles) const +{ + const size_t fromIndex = particles.array_size(); + const size_t newSize = LimitParticleCount(fromIndex + newParticles); + particles.array_resize(newSize); + return min(fromIndex, newSize); +} + +void ParticleSystem::SimulateParticles (const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, float dt) +{ + size_t particleCount = ps.array_size(); + for (size_t q = fromIndex; q < particleCount; ) + { + ps.lifetime[q] -= dt; + +#if UNITY_EDITOR + if(ParticleSystemEditor::GetIsExtraInterpolationStep()) + { + ps.lifetime[q] = max(ps.lifetime[q], 0.0f); + } + else +#endif + + if (ps.lifetime[q] < 0) + { + KillParticle(roState, state, ps, q, particleCount); + continue; + } + ++q; + } + ps.array_resize(particleCount); + + for (size_t q = fromIndex; q < particleCount; ++q) + ps.position[q] += (ps.velocity[q] + ps.animatedVelocity[q]) * dt; + + if(ps.usesRotationalSpeed) + for (size_t q = fromIndex; q < particleCount; ++q) + ps.rotation[q] += ps.rotationalSpeed[q] * dt; +} + +// Copy staging particles into real particle buffer +void ParticleSystem::AddStagingBuffer(ParticleSystem& system) +{ + if(0 == system.m_ParticlesStaging.array_size()) + return; + + bool needsAxisOfRotation = system.m_Particles[kParticleBuffer0].usesAxisOfRotation; + bool needsEmitAccumulator = system.m_Particles[kParticleBuffer0].numEmitAccumulators > 0; + + ASSERT_RUNNING_ON_MAIN_THREAD; + const int numParticles = system.m_Particles[kParticleBuffer0].array_size(); + const int numStaging = system.m_ParticlesStaging.array_size(); + system.m_Particles->array_resize(numParticles + numStaging); + system.m_Particles->array_merge_preallocated(system.m_ParticlesStaging, numParticles, needsAxisOfRotation, needsEmitAccumulator); + system.m_ParticlesStaging.array_resize(0); +} + +void ParticleSystem::Emit(ParticleSystem& system, const SubEmitterEmitCommand& command, ParticleSystemEmitMode emitMode) +{ + int amountOfParticlesToEmit = command.particlesToEmit; + if (amountOfParticlesToEmit > 0) + { + ParticleSystemState& state = *system.m_State; + const ParticleSystemReadOnlyState& roState = *system.m_ReadOnlyState; + + const int numContinuous = command.particlesToEmitContinuous; + const float deltaTime = command.deltaTime; + float parentT = command.parentT; + Vector3f position = command.position; + Vector3f initialVelocity = command.velocity; + + Matrix3x3f rotMat; + Vector3f normalizedVelocity = NormalizeSafe(initialVelocity); + float angle = Abs(Dot(normalizedVelocity, Vector3f::zAxis)); + Vector3f up = Lerp(Vector3f::zAxis, Vector3f::yAxis, angle); + if (!LookRotationToMatrix(normalizedVelocity, up, &rotMat)) + rotMat.SetIdentity(); + + Matrix4x4f parentParticleMatrix = rotMat; + parentParticleMatrix.SetPosition(position); + + // Transform into local space of sub emitter + Matrix4x4f concatMatrix; + if(roState.useLocalSpace) + MultiplyMatrices3x4(state.worldToLocal, parentParticleMatrix, concatMatrix); + else + concatMatrix = parentParticleMatrix; + + if(roState.useLocalSpace) + initialVelocity = state.worldToLocal.MultiplyVector3(initialVelocity); + + DebugAssert(state.GetIsSubEmitter()); + DebugAssert(state.stopEmitting); + + const float commandAliveTime = command.timeAlive; + const float timeStep = GetTimeStep(commandAliveTime, true); + + // @TODO: Perform culling: if max lifetime < timeAlive, then just skip emit + + // Perform sub emitter loop + if(roState.looping) + parentT = fmodf(parentT, roState.lengthInSec); + + ParticleSystemParticles& particles = (kParticleSystemEMDirect == emitMode) ? system.m_Particles[kParticleBuffer0] : system.m_ParticlesStaging; + + size_t fromIndex = system.AddNewParticles(particles, amountOfParticlesToEmit); + StartModules (system, roState, state, command.emissionState, initialVelocity, concatMatrix, particles, fromIndex, deltaTime, parentT, numContinuous, 0.0f); + + // Make sure particles get updated + if(0 == fromIndex) + system.KeepUpdating(); + + // Update incremental + if(timeStep > 0.0001f) + { + float accumulatedDt = commandAliveTime; + while (accumulatedDt >= timeStep) + { + accumulatedDt -= timeStep; + UpdateModulesIncremental(system, roState, state, particles, fromIndex, timeStep); + } + } + } +} + + +void ParticleSystem::PlaybackSubEmitterCommandBuffer(const ParticleSystem& parent, ParticleSystemState& state, bool fixedTimeStep) +{ + ParticleSystem* subEmittersBirth[kParticleSystemMaxSubBirth]; + ParticleSystem* subEmittersCollision[kParticleSystemMaxSubCollision]; + ParticleSystem* subEmittersDeath[kParticleSystemMaxSubDeath]; + int numBirth = parent.m_SubModule->GetSubEmitterPtrsBirth(&subEmittersBirth[0]); + int numCollision = parent.m_SubModule->GetSubEmitterPtrsCollision(&subEmittersCollision[0]); + int numDeath = parent.m_SubModule->GetSubEmitterPtrsDeath(&subEmittersDeath[0]); + + const int numCommands = state.subEmitterCommandBuffer.commandCount; + const SubEmitterEmitCommand* commands = state.subEmitterCommandBuffer.commands; + for(int i = 0; i < numCommands; i++) + { + const SubEmitterEmitCommand& command = commands[i]; + ParticleSystem* shuriken = NULL; + if(command.subEmitterType == kParticleSystemSubTypeBirth) + shuriken = (command.subEmitterIndex < numBirth) ? subEmittersBirth[command.subEmitterIndex] : NULL; + else if(command.subEmitterType == kParticleSystemSubTypeCollision) + shuriken = (command.subEmitterIndex < numCollision) ? subEmittersCollision[command.subEmitterIndex] : NULL; + else if(command.subEmitterType == kParticleSystemSubTypeDeath) + shuriken = (command.subEmitterIndex < numDeath) ? subEmittersDeath[command.subEmitterIndex] : NULL; + else + Assert(!"PlaybackSubEmitterCommandBuffer: Sub emitter type not implemented"); + + DebugAssert(shuriken && "Y U NO HERE ANYMORE?"); + if(!shuriken) + continue; + + ParticleSystem::Emit(*shuriken, command, kParticleSystemEMDirect); + } + + state.subEmitterCommandBuffer.commandCount = 0; +} + +void ParticleSystem::UpdateBounds(const ParticleSystem& system, const ParticleSystemParticles& ps, ParticleSystemState& state) +{ + const size_t particleCount = ps.array_size(); + if (particleCount == 0) + { + state.minMaxAABB = MinMaxAABB (Vector3f::zero, Vector3f::zero); + return; + } + + state.minMaxAABB.Init(); + + if(CheckSupportsProcedural(system)) + { + const Transform& transform = system.GetComponent (Transform); + Matrix4x4f localToWorld = transform.GetLocalToWorldMatrixNoScale (); + + // Lifetime and max speed + const Vector2f minMaxLifeTime = system.m_InitialModule.GetLifeTimeCurve().FindMinMax(); + + const float maxLifeTime = minMaxLifeTime.y; + const Vector2f minMaxStartSpeed = system.m_InitialModule.GetSpeedCurve().FindMinMax() * maxLifeTime; + + state.minMaxAABB = MinMaxAABB(Vector3f::zero, Vector3f::zero); + state.minMaxAABB.Encapsulate(Vector3f::zAxis * minMaxStartSpeed.x); + state.minMaxAABB.Encapsulate(Vector3f::zAxis * minMaxStartSpeed.y); + if(system.m_ShapeModule.GetEnabled()) + system.m_ShapeModule.CalculateProceduralBounds(state.minMaxAABB, state.emitterScale, minMaxStartSpeed); + + // Gravity + // @TODO: Do what we do for force and velocity here, i.e. transform bounds properly + const Vector3f gravityBounds = system.m_InitialModule.GetGravity(*system.m_ReadOnlyState, *system.m_State) * maxLifeTime * maxLifeTime * 0.5f; + + state.minMaxAABB.m_Max += max(gravityBounds, Vector3f::zero); + state.minMaxAABB.m_Min += min(gravityBounds, Vector3f::zero); + + MinMaxAABB velocityBounds (Vector3f::zero, Vector3f::zero); + if(system.m_VelocityModule->GetEnabled()) + system.m_VelocityModule->CalculateProceduralBounds(velocityBounds, localToWorld, maxLifeTime); + state.minMaxAABB.m_Max += velocityBounds.m_Max; + state.minMaxAABB.m_Min += velocityBounds.m_Min; + + MinMaxAABB forceBounds (Vector3f::zero, Vector3f::zero); + if(system.m_ForceModule->GetEnabled()) + system.m_ForceModule->CalculateProceduralBounds(forceBounds, localToWorld, maxLifeTime); + state.minMaxAABB.m_Max += forceBounds.m_Max; + state.minMaxAABB.m_Min += forceBounds.m_Min; + + // Modules that are not supported by procedural + DebugAssert(!system.m_RotationBySpeedModule->GetEnabled()); // find out if possible to support it + DebugAssert(!system.m_ClampVelocityModule->GetEnabled()); // unsupported: (Need to compute velocity by deriving position curves...), possible to support? + DebugAssert(!system.m_CollisionModule->GetEnabled()); + DebugAssert(!system.m_SubModule->GetEnabled()); // find out if possible to support it + DebugAssert(!system.m_ExternalForcesModule->GetEnabled()); + } + else + { + for (size_t q = 0; q < particleCount; ++q) + state.minMaxAABB.Encapsulate (ps.position[q]); + + ParticleSystemRenderer* renderer = system.QueryComponent(ParticleSystemRenderer); + if(renderer && (renderer->GetRenderMode() == kSRMStretch3D)) + { + const float velocityScale = renderer->GetVelocityScale(); + const float lengthScale = renderer->GetLengthScale(); + for(size_t q = 0; q < particleCount; ++q ) + { + float sqrVelocity = SqrMagnitude (ps.velocity[q]+ps.animatedVelocity[q]); + if (sqrVelocity > Vector3f::epsilon) + { + float scale = velocityScale + FastInvSqrt (sqrVelocity) * lengthScale * ps.size[q]; + state.minMaxAABB.Encapsulate(ps.position[q] - ps.velocity[q] * scale); + } + } + } + } + + // Expand with maximum particle size * sqrt(2) (the length of a diagonal of a particle, if it should happen to be rotated) + const float kSqrtOf2 = 1.42f; + float maxSize = kSqrtOf2 * 0.5f * system.m_InitialModule.GetSizeCurve().FindMinMax().y; + if(system.m_SizeModule->GetEnabled()) + maxSize *= system.m_SizeModule->GetCurve().FindMinMax().y; + if(system.m_SizeBySpeedModule->GetEnabled()) + maxSize *= system.m_SizeBySpeedModule->GetCurve().FindMinMax().y; + state.minMaxAABB.Expand (max(maxSize, state.maxSize)); + + Assert (state.minMaxAABB.IsValid ()); +} + +IMPLEMENT_CLASS_HAS_INIT (ParticleSystem) +IMPLEMENT_OBJECT_SERIALIZE (ParticleSystem) + + +template<class TransferFunction> +void ParticleSystem::Transfer (TransferFunction& transfer) +{ + Super::Transfer (transfer); + + m_ReadOnlyState->Transfer (transfer); m_ReadOnlyState->CheckConsistency(); + m_State->Transfer (transfer); + transfer.Transfer(m_InitialModule, m_InitialModule.GetName ()); m_InitialModule.CheckConsistency (); + transfer.Transfer(m_ShapeModule, m_ShapeModule.GetName ()); m_ShapeModule.CheckConsistency (); + transfer.Transfer(m_EmissionModule, m_EmissionModule.GetName ()); m_EmissionModule.CheckConsistency (); + transfer.Transfer(*m_SizeModule, m_SizeModule->GetName ()); m_SizeModule->CheckConsistency (); + transfer.Transfer(*m_RotationModule, m_RotationModule->GetName ()); m_RotationModule->CheckConsistency (); + transfer.Transfer(*m_ColorModule, m_ColorModule->GetName ()); m_ColorModule->CheckConsistency (); + transfer.Transfer(*m_UVModule, m_UVModule->GetName ()); m_UVModule->CheckConsistency (); + transfer.Transfer(*m_VelocityModule, m_VelocityModule->GetName ()); m_VelocityModule->CheckConsistency (); + transfer.Transfer(*m_ForceModule, m_ForceModule->GetName ()); m_ForceModule->CheckConsistency (); + transfer.Transfer(*m_ExternalForcesModule, m_ExternalForcesModule->GetName ()); m_ExternalForcesModule->CheckConsistency (); + transfer.Transfer(*m_ClampVelocityModule, m_ClampVelocityModule->GetName ()); m_ClampVelocityModule->CheckConsistency (); + transfer.Transfer(*m_SizeBySpeedModule, m_SizeBySpeedModule->GetName ()); m_SizeBySpeedModule->CheckConsistency (); + transfer.Transfer(*m_RotationBySpeedModule, m_RotationBySpeedModule->GetName ()); m_RotationBySpeedModule->CheckConsistency (); + transfer.Transfer(*m_ColorBySpeedModule, m_ColorBySpeedModule->GetName ()); m_ColorBySpeedModule->CheckConsistency (); + transfer.Transfer(*m_CollisionModule, m_CollisionModule->GetName ()); m_CollisionModule->CheckConsistency (); + transfer.Transfer(*m_SubModule, m_SubModule->GetName ()); m_SubModule->CheckConsistency (); + if(transfer.IsReading()) + { + m_State->supportsProcedural = DetermineSupportsProcedural(*this); + m_State->invalidateProcedural = true; // Stuff might have changed which we can't support (example: start speed has become smaller) + } +} + +bool ParticleSystem::CheckSupportsProcedural(const ParticleSystem& system) +{ + ParticleSystemRenderer* renderer = system.QueryComponent(ParticleSystemRenderer); + if(renderer && (renderer->GetRenderMode() == kSRMStretch3D)) + return false; + return system.m_State->supportsProcedural && !system.m_State->invalidateProcedural; +} + +// TODO: Needs to match UpdateCullingSupportedString in all xxModule.cs files +bool ParticleSystem::DetermineSupportsProcedural(const ParticleSystem& system) +{ + bool supportsProcedural = true; + supportsProcedural = supportsProcedural && system.m_ReadOnlyState->useLocalSpace; + + // Can't be random as we recreate it every frame. TODO: Would be great if we can support this in procedural mode. + const short lifeTimeMinMaxState = system.m_InitialModule.GetLifeTimeCurve().minMaxState; + supportsProcedural = supportsProcedural && (lifeTimeMinMaxState == kMMCScalar || lifeTimeMinMaxState == kMMCCurve); + + supportsProcedural = supportsProcedural && (EmissionModule::kEmissionTypeDistance != system.m_EmissionModule.GetEmissionDataRef().type); + + supportsProcedural = supportsProcedural && !system.m_ExternalForcesModule->GetEnabled(); + supportsProcedural = supportsProcedural && !system.m_ClampVelocityModule->GetEnabled(); + supportsProcedural = supportsProcedural && !system.m_RotationBySpeedModule->GetEnabled(); + supportsProcedural = supportsProcedural && !system.m_CollisionModule->GetEnabled(); + supportsProcedural = supportsProcedural && !system.m_SubModule->GetEnabled(); + + if(system.m_RotationModule->GetEnabled()) + supportsProcedural = supportsProcedural && CurvesSupportProcedural (system.m_RotationModule->GetCurve().editorCurves, system.m_RotationModule->GetCurve().minMaxState); + + if(system.m_VelocityModule->GetEnabled()) + { + supportsProcedural = supportsProcedural && CurvesSupportProcedural (system.m_VelocityModule->GetXCurve().editorCurves, system.m_VelocityModule->GetXCurve().minMaxState); + supportsProcedural = supportsProcedural && CurvesSupportProcedural (system.m_VelocityModule->GetYCurve().editorCurves, system.m_VelocityModule->GetYCurve().minMaxState); + supportsProcedural = supportsProcedural && CurvesSupportProcedural (system.m_VelocityModule->GetZCurve().editorCurves, system.m_VelocityModule->GetZCurve().minMaxState); + } + + if(system.m_ForceModule->GetEnabled()) + { + supportsProcedural = supportsProcedural && CurvesSupportProcedural (system.m_ForceModule->GetXCurve().editorCurves, system.m_ForceModule->GetXCurve().minMaxState); + supportsProcedural = supportsProcedural && CurvesSupportProcedural (system.m_ForceModule->GetYCurve().editorCurves, system.m_ForceModule->GetYCurve().minMaxState); + supportsProcedural = supportsProcedural && CurvesSupportProcedural (system.m_ForceModule->GetZCurve().editorCurves, system.m_ForceModule->GetZCurve().minMaxState); + supportsProcedural = supportsProcedural && !system.m_ForceModule->GetRandomizePerFrame(); + } + + return supportsProcedural; +} + +bool ParticleSystem::ComputePrewarmStartParameters(float& prewarmTime, float t) +{ + const float fixedDelta = GetTimeManager().GetFixedDeltaTime(); + const float maxLifetime = m_InitialModule.GetLifeTimeCurve().FindMinMax().y; + const float length = m_ReadOnlyState->lengthInSec; + if(!m_ReadOnlyState->looping && ((maxLifetime + length) < t)) + return false; + + prewarmTime = m_SubModule->GetEnabled() ? CalculateSubEmitterMaximumLifeTime(maxLifetime) : 0.0f; + prewarmTime = max(prewarmTime, maxLifetime); + + float frac = fmodf(t, fixedDelta); + float startT = t - prewarmTime - frac; + prewarmTime += frac; + + // Clamp to start + if(!m_ReadOnlyState->prewarm) + { + startT = max(startT, 0.0f); + prewarmTime = min(t, prewarmTime); + } + + // This is needed to figure out if emitacc should be inverted or not + const float signStartT = Sign(startT); + const float absStartT = Abs(startT); + + while(startT < 0.0f) + startT += length; + float endT = startT + absStartT; + m_State->t = fmodf(startT, length); + + ParticleSystemEmissionState emissionState; + const Vector3f emitVelocity = Vector3f::zero; + const float epsilon = 0.0001f; + int seedIndex = 0; + + const float prevStartT = startT; + const float nextStartT = startT + fixedDelta; + const float prevEndT = endT; + const float nextEndT = endT + fixedDelta; + if (!(nextStartT > prevStartT && nextEndT > prevEndT)) + { + ErrorStringObject ("Precision issue while prewarming particle system - 'Duration' or 'Start Lifetime' is likely a too large value.", this); + return false; + } + + while((startT + epsilon) < endT) + { + const float toT = fmodf(startT + fixedDelta, length); + const float fromT = fmodf(startT, length); + + size_t numContinuous; + int numParticles = ParticleSystem::EmitFromModules(*this, *m_ReadOnlyState, emissionState, numContinuous, emitVelocity, fromT, toT, fixedDelta); + if(numParticles > 0) + seedIndex++; + startT += fixedDelta; + } + m_State->emissionState.m_ToEmitAccumulator = ((signStartT > 0.0f ? emissionState.m_ToEmitAccumulator : 1.0f - emissionState.m_ToEmitAccumulator)) + epsilon; +#if UNITY_EDITOR + m_EditorRandomSeedIndex = signStartT > 0.0f ? seedIndex : -seedIndex-1; +#endif + + return true; +} + +void ParticleSystem::Simulate (float t, bool restart) +{ + PROFILER_AUTO(gParticleSystemPrewarm, NULL) + + if(restart) + { + m_InitialModule.ResetSeed(*m_ReadOnlyState); + m_ShapeModule.ResetSeed(*m_ReadOnlyState); + Stop(); + Clear(); + Play (false); + + ApplyStartDelay (m_State->delayT, t); + float prewarmTime; + if(ComputePrewarmStartParameters(prewarmTime, t)) + { + Update(*this, prewarmTime, true, CheckSupportsProcedural(*this)); + Pause (); + } + else + { + Stop(); + Clear(); + } + } + else + { + m_State->playing = true; + Update(*this, t, true, false); + Pause (); + } +} + +void ParticleSystem::Play (bool autoPrewarm) +{ + Assert (m_State); + if(!IsActive () || m_State->GetIsSubEmitter()) + return; + + m_State->stopEmitting = false; + m_State->playing = true; + if (m_State->needRestart) + { + if(m_ReadOnlyState->prewarm) + { + if (autoPrewarm) + AutoPrewarm(); + } + else + { + m_State->delayT = m_ReadOnlyState->startDelay; + } + + m_State->playing = true; + m_State->t = 0.0f; + m_State->numLoops = 0; + m_State->invalidateProcedural = false; + m_State->accumulatedDt = 0.0f; +#if UNITY_EDITOR + m_EditorRandomSeedIndex = 0; +#endif + m_State->emissionState.Clear(); + } + + if(m_State->culled && CheckSupportsProcedural(*this)) + Cull(); + else + AddToManager(); +} + +void ParticleSystem::AutoPrewarm() +{ + if(m_ReadOnlyState->prewarm && m_ReadOnlyState->looping) + { + DebugAssert(!m_State->GetIsSubEmitter()); + DebugAssert(m_State->playing); // Only use together with play + Simulate (0.0f, true); + //DebugAssert(CompareApproximately(m_State->emissionState.m_ToEmitAccumulator, 1.0f, 0.01f) && "Particle emission gaps may occur"); + } +} + +void ParticleSystem::Stop () +{ + Assert (m_State); + m_State->needRestart = true; + m_State->stopEmitting = true; +} + +void ParticleSystem::Pause () +{ + Assert (m_State); + m_State->playing = false; + m_State->needRestart = false; + RemoveFromManager(); +} + +bool ParticleSystem::IsPaused () const +{ + Assert (m_State); + return !m_State->playing && !m_State->needRestart; +} + +bool ParticleSystem::IsStopped () const +{ + Assert (m_State); + return !m_State->playing && m_State->needRestart; +} + +void ParticleSystem::KeepUpdating() +{ + if (IsActive()) + { + // Ensure added particles will update, but stop emission + m_State->playing = true; + m_State->stopEmitting = true; + AddToManager(); + } +} + +void ParticleSystem::Emit (int count) +{ + // StartParticles() takes size_t so check for negative value here (case 495098) + if (count <= 0) + return; + + KeepUpdating(); + Assert (m_State); + + const Transform& transform = GetComponent (Transform); + Matrix4x4f oldLocalToWorld = m_State->localToWorld; + Matrix4x4f oldWorldToLocal = m_State->worldToLocal; + Vector3f oldEmitterScale = m_State->emitterScale; + m_State->localToWorld = transform.GetLocalToWorldMatrixNoScale (); + Matrix4x4f::Invert_General3D(m_State->localToWorld, m_State->worldToLocal); + if (IS_CONTENT_NEWER_OR_SAME (kUnityVersion4_2_a1)) + m_State->emitterScale = transform.GetWorldScaleLossy(); + else + m_State->emitterScale = transform.GetLocalScale(); + StartParticles(*this, m_Particles[kParticleBuffer0], 0.0f, m_State->t, 0.0f, 0, count, 0.0f); + m_State->localToWorld = oldLocalToWorld; + m_State->worldToLocal = oldWorldToLocal; + m_State->emitterScale = oldEmitterScale; +} + +void ParticleSystem::EmitParticleExternal(ParticleSystemParticle* particle) +{ +#if UNITY_EDITOR + if(!IsWorldPlaying() && IsStopped ()) + return; +#endif + + m_State->invalidateProcedural = true; + + CheckParticleConsistency(*m_State, *particle); + if(particle->lifetime <= 0.0f) + return; + + KeepUpdating(); + + m_Particles[kParticleBuffer0].AddParticle(particle); + +#if UNITY_EDITOR + if(!IsWorldPlaying()) + m_Particles[kParticleBuffer1].AddParticle(particle); +#endif + + if(!IsPlaying()) + UpdateBounds(*this, m_Particles[kParticleBuffer0], *m_State); +} + +void ParticleSystem::Clear (bool updateBounds) +{ + for(int i = 0; i < kNumParticleBuffers; i++) + m_Particles[i].array_resize(0); + m_ParticlesStaging.array_resize(0); + m_State->emitReplay.resize_uninitialized(0); + + if (m_State->stopEmitting) + { + // This triggers sometimes, why? (case 491684) + DebugAssert (m_State->needRestart); + m_State->playing = false; + RemoveFromManager(); + } + + if(updateBounds) + { + UpdateBounds(*this, m_Particles[kParticleBuffer0], *m_State); + Update2(*this, *m_ReadOnlyState, *m_State, false); + } +} + +float ParticleSystem::GetStartDelay() const +{ + Assert(m_State); + return m_ReadOnlyState->startDelay; +} + +void ParticleSystem::SetStartDelay(float value) +{ + Assert(m_State); + m_ReadOnlyState->startDelay = value; + SetDirty (); +} + +bool ParticleSystem::IsAlive () const +{ + Assert(m_State); + return (!IsStopped() || (GetParticleCount() > 0)); +} + +bool ParticleSystem::IsPlaying () const +{ + Assert (m_State); + return m_State->playing; +} + +bool ParticleSystem::GetLoop () const +{ + Assert (m_State); + return m_ReadOnlyState->looping; +} + +void ParticleSystem::SetLoop (bool loop) +{ + Assert (m_State); + m_ReadOnlyState->looping = loop; + SetDirty (); +} + +bool ParticleSystem::GetPlayOnAwake () const +{ + Assert (m_State); + return m_ReadOnlyState->playOnAwake; +} + +void ParticleSystem::SetPlayOnAwake (bool playOnAwake) +{ + Assert (m_State); + m_ReadOnlyState->playOnAwake = playOnAwake; + SetDirty (); +} + +ParticleSystemSimulationSpace ParticleSystem::GetSimulationSpace () const +{ + Assert (m_State); + return (m_ReadOnlyState->useLocalSpace ? kSimLocal : kSimWorld); +} + +void ParticleSystem::SetSimulationSpace (ParticleSystemSimulationSpace simulationSpace) +{ + Assert (m_State); + m_ReadOnlyState->useLocalSpace = (simulationSpace == kSimLocal); + SetDirty (); +} + +float ParticleSystem::GetSecPosition () const +{ + Assert (m_State); + return m_State->t; +} + +void ParticleSystem::SetSecPosition (float pos) +{ + Assert (m_State); + m_State->t = pos; + SetDirty (); +} + +float ParticleSystem::GetPlaybackSpeed () const +{ + Assert (m_State); + return m_ReadOnlyState->speed; +} + +void ParticleSystem::SetPlaybackSpeed (float speed) +{ + Assert (m_State); + m_ReadOnlyState->speed = speed; + SetDirty (); +} + +float ParticleSystem::GetLengthInSec () const +{ + Assert (m_State); + return m_ReadOnlyState->lengthInSec; +} + +void ParticleSystem::GetNumTiles(int& uvTilesX, int& uvTilesY) const +{ + uvTilesX = uvTilesY = 1; + if(m_UVModule->GetEnabled()) + m_UVModule->GetNumTiles(uvTilesX, uvTilesY); +} + +Matrix4x4f ParticleSystem::GetLocalToWorldMatrix() const +{ + return m_State->localToWorld; +} + +bool ParticleSystem::GetEnableEmission() const +{ + return m_EmissionModule.GetEnabled(); +} + +void ParticleSystem::SetEnableEmission(bool value) +{ + m_State->invalidateProcedural = true; + m_EmissionModule.SetEnabled(value); + SetDirty(); +} + +float ParticleSystem::GetEmissionRate() const +{ + return m_EmissionModule.GetEmissionDataRef().rate.GetScalar(); +} + +void ParticleSystem::SetEmissionRate(float value) +{ + m_State->invalidateProcedural = true; + m_EmissionModule.GetEmissionData().rate.SetScalar(value); + SetDirty(); +} + +float ParticleSystem::GetStartSpeed() const +{ + return m_InitialModule.GetSpeedCurve().GetScalar(); +} + +void ParticleSystem::SetStartSpeed(float value) +{ + m_State->invalidateProcedural = true; + m_InitialModule.GetSpeedCurve().SetScalar(value); + SetDirty(); +} + +float ParticleSystem::GetStartSize() const +{ + return m_InitialModule.GetSizeCurve().GetScalar(); +} + +void ParticleSystem::SetStartSize(float value) +{ + m_InitialModule.GetSizeCurve().SetScalar(value); + SetDirty(); +} + +ColorRGBAf ParticleSystem::GetStartColor() const +{ + return m_InitialModule.GetColor().maxColor; +} + +void ParticleSystem::SetStartColor(ColorRGBAf value) +{ + m_InitialModule.GetColor().maxColor = value; + SetDirty(); +} + +float ParticleSystem::GetStartRotation() const +{ + return m_InitialModule.GetRotationCurve().GetScalar(); +} + +void ParticleSystem::SetStartRotation(float value) +{ + m_InitialModule.GetRotationCurve().SetScalar(value); + SetDirty(); +} + +float ParticleSystem::GetStartLifeTime() const +{ + return m_InitialModule.GetLifeTimeCurve().GetScalar(); +} + +void ParticleSystem::SetStartLifeTime(float value) +{ + m_State->invalidateProcedural = true; + m_InitialModule.GetLifeTimeCurve().SetScalar(value); + SetDirty(); +} + +float ParticleSystem::GetGravityModifier() const +{ + return m_InitialModule.GetGravityModifier(); +} + +void ParticleSystem::SetGravityModifier(float value) +{ + m_State->invalidateProcedural = true; + m_InitialModule.SetGravityModifier(value); + SetDirty(); +} + +UInt32 ParticleSystem::GetRandomSeed() const +{ + return m_ReadOnlyState->randomSeed; +} + +void ParticleSystem::SetRandomSeed(UInt32 value) +{ + m_ReadOnlyState->randomSeed = value; + SetDirty(); +} + +int ParticleSystem::GetMaxNumParticles () const +{ + return m_InitialModule.GetMaxNumParticles (); +} + +void ParticleSystem::SetMaxNumParticles (int value) +{ + m_InitialModule.SetMaxNumParticles (value); + SetDirty(); +} + +void ParticleSystem::AllocateAllStructuresOfArrays() +{ + // Make sure all particle buffers have all elements + for(int i = 0; i < kNumParticleBuffers; i++) + { + if(!m_Particles[i].usesAxisOfRotation) + m_Particles[i].SetUsesAxisOfRotation(); + m_Particles[i].SetUsesEmitAccumulator(kParticleSystemMaxNumEmitAccumulators); + } +} + +void ParticleSystem::SetParticlesExternal (ParticleSystemParticle* particles, int size) +{ +#if UNITY_EDITOR + if(!IsWorldPlaying() && IsStopped ()) + return; +#endif + + m_State->invalidateProcedural = true; + + // Make sure particles are in the correct ranges etc. + for (size_t q = 0; q < size; q++) + CheckParticleConsistency(*m_State, particles[q]); + + AllocateAllStructuresOfArrays(); + ParticleSystemParticles& ps = m_Particles[kParticleBuffer0]; + ps.array_resize(size); + ps.CopyFromArrayAOS(particles, size); + size_t particleCount = size; + for (size_t q = 0; q < particleCount; ) + { + if (ps.lifetime[q] < 0) + { + KillParticle(*m_ReadOnlyState, *m_State, ps, q, particleCount); + continue; + } + ++q; + } + ps.array_resize(particleCount); + + #if UNITY_EDITOR + if(!IsWorldPlaying()) + m_Particles[kParticleBuffer1].array_assign(ps); + #endif + + if(!IsPlaying()) + UpdateBounds(*this, ps, *m_State); +} + +void ParticleSystem::GetParticlesExternal (ParticleSystemParticle* particles, int size) +{ + AllocateAllStructuresOfArrays(); + ParticleSystemParticles& ps = m_Particles[kParticleBuffer0]; + ps.CopyToArrayAOS(particles, size); +} + +int ParticleSystem::GetSafeCollisionEventSize () const +{ + const ParticleSystemParticles& ps = m_Particles[kParticleBuffer0]; + return ps.collisionEvents.GetCollisionEventCount (); +} + +int ParticleSystem::GetCollisionEventsExternal (int instanceID, MonoParticleCollisionEvent* collisionEvents, int size) const +{ + const ParticleSystemParticles& ps = m_Particles[kParticleBuffer0]; + return ps.collisionEvents.GetCollisionEvents (instanceID, collisionEvents, size); +} + +ParticleSystemParticles& ParticleSystem::GetParticles (int index) +{ +#if UNITY_EDITOR + if(index == -1) + if(ParticleSystemEditor::UseInterpolation(*this)) + index = (int)kParticleBuffer1; + else + index = (int)kParticleBuffer0; + return m_Particles[index]; +#else + return m_Particles[kParticleBuffer0]; +#endif +} + +const ParticleSystemParticles& ParticleSystem::GetParticles (int index) const +{ +#if UNITY_EDITOR + if(index == -1) + if(ParticleSystemEditor::UseInterpolation(*this)) + index = (int)kParticleBuffer1; + else + index = (int)kParticleBuffer0; + return m_Particles[index]; +#else + return m_Particles[kParticleBuffer0]; +#endif +} + +size_t ParticleSystem::GetParticleCount () const +{ + return m_Particles[kParticleBuffer0].array_size (); +} + +int ParticleSystem::SetupSubEmitters(ParticleSystem& shuriken, ParticleSystemState& state) +{ + Assert(!state.cachedSubDataBirth && !state.numCachedSubDataBirth); + Assert(!state.cachedSubDataCollision && !state.numCachedSubDataCollision); + Assert(!state.cachedSubDataDeath && !state.numCachedSubDataDeath); + int subEmitterCount = 0; + + if(shuriken.m_SubModule->GetEnabled()) + { + ParticleSystem* subEmittersBirth[kParticleSystemMaxSubBirth]; + state.numCachedSubDataBirth = shuriken.m_SubModule->GetSubEmitterPtrsBirth(&subEmittersBirth[0]); + state.cachedSubDataBirth = ALLOC_TEMP_MANUAL(ParticleSystemSubEmitterData, state.numCachedSubDataBirth); + std::uninitialized_fill (state.cachedSubDataBirth, state.cachedSubDataBirth + state.numCachedSubDataBirth, ParticleSystemSubEmitterData()); + for(int i = 0; i < state.numCachedSubDataBirth; i++) + { + ParticleSystem* subEmitter = subEmittersBirth[i]; + ParticleSystemSubEmitterData& subData = state.cachedSubDataBirth[i]; + subData.startDelayInSec = subEmitter->m_ReadOnlyState->startDelay; + subData.lengthInSec = subEmitter->GetLoop() ? numeric_limits<float>::max() : subEmitter->GetLengthInSec(); + subData.maxLifetime = subEmitter->m_InitialModule.GetLifeTimeCurve().GetScalar(); + subData.emitter = subEmitter; + subEmitter->m_EmissionModule.GetEmissionDataCopy(&subData.emissionData); + subEmitter->m_State->SetIsSubEmitter(true); + subEmitterCount++; + } + ParticleSystem* subEmittersCollision[kParticleSystemMaxSubCollision]; + state.numCachedSubDataCollision = shuriken.m_SubModule->GetSubEmitterPtrsCollision(&subEmittersCollision[0]); + state.cachedSubDataCollision = ALLOC_TEMP_MANUAL(ParticleSystemSubEmitterData, state.numCachedSubDataCollision); + std::uninitialized_fill (state.cachedSubDataCollision, state.cachedSubDataCollision + state.numCachedSubDataCollision, ParticleSystemSubEmitterData()); + for(int i = 0; i < state.numCachedSubDataCollision; i++) + { + ParticleSystem* subEmitter = subEmittersCollision[i]; + ParticleSystemSubEmitterData& subData = state.cachedSubDataCollision[i]; + subData.emitter = subEmitter; + subEmitter->m_EmissionModule.GetEmissionDataCopy(&subData.emissionData); + subEmitter->m_State->SetIsSubEmitter(true); + subEmitterCount++; + } + ParticleSystem* subEmittersDeath[kParticleSystemMaxSubDeath]; + state.numCachedSubDataDeath = shuriken.m_SubModule->GetSubEmitterPtrsDeath(&subEmittersDeath[0]); + state.cachedSubDataDeath = ALLOC_TEMP_MANUAL(ParticleSystemSubEmitterData, state.numCachedSubDataDeath); + std::uninitialized_fill (state.cachedSubDataDeath, state.cachedSubDataDeath + state.numCachedSubDataDeath, ParticleSystemSubEmitterData()); + for(int i = 0; i < state.numCachedSubDataDeath; i++) + { + ParticleSystem* subEmitter = subEmittersDeath[i]; + ParticleSystemSubEmitterData& subData = state.cachedSubDataDeath[i]; + subData.emitter = subEmitter; + subEmitter->m_EmissionModule.GetEmissionDataCopy(&subData.emissionData); + subEmitter->m_State->SetIsSubEmitter(true); + subEmitterCount++; + } + } + return subEmitterCount; +} + +int ParticleSystem::CalculateMaximumSubEmitterEmitCount(ParticleSystem& shuriken, ParticleSystemState& state, float deltaTime, bool fixedTimeStep) +{ + // Save emission state + const ParticleSystemEmissionState orgEmissionState = state.emissionState; + ParticleSystemEmissionState emissionState; + + // Simulate emission + float timeStep = GetTimeStep(deltaTime, fixedTimeStep); + DebugAssert(timeStep > 0.0001f); + + // @TODO: Maybe we can go back to just picking the conservative solution, since no longer use this for scrubbing/prewarm we shouldn't allocate that much. + + + int existingParticleCount = shuriken.GetParticleCount(); + int totalPossibleEmits = 0; + float acc = deltaTime; + float fromT = state.t; + while(acc >= timeStep) + { + acc -= timeStep; + float toT = fromT + (deltaTime - acc); + float dt; + const float length = shuriken.m_ReadOnlyState->lengthInSec; + if(shuriken.m_ReadOnlyState->looping) + { + toT = fmodf (toT, length); + dt = timeStep; + } + else + { + toT = min(toT, length); + dt = toT - fromT; + } + + size_t numContinuous; + const Vector3f emitVelocity = state.emitterVelocity; + existingParticleCount += EmitFromModules(shuriken, *shuriken.m_ReadOnlyState, emissionState, numContinuous, emitVelocity, fromT, toT, dt); + totalPossibleEmits += existingParticleCount; + fromT = toT; + } + + totalPossibleEmits += existingParticleCount; + + // Restore emission state + state.emissionState = orgEmissionState; + + const int kExtra = 5; // Give a few extra to avoid denying due to rounding etc. + return totalPossibleEmits + kExtra; +} + +// @TODO: what about chained effects? +float ParticleSystem::CalculateSubEmitterMaximumLifeTime(float parentLifeTime) const +{ + ParticleSystem* subEmitters[kParticleSystemMaxSubTotal]; + m_SubModule->GetSubEmitterPtrs(subEmitters); + + float maxLifeTime = 0.0f; + for(int i = 0; i < kParticleSystemMaxSubTotal; i++) + { + if (subEmitters[i] && (subEmitters[i] != this)) + { + const float emitterMaximumLifetime = subEmitters[i]->m_InitialModule.GetLifeTimeCurve().FindMinMax().y; + maxLifeTime = max(maxLifeTime, parentLifeTime + emitterMaximumLifetime); + maxLifeTime = std::max(maxLifeTime, subEmitters[i]->CalculateSubEmitterMaximumLifeTime(parentLifeTime + emitterMaximumLifetime)); + } + } + return maxLifeTime; +} + +void ParticleSystem::SmartReset () +{ + Super::SmartReset(); + AddParticleSystemRenderer (); +} + +void ParticleSystem::AddParticleSystemRenderer () +{ + if (GameObject* go = GetGameObjectPtr ()) + { + ParticleSystemRenderer* renderer = go->QueryComponent (ParticleSystemRenderer); + if (renderer == NULL) + { + string error; + AddComponent (*go, ClassID(ParticleSystemRenderer), NULL, &error); + if (error.empty ()) + go->GetComponent (ParticleSystemRenderer).SetMaterial (GetDefaultParticleMaterial(), 0); + else + LogString (Format("%s", error.c_str())); + } + } +} + + +void ParticleSystem::SetUsesRotationalSpeed() +{ + ParticleSystemParticles& ps0 = m_Particles[kParticleBuffer0]; + if(!ps0.usesRotationalSpeed) + ps0.SetUsesRotationalSpeed (); +#if UNITY_EDITOR + ParticleSystemParticles& ps1 = m_Particles[kParticleBuffer1]; + if(!ps1.usesRotationalSpeed) + ps1.SetUsesRotationalSpeed (); +#endif + ParticleSystemParticles& pss = m_ParticlesStaging; + if(!pss.usesRotationalSpeed) + pss.SetUsesRotationalSpeed (); +} + +void ParticleSystem::SetUsesAxisOfRotation() +{ + ParticleSystemParticles& ps0 = m_Particles[kParticleBuffer0]; + if(!ps0.usesAxisOfRotation) + ps0.SetUsesAxisOfRotation (); +#if UNITY_EDITOR + ParticleSystemParticles& ps1 = m_Particles[kParticleBuffer1]; + if(!ps1.usesAxisOfRotation) + ps1.SetUsesAxisOfRotation (); +#endif + ParticleSystemParticles& pss = m_ParticlesStaging; + if(!pss.usesAxisOfRotation) + pss.SetUsesAxisOfRotation (); +} + +void ParticleSystem::SetUsesEmitAccumulator(int numAccumulators) +{ + m_Particles[kParticleBuffer0].SetUsesEmitAccumulator (numAccumulators); +#if UNITY_EDITOR + m_Particles[kParticleBuffer1].SetUsesEmitAccumulator (numAccumulators); +#endif + m_ParticlesStaging.SetUsesEmitAccumulator (numAccumulators); +} + +bool ParticleSystem::GetIsDistanceEmitter() const +{ + return (EmissionModule::kEmissionTypeDistance == m_EmissionModule.GetEmissionDataRef().type); +} + +// check if the system would like to use any raycasting in this frame +bool ParticleSystem::SystemWannaRayCast(const ParticleSystem& system) +{ + return system.IsActive() && system.m_CollisionModule && system.m_CollisionModule->GetEnabled() && system.m_CollisionModule->IsWorldCollision() && system.m_RayBudgetState.ReceiveRays(); +} + +// check if the system will actually use any raycasting in this frame +bool ParticleSystem::SystemWillRayCast(const ParticleSystem& system) +{ + return system.IsActive() && system.m_CollisionModule && system.m_CollisionModule->GetEnabled() && system.m_CollisionModule->IsWorldCollision() && (system.GetRayBudget() > 0); +} + +// dole out ray budgets to each system that will do raycasting +void ParticleSystem::AssignRayBudgets() +{ + int activeCount = gParticleSystemManager.activeEmitters.size(); + + // count the jobs and update quality setting + int numApproximateWorldCollisionJobs = 0; + for(int i = 0; i < activeCount; i++) + { + ParticleSystem& system = *gParticleSystemManager.activeEmitters[i]; + system.m_RayBudgetState.SetQuality( system.m_CollisionModule->GetQuality() ); + system.SetRayBudget( 0 ); + if ( SystemWannaRayCast( system ) ) + { + if ( system.m_CollisionModule->IsApproximate() ) + { + numApproximateWorldCollisionJobs++; + } + else + { + // high quality always get to trace all rays! + system.SetRayBudget( (int)system.GetParticleCount() ); + } + } + } + if (numApproximateWorldCollisionJobs<1) return; + + // assign ray budget to particle systems + int totalBudget = GetQualitySettings().GetCurrent().particleRaycastBudget; + int raysPerSystem = std::max( 0, totalBudget / numApproximateWorldCollisionJobs ); + for(int i = 0; i < activeCount; i++) + { + ParticleSystem& system = *gParticleSystemManager.activeEmitters[i]; + if ( SystemWannaRayCast( system ) && system.m_CollisionModule->IsApproximate() ) + { + const int rays = std::min((int)system.GetParticleCount(),raysPerSystem); + system.SetRayBudget( rays ); + totalBudget = std::max( totalBudget-rays, 0); + } + } + + // assign any remaining rays + // TODO: possibly better to sort and go through the list incrementally updating the per system budget, than doing this hacky two pass thing + for(int i = 0; i < activeCount; i++) + { + ParticleSystem& system = *gParticleSystemManager.activeEmitters[i]; + if ( SystemWannaRayCast( system ) && system.m_CollisionModule->IsApproximate() ) + { + const int rays = std::min(totalBudget,(int)system.GetParticleCount()-system.GetRayBudget()); + system.SetRayBudget( system.GetRayBudget()+rays ); + totalBudget -= rays; + } + system.m_RayBudgetState.Update(); + } +} + +void ParticleSystem::BeginUpdateAll () +{ + const float deltaTimeEpsilon = 0.0001f; + float deltaTime = GetDeltaTime(); + if(deltaTime < deltaTimeEpsilon) + return; + + PROFILER_AUTO(gParticleSystemProfile, NULL) + + for(int i = 0; i < gParticleSystemManager.activeEmitters.size(); i++) + { + ParticleSystem& system = *gParticleSystemManager.activeEmitters[i]; + + if (!system.IsActive ()) + { + AssertStringObject( "UpdateParticle system should not happen on disabled vGO", &system); + system.RemoveFromManager(); + continue; + } + +#if ENABLE_MULTITHREADED_PARTICLES + system.m_State->recordSubEmits = true; +#else + system.m_State->recordSubEmits = false; +#endif + Update0 (system, *system.m_ReadOnlyState, *system.m_State, deltaTime, false); + } + + gParticleSystemManager.needSync = true; + + // make sure ray budgets are assigned for the frame + ParticleSystem::AssignRayBudgets(); + +#if ENABLE_MULTITHREADED_PARTICLES + JobScheduler& scheduler = GetJobScheduler(); + int activeCount = gParticleSystemManager.activeEmitters.size(); + + // count the jobs + int numActiveWorldCollisionJobs = 0; + for(int i = 0; i < activeCount; i++) + { + ParticleSystem& system = *gParticleSystemManager.activeEmitters[i]; + if ( system.GetRayBudget() > 0 ) + numActiveWorldCollisionJobs++; + } + + // add collision jobs + gParticleSystemManager.worldCollisionJobGroup = scheduler.BeginGroup(numActiveWorldCollisionJobs); + gParticleSystemManager.jobGroup = scheduler.BeginGroup(activeCount-numActiveWorldCollisionJobs); + for(int i = 0; i < activeCount; i++) + { + ParticleSystem& system = *gParticleSystemManager.activeEmitters[i]; + system.GetThreadScratchPad().deltaTime = deltaTime; + if ( system.GetRayBudget() > 0 ) + scheduler.SubmitJob (gParticleSystemManager.worldCollisionJobGroup, ParticleSystem::UpdateFunction, &system, NULL); + else + scheduler.SubmitJob (gParticleSystemManager.jobGroup, ParticleSystem::UpdateFunction, &system, NULL); + } + scheduler.WaitForGroup(gParticleSystemManager.worldCollisionJobGroup); +#else + for(int i = 0; i < gParticleSystemManager.activeEmitters.size(); i++) + { + //printf_console("BeginUpdateAll [%d]:\n",i); + ParticleSystem& system = *gParticleSystemManager.activeEmitters[i]; + system.Update1 (system, system.GetParticles((int)ParticleSystem::kParticleBuffer0), deltaTime, false, false); + } +#endif + +} + +void ParticleSystem::EndUpdateAll () +{ + SyncJobs(); + + // messages + for (int i = 0; i < gParticleSystemManager.activeEmitters.size(); ++i) + { + ParticleSystem& system = *gParticleSystemManager.activeEmitters[i]; + if (!system.IsActive ()) + continue; + if (!system.m_CollisionModule->GetUsesCollisionMessages ()) + continue; + ParticleSystemParticles& ps = system.GetParticles((int)ParticleSystem::kParticleBuffer0); + ps.collisionEvents.SwapCollisionEventArrays (); + ps.collisionEvents.SendCollisionEvents (system); + } + + // Remove emitters that are finished (no longer emitting) + for(int i = 0; i < gParticleSystemManager.activeEmitters.size();) + { + ParticleSystem& system = *gParticleSystemManager.activeEmitters[i]; + ParticleSystemState& state = *system.m_State; + const size_t particleCount = system.GetParticleCount(); + if ((particleCount == 0) && state.playing && state.stopEmitting) + { + // collision subemitters may not have needRestart==true when being restarted + // from a paused state + //Assert (state.needRestart); + state.playing = false; + system.RemoveFromManager(); + continue; + } + + i++; + } + + // Interpolate in the editor for very low preview speeds +#if UNITY_EDITOR + for(int i = 0; i < gParticleSystemManager.activeEmitters.size(); i++) + ParticleSystemEditor::PerformInterpolationStep(gParticleSystemManager.activeEmitters[i]); +#endif +} + +void ParticleSystem::StartParticles(ParticleSystem& system, ParticleSystemParticles& ps, const float prevT, const float t, const float dt, const size_t numContinuous, size_t amountOfParticlesToEmit, float frameOffset) +{ + if (amountOfParticlesToEmit <= 0) + return; + + const ParticleSystemReadOnlyState& roState = *system.m_ReadOnlyState; + ParticleSystemState& state = *system.m_State; + size_t fromIndex = system.AddNewParticles(ps, amountOfParticlesToEmit); + const Matrix4x4f localToWorld = !roState.useLocalSpace ? state.localToWorld : Matrix4x4f::identity; + StartModules (system, roState, state, state.emissionState, state.emitterVelocity, localToWorld, ps, fromIndex, dt, t, numContinuous, frameOffset); +} + +void ParticleSystem::StartParticlesProcedural(ParticleSystem& system, ParticleSystemParticles& ps, const float prevT, const float t, const float dt, const size_t numContinuous, size_t amountOfParticlesToEmit, float frameOffset) +{ + DebugAssert(CheckSupportsProcedural(system)); + + ParticleSystemState& state = *system.m_State; + + int numParticlesRecorded = 0; + for (int i=0;i<state.emitReplay.size();i++) + numParticlesRecorded += state.emitReplay[i].particlesToEmit; + + float emissionOffset = state.emissionState.m_ToEmitAccumulator; + float emissionGap = state.emissionState.m_ParticleSpacing * dt; + amountOfParticlesToEmit = system.LimitParticleCount(numParticlesRecorded + amountOfParticlesToEmit) - numParticlesRecorded; + + if (amountOfParticlesToEmit > 0) + { + UInt32 randomSeed = 0; +#if UNITY_EDITOR + ParticleSystemEditor::UpdateRandomSeed(system); + randomSeed = ParticleSystemEditor::GetRandomSeed(system); +#endif + state.emitReplay.push_back(ParticleSystemEmitReplay(t, amountOfParticlesToEmit, emissionOffset, emissionGap, numContinuous, randomSeed)); + } +} + +void ParticleSystem::StartModules (ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, const ParticleSystemEmissionState& emissionState, Vector3f initialVelocity, const Matrix4x4f& matrix, ParticleSystemParticles& ps, size_t fromIndex, float dt, float t, size_t numContinuous, float frameOffset) +{ +#if UNITY_EDITOR + ParticleSystemEditor::UpdateRandomSeed(system); + ParticleSystemEditor::ApplyRandomSeed(system, ParticleSystemEditor::GetRandomSeed(system)); +#endif + + system.m_InitialModule.Start (roState, state, ps, matrix, fromIndex, t); + if(system.m_ShapeModule.GetEnabled()) + system.m_ShapeModule.Start (roState, state, ps, matrix, fromIndex, t); + + DebugAssert(roState.lengthInSec > 0.0001f); + const float normalizedT = t / roState.lengthInSec; + DebugAssert (normalizedT >= 0.0f); + DebugAssert (normalizedT <= 1.0f); + + size_t count = ps.array_size(); + const Vector3f velocityOffset = system.m_InitialModule.GetInheritVelocity() * initialVelocity; + for(size_t q = fromIndex; q < count; q++) + { + const float randomValue = GenerateRandom(ps.randomSeed[q] + kParticleSystemStartSpeedCurveId); + ps.velocity[q] *= Evaluate (system.m_InitialModule.GetSpeedCurve(), normalizedT, randomValue); + ps.velocity[q] += velocityOffset; + } + + for(size_t q = fromIndex; q < count; ) // array size changes + { + // subFrameOffset allows particles to be spawned at increasing times, thus spacing particles within a single frame. + // For example if you spawn particles with high velocity you will get a continous streaming instead of a clump of particles. + const int particleIndex = q - fromIndex; + float subFrameOffset = (particleIndex < numContinuous) ? (float(particleIndex) + emissionState.m_ToEmitAccumulator) * emissionState.m_ParticleSpacing : 0.0f; + DebugAssert(subFrameOffset >= -0.01f); + DebugAssert(subFrameOffset <= 1.5f); // Not 1 due to possibly really bad precision + subFrameOffset = clamp01(subFrameOffset); + + // Update from curves and apply forces etc. + UpdateModulesPreSimulationIncremental (system, roState, state, ps, q, q+1, subFrameOffset * dt); + + // Position change due to where the emitter was at time of emission + ps.position[q] -= initialVelocity * (frameOffset + subFrameOffset) * dt; + + // Position, rotation and energy change due to how much the particle has travelled since time of emission + // @TODO: Call Simulate instead? + ps.lifetime[q] -= subFrameOffset * dt; + if((ps.lifetime[q] < 0.0f) && (count > 0)) + { + KillParticle(roState, state, ps, q, count); + continue; + } + + ps.position[q] += (ps.velocity[q] + ps.animatedVelocity[q]) * subFrameOffset * dt; + + if(ps.usesRotationalSpeed) + ps.rotation[q] += ps.rotationalSpeed[q] * subFrameOffset * dt; + + if(system.m_SubModule->GetEnabled()) + system.m_SubModule->Update (roState, state, ps, q, q+1, subFrameOffset * dt); + + ++q; + } + ps.array_resize(count); +} + +void ParticleSystem::UpdateModulesPreSimulationIncremental (const ParticleSystem& system, const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& particles, const size_t fromIndex, const size_t toIndex, float dt) +{ + const size_t count = particles.array_size(); + system.m_InitialModule.Update (roState, state, particles, fromIndex, toIndex, dt); + if(system.m_RotationModule->GetEnabled()) + system.m_RotationModule->Update (roState, state, particles, fromIndex, toIndex); + if(system.m_VelocityModule->GetEnabled()) + system.m_VelocityModule->Update (roState, state, particles, fromIndex, toIndex); + if(system.m_ForceModule->GetEnabled()) + system.m_ForceModule->Update (roState, state, particles, fromIndex, toIndex, dt); + if(system.m_ExternalForcesModule->GetEnabled()) + system.m_ExternalForcesModule->Update (roState, state, particles, fromIndex, toIndex, dt); + if(system.m_ClampVelocityModule->GetEnabled()) + system.m_ClampVelocityModule->Update (roState, state, particles, fromIndex, toIndex); + if(system.m_RotationBySpeedModule->GetEnabled()) + system.m_RotationBySpeedModule->Update (roState, state, particles, fromIndex, toIndex); + + Assert(count >= toIndex); + Assert(particles.array_size() == count); +} + +void ParticleSystem::UpdateModulesPostSimulationIncremental (const ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& particles, const size_t fromIndex, float dt) +{ + const size_t count = particles.array_size(); + if(system.m_SubModule->GetEnabled()) + system.m_SubModule->Update (roState, state, particles, fromIndex, particles.array_size(), dt); + Assert(count == particles.array_size()); + + if(system.m_CollisionModule->GetEnabled()) + { +#if !ENABLE_MULTITHREADED_PARTICLES + PROFILER_AUTO(gParticleSystemUpdateCollisions, NULL) +#endif + system.m_CollisionModule->Update (roState, state, particles, fromIndex, dt); + } +} + +void ParticleSystem::UpdateModulesNonIncremental (const ParticleSystem& system, const ParticleSystemParticles& particles, ParticleSystemParticlesTempData& psTemp, size_t fromIndex, size_t toIndex) +{ + Assert(particles.array_size() == psTemp.particleCount); + + for(int i = fromIndex; i < toIndex; i++) + psTemp.color[i] = particles.color[i]; + for(int i = fromIndex; i < toIndex; i++) + psTemp.size[i] = particles.size[i]; + + if(system.m_ColorModule->GetEnabled()) + system.m_ColorModule->Update (particles, psTemp.color, fromIndex, toIndex); + if(system.m_ColorBySpeedModule->GetEnabled()) + system.m_ColorBySpeedModule->Update (particles, psTemp.color, fromIndex, toIndex); + if(system.m_SizeModule->GetEnabled()) + system.m_SizeModule->Update (particles, psTemp.size, fromIndex, toIndex); + if(system.m_SizeBySpeedModule->GetEnabled()) + system.m_SizeBySpeedModule->Update (particles, psTemp.size, fromIndex, toIndex); + + if (gGraphicsCaps.needsToSwizzleVertexColors) + std::transform(&psTemp.color[fromIndex], &psTemp.color[toIndex], &psTemp.color[fromIndex], SwizzleColorForPlatform); + + if(system.m_UVModule->GetEnabled()) + { + // No other systems used sheet index yet, allocate! + if(!psTemp.sheetIndex) + { + psTemp.sheetIndex = ALLOC_TEMP_MANUAL(float, psTemp.particleCount); + for(int i = 0; i < fromIndex; i++) + psTemp.sheetIndex[i] = 0.0f; + } + system.m_UVModule->Update (particles, psTemp.sheetIndex, fromIndex, toIndex); + } + else if(psTemp.sheetIndex) // if this is present with disabled module, that means we have a combined buffer with one system not using UV module, just initislize to 0.0f + for(int i = fromIndex; i < toIndex; i++) + psTemp.sheetIndex[i] = 0.0f; +} + +void ParticleSystem::UpdateModulesIncremental (const ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& particles, size_t fromIndex, float dt) +{ + UpdateModulesPreSimulationIncremental (system, roState, state, particles, fromIndex, particles.array_size(), dt); + SimulateParticles(roState, state, particles, fromIndex, dt); + UpdateModulesPostSimulationIncremental (system, roState, state, particles, fromIndex, dt); +} + +void ParticleSystem::Update0 (ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, float dt, bool fixedTimeStep) +{ + const Transform& transform = system.GetComponent (Transform); + Vector3f oldPosition = state.localToWorld.GetPosition(); + state.localToWorld = transform.GetLocalToWorldMatrixNoScale (); + Matrix4x4f::Invert_General3D(state.localToWorld, state.worldToLocal); + + if (IS_CONTENT_NEWER_OR_SAME (kUnityVersion4_1_a1)) + state.emitterScale = transform.GetWorldScaleLossy(); + else + state.emitterScale = transform.GetLocalScale(); + + if (state.playing && (dt > 0.0001f)) + { + const Vector3f position = transform.GetPosition(); + + if (roState.useLocalSpace) + state.emitterVelocity = Vector3f::zero; + else + state.emitterVelocity = (position - oldPosition) / dt; + } + + AddStagingBuffer(system); + + ParticleSystemRenderer* renderer = system.QueryComponent(ParticleSystemRenderer); + if(renderer && !renderer->GetScreenSpaceRotation()) + ParticleSystemRenderer::SetUsesAxisOfRotationRec(system, true); + + if(system.m_RotationModule->GetEnabled() || system.m_RotationBySpeedModule->GetEnabled()) + system.SetUsesRotationalSpeed(); + + int subEmitterBirthTypeCount = system.m_SubModule->GetSubEmitterTypeCount(kParticleSystemSubTypeBirth); + if(system.m_SubModule->GetEnabled() && subEmitterBirthTypeCount) + system.SetUsesEmitAccumulator (subEmitterBirthTypeCount); + + int subEmitterCount = SetupSubEmitters(system, *system.m_State); + +#if ENABLE_MULTITHREADED_PARTICLES + if(state.recordSubEmits) + { + const int numCommands = min(ParticleSystem::CalculateMaximumSubEmitterEmitCount(system, *system.m_State, dt, fixedTimeStep) * subEmitterCount, MAX_NUM_SUB_EMIT_CMDS); + ParticleSystemSubEmitCmdBuffer& buffer = system.m_State->subEmitterCommandBuffer; + Assert(NULL == buffer.commands); + buffer.commands = ALLOC_TEMP_MANUAL(SubEmitterEmitCommand, numCommands); + buffer.commandCount = 0; + buffer.maxCommandCount = numCommands; + } +#endif + + if(system.m_CollisionModule->GetEnabled()) + system.m_CollisionModule->AllocateAndCache(roState, state); + if(system.m_ExternalForcesModule->GetEnabled()) + system.m_ExternalForcesModule->AllocateAndCache(roState, state); +} + +void ParticleSystem::Update1 (ParticleSystem& system, ParticleSystemParticles& ps, float dt, bool fixedTimeStep, bool useProcedural, int rayBudget) +{ + PROFILER_AUTO(gParticleSystemJobProfile, NULL) + + const ParticleSystemReadOnlyState& roState = *system.m_ReadOnlyState; + ParticleSystemState& state = *system.m_State; + state.rayBudget = rayBudget; + + // Exposed through script + dt *= std::max<float> (roState.speed, 0.0f); + + float timeStep = GetTimeStep(dt, fixedTimeStep); + if(timeStep < 0.00001f) + return; + + if (state.playing) + { + state.accumulatedDt += dt; + + if(system.GetIsDistanceEmitter()) + { + float t = state.t + state.accumulatedDt; + const float length = roState.lengthInSec; + t = roState.looping ? fmodf(t, length) : min(t, length); + size_t numContinuous = 0; + size_t amountOfParticlesToEmit = system.EmitFromModules (system, roState, state.emissionState, numContinuous, state.emitterVelocity, state.t, t, dt); + StartParticles(system, ps, state.t, t, dt, numContinuous, amountOfParticlesToEmit, 0.0f); + } + + Update1Incremental(system, roState, state, ps, 0, timeStep, useProcedural); + + if (useProcedural) + UpdateProcedural(system, roState, state, ps); + } + + UpdateBounds(system, ps, state); +} + +void ParticleSystem::Update2 (ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, bool fixedTimeStep) +{ + if(state.subEmitterCommandBuffer.commandCount > 0) + PlaybackSubEmitterCommandBuffer(system, state, fixedTimeStep); + state.ClearSubEmitterCommandBuffer(); + + CollisionModule::FreeCache(state); + ExternalForcesModule::FreeCache(state); + + AddStagingBuffer(system); + + // + // Update renderer + ParticleSystemRenderer* renderer = system.QueryComponent(ParticleSystemRenderer); + if (renderer) + { + MinMaxAABB result; + ParticleSystemRenderer::CombineBoundsRec(system, result, true); + renderer->Update (result); + } +} + +// Returns true if update loop is executed at least once +void ParticleSystem::Update1Incremental(ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, size_t fromIndex, float dt, bool useProcedural) +{ + ApplyStartDelay (state.delayT, state.accumulatedDt); + + int numTimeSteps = 0; + const int numTimeStepsTotal = int(state.accumulatedDt / dt); + + while (state.accumulatedDt >= dt) + { + const float prevT = state.t; + state.Tick (roState, dt); + const float t = state.t; + const bool timePassedDuration = t >= (roState.lengthInSec); + const float frameOffset = float(numTimeStepsTotal - 1 - numTimeSteps); + + if(!roState.looping && timePassedDuration) + system.Stop(); + + // Update simulation + if (!useProcedural) + UpdateModulesIncremental(system, roState, state, ps, fromIndex, dt); + else + for (int i=0;i<state.emitReplay.size();i++) + state.emitReplay[i].aliveTime += dt; + + // Emission + bool emit = !system.GetIsDistanceEmitter() && !state.stopEmitting; + if(emit) + { + size_t numContinuous = 0; + size_t amountOfParticlesToEmit = system.EmitFromModules (system, roState, state.emissionState, numContinuous, state.emitterVelocity, prevT, t, dt); + if(useProcedural) + StartParticlesProcedural(system, ps, prevT, t, dt, numContinuous, amountOfParticlesToEmit, frameOffset); + else + StartParticles(system, ps, prevT, t, dt, numContinuous, amountOfParticlesToEmit, frameOffset); + } + + state.accumulatedDt -= dt; + + AddStagingBuffer(system); + + // Workaround for external forces being dependent on AABB (need to update it before the next time step) + if(!useProcedural && (state.accumulatedDt >= dt) && system.m_ExternalForcesModule->GetEnabled()) + UpdateBounds(system, ps, state); + + numTimeSteps++; + } +} + +void ParticleSystem::UpdateProcedural (ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps) +{ + DebugAssert(CheckSupportsProcedural(system)); + + // Clear all particles + ps.array_resize(0); + + const Matrix4x4f localToWorld = !roState.useLocalSpace ? state.localToWorld : Matrix4x4f::identity; + + // Emit all particles + for (int i=0; i<state.emitReplay.size(); i++) + { + const ParticleSystemEmitReplay& emit = state.emitReplay[i]; + +#if UNITY_EDITOR + ParticleSystemEditor::ApplyRandomSeed(system, emit.randomSeed); +#endif + //@TODO: remove passing m_State since that is very dangerous when making things procedural compatible + size_t previousParticleCount = ps.array_size(); + system.m_InitialModule.GenerateProcedural (roState, state, ps, emit); + + //@TODO: This can be moved out of the emit all particles loop... + if (system.m_ShapeModule.GetEnabled()) + system.m_ShapeModule.Start (roState, state, ps, localToWorld, previousParticleCount, emit.t); + + // Apply gravity & integrated velocity after shape module so that it picks up any changes done in shapemodule (for example rotating the velocity) + Vector3f gravity = system.m_InitialModule.GetGravity(roState, state); + float particleIndex = 0.0f; + const size_t particleCount = ps.array_size(); + for (int q = previousParticleCount; q < particleCount; q++) + { + const float normalizedT = emit.t / roState.lengthInSec; + ps.velocity[q] *= Evaluate (system.m_InitialModule.GetSpeedCurve(), normalizedT, GenerateRandom(ps.randomSeed[q] + kParticleSystemStartSpeedCurveId)); + Vector3f velocity = ps.velocity[q]; + float frameOffset = (particleIndex + emit.emissionOffset) * emit.emissionGap * float(particleIndex < emit.numContinuous); + float aliveTime = emit.aliveTime + frameOffset; + + ps.position[q] += velocity * aliveTime + gravity * aliveTime * aliveTime * 0.5F; + ps.velocity[q] += gravity * aliveTime; + + particleIndex += 1.0f; + } + + // If no particles were emitted we can get rid of the emit replay state... + if (previousParticleCount == ps.array_size()) + { + state.emitReplay[i] = state.emitReplay.back(); + state.emitReplay.pop_back(); + i--; + } + } + + if (system.m_RotationModule->GetEnabled()) + system.m_RotationModule->UpdateProcedural (state, ps); + + if (system.m_VelocityModule->GetEnabled()) + system.m_VelocityModule->UpdateProcedural (roState, state, ps); + + if (system.m_ForceModule->GetEnabled()) + system.m_ForceModule->UpdateProcedural (roState, state, ps); + + // Modules that are not supported by procedural + DebugAssert(!system.m_RotationBySpeedModule->GetEnabled()); // find out if possible to support it + DebugAssert(!system.m_ClampVelocityModule->GetEnabled()); // unsupported: (Need to compute velocity by deriving position curves...), possible to support? + DebugAssert(!system.m_CollisionModule->GetEnabled()); + DebugAssert(!system.m_SubModule->GetEnabled()); // find out if possible to support it + DebugAssert(!system.m_ExternalForcesModule->GetEnabled()); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// // Editor only +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if UNITY_EDITOR +void ParticleSystem::TransformChanged() +{ + if(!IsWorldPlaying() && ParticleSystemEditor::GetResimulation()) + { + const Transform& transform = GetComponent (Transform); + Matrix4x4f newMatrix = transform.GetLocalToWorldMatrixNoScale (); + newMatrix.SetPosition(Vector3f::zero); + Matrix4x4f oldMatrix = m_State->localToWorld; + oldMatrix.SetPosition(Vector3f::zero); + bool rotationChanged = !CompareApproximately(oldMatrix, newMatrix); + if(m_ReadOnlyState->useLocalSpace || rotationChanged) + ParticleSystemEditor::PerformCompleteResimulation(this); + } +} +#endif + diff --git a/Runtime/Graphics/ParticleSystem/ParticleSystem.h b/Runtime/Graphics/ParticleSystem/ParticleSystem.h new file mode 100644 index 0000000..10c8f2a --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleSystem.h @@ -0,0 +1,298 @@ +#ifndef SHURIKEN_H +#define SHURIKEN_H + +#include "Runtime/BaseClasses/GameObject.h" +#include "ParticleSystemParticle.h" +#include "Runtime/Utilities/LinkedList.h" +#include "Modules/InitialModule.h" +#include "Modules/ShapeModule.h" +#include "Modules/EmissionModule.h" + +struct ParticleSystemEmitReplay; +struct ParticleSystemState; +class ParticleSystemModule; +class SizeModule; +class RotationModule; +class ColorModule; +class UVModule; +class VelocityModule; +class ForceModule; +class ExternalForcesModule; +class ClampVelocityModule; +class SizeBySpeedModule; +class RotationBySpeedModule; +class ColorBySpeedModule; +class CollisionModule; +class SubModule; + +enum ParticleSystemSimulationSpace { + kSimLocal = 0, + kSimWorld = 1, +}; + +// TODO: rename +struct ParticleSystemThreadScratchPad +{ + ParticleSystemThreadScratchPad () + : deltaTime (1.0f) + {} + + float deltaTime; +}; + +enum { kGoodQualityDelay = 0, kMediumQualityDelay = 0, kLowQualityDelay = 4 }; +struct RayBudgetState +{ + void SetQuality(int quality) + { + if ( m_Quality != quality ) + { + switch (quality) + { + case 0: + m_QualityFrameDelay = kGoodQualityDelay; + break; + case 1: + m_QualityFrameDelay = kMediumQualityDelay; + break; + case 2: + m_QualityFrameDelay = kLowQualityDelay; + break; + default: + m_QualityFrameDelay = kGoodQualityDelay; + } + m_FramesRemaining = m_QualityFrameDelay; + m_Quality = quality; + }; + } + RayBudgetState() { m_Quality=m_QualityFrameDelay=m_FramesRemaining=0; } + bool ReceiveRays() const { return m_FramesRemaining==0; } + void Update() { m_FramesRemaining = ( m_FramesRemaining ? m_FramesRemaining-1 : m_QualityFrameDelay ); } + int m_Quality; + int m_QualityFrameDelay; + int m_FramesRemaining; +}; + +class ParticleSystem : public Unity::Component +{ +public: + REGISTER_DERIVED_CLASS (ParticleSystem, Unity::Component) + DECLARE_OBJECT_SERIALIZE (ParticleSystem) + + enum + { + kParticleBuffer0, +#if UNITY_EDITOR // Double buffered + interpolation + kParticleBuffer1, +#endif + kNumParticleBuffers, + }; + + ParticleSystem (MemLabelId label, ObjectCreationMode mode); + + // ParticleSystem (); declared-by-macro + + void SmartReset (); + void AddParticleSystemRenderer (); + + void Deactivate (DeactivateOperation operation); + void AwakeFromLoad (AwakeFromLoadMode awakeMode); + + #if UNITY_EDITOR + // ParticleSystemRenderer always goes with ParticleSystem + virtual int GetCoupledComponentClassID() const { return ClassID(ParticleSystemRenderer); } + #endif + + + static void* UpdateFunction (void* rawData); + + static void BeginUpdateAll(); + static void EndUpdateAll(); + static void Update(ParticleSystem& system, float deltaTime, bool fixedTimeStep, bool useProcedural, int rayBudget = 0); + + static bool SystemWannaRayCast(const ParticleSystem& system); + static bool SystemWillRayCast(const ParticleSystem& system); + static void AssignRayBudgets(); + + static void SyncJobs(); + + static void Emit(ParticleSystem& system, const SubEmitterEmitCommand& command, ParticleSystemEmitMode emitMode); + + // Client interface + void Simulate (float t, bool restart); // Fastforwards the particle system by simulating particles over given period of time, then pauses it. + void Play (bool autoPrewarm = true); + void Stop (); + void Pause (); + void Emit (int count); + void EmitParticleExternal(ParticleSystemParticle* particle); + void Clear (bool updateBounds = true); + void AutoPrewarm(); + + bool IsAlive () const; + bool IsPlaying () const; + bool IsPaused () const; + bool IsStopped () const; + float GetStartDelay() const; + void SetStartDelay(float value); + bool GetLoop () const; + void SetLoop (bool loop); + bool GetPlayOnAwake () const; + void SetPlayOnAwake (bool playOnAwake); + ParticleSystemSimulationSpace GetSimulationSpace () const; + void SetSimulationSpace (ParticleSystemSimulationSpace simulationSpace); + float GetSecPosition () const; + void SetSecPosition (float pos); + float GetLengthInSec () const; + void SetPlaybackSpeed (float speed); + float GetPlaybackSpeed () const; + void SetRayBudget (int rayBudget); + int GetRayBudget() const; + + bool GetEnableEmission() const; + void SetEnableEmission(bool value); + float GetEmissionRate() const; + void SetEmissionRate(float value); + float GetStartSpeed() const; + void SetStartSpeed(float value); + float GetStartSize() const; + void SetStartSize(float value); + ColorRGBAf GetStartColor() const; + void SetStartColor(ColorRGBAf value); + float GetStartRotation() const; + void SetStartRotation(float value); + float GetStartLifeTime() const; + void SetStartLifeTime(float value); + float GetGravityModifier() const; + void SetGravityModifier(float value); + UInt32 GetRandomSeed() const; + void SetRandomSeed(UInt32 value); + int GetMaxNumParticles() const; + void SetMaxNumParticles(int value); + + ShapeModule& GetShapeModule () { return m_ShapeModule; } + + + Matrix4x4f GetLocalToWorldMatrix() const; + + void GetNumTiles(int& uvTilesX, int& uvTilesY) const; + + void AllocateAllStructuresOfArrays(); + void SetParticlesExternal (ParticleSystemParticle* particles, int size); + void GetParticlesExternal (ParticleSystemParticle* particles, int size); + + int GetSafeCollisionEventSize () const; + int GetCollisionEventsExternal (int instanceID, MonoParticleCollisionEvent* collisionEvents, int size) const; + + ParticleSystemParticles& GetParticles (int index = -1); + const ParticleSystemParticles& GetParticles (int index = -1) const; + size_t GetParticleCount () const; + + ParticleSystemThreadScratchPad& GetThreadScratchPad () { return m_ThreadScratchpad; } + + static void InitializeClass (); + static void CleanupClass () {}; + + void DidModifyMesh (); + void DidDeleteMesh (); + +#if UNITY_EDITOR + void TransformChanged(); +#endif + + void RendererBecameVisible(); + void RendererBecameInvisible(); + + static size_t EmitFromData (ParticleSystemEmissionState& emissionState, size_t& numContinuous, const ParticleSystemEmissionData& emissionData, const Vector3f velocity, float fromT, float toT, float dt, float length); +private: + static void Update0 (ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, float dt, bool fixedTimeStep); + static void Update1 (ParticleSystem& system, ParticleSystemParticles& ps, float dt, bool fixedTimeStep, bool useProcedural, int rayBudget = 0); + static void Update2 (ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, bool fixedTimeStep); + static void Update1Incremental(ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, size_t fromIndex, float dt, bool useProcedural); + + static size_t EmitFromModules (const ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemEmissionState& emissionState, size_t& numContinuous, const Vector3f velocity, float fromT, float toT, float dt); + static void StartModules (ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, const ParticleSystemEmissionState& emissionState, Vector3f initialVelocity, const Matrix4x4f& matrix, ParticleSystemParticles& ps, size_t fromIndex, float dt, float t, size_t numContinuous, float frameOffset); + static void StartParticles(ParticleSystem& system, ParticleSystemParticles& ps, const float prevT, const float t, const float dt, const size_t numContinuous, size_t amountOfParticlesToEmit, float frameOffset); + static void StartParticlesProcedural(ParticleSystem& system, ParticleSystemParticles& ps, const float prevT, const float t, const float dt, const size_t numContinuous, size_t amountOfParticlesToEmit, float frameOffset); + static void UpdateProcedural(ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps); + static void UpdateModulesPreSimulationIncremental (const ParticleSystem& system, const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex, float dt); + static void UpdateModulesPostSimulationIncremental (const ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, float dt); + static void UpdateModulesIncremental (const ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, size_t fromIndex, float dt); + static void UpdateModulesNonIncremental (const ParticleSystem& system, const ParticleSystemParticles& ps, ParticleSystemParticlesTempData& psTemp, size_t fromIndex, size_t toIndex); + static void SimulateParticles (const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, float dt); + static void PlaybackSubEmitterCommandBuffer(const ParticleSystem& shuriken, ParticleSystemState& state, bool fixedTimeStep); + static void UpdateBounds(const ParticleSystem& system, const ParticleSystemParticles& ps, ParticleSystemState& state); + + static void AddStagingBuffer(ParticleSystem& system); + static int CalculateMaximumSubEmitterEmitCount(ParticleSystem& shuriken, ParticleSystemState& state, float deltaTime, bool fixedTimeStep); + static int SetupSubEmitters(ParticleSystem& shuriken, ParticleSystemState& state); + + static bool CheckSupportsProcedural(const ParticleSystem& system); + static bool DetermineSupportsProcedural(const ParticleSystem& system); + + bool ComputePrewarmStartParameters(float& prewarmTime, float t); + + void SetUsesAxisOfRotation(); + void SetUsesEmitAccumulator(int numAccumulators); + void SetUsesRotationalSpeed(); + void KeepUpdating(); + void Cull(); + + size_t AddNewParticles(ParticleSystemParticles& particles, size_t newParticles) const; + size_t LimitParticleCount(size_t requestSize) const; + float CalculateSubEmitterMaximumLifeTime(float parentLifeTime) const; + + bool GetIsDistanceEmitter() const; + + void AddToManager(); + void RemoveFromManager(); + + ParticleSystemParticles m_Particles[kNumParticleBuffers]; + ParticleSystemParticles m_ParticlesStaging; // staging buffer for emitting into the emitter + ParticleSystemReadOnlyState* m_ReadOnlyState; + ParticleSystemState* m_State; + InitialModule m_InitialModule; + ShapeModule m_ShapeModule; + EmissionModule m_EmissionModule; + + // Dependent on energy value + SizeModule* m_SizeModule; + RotationModule* m_RotationModule; // @TODO: Requires outputs angular velocity and thus requires integration (Inconsistent with other modules in this group) + ColorModule* m_ColorModule; + UVModule* m_UVModule; + + // Dependent on energy value + VelocityModule* m_VelocityModule; + ForceModule* m_ForceModule; + ExternalForcesModule* m_ExternalForcesModule; + + // Depends on velocity and modifies velocity + ClampVelocityModule* m_ClampVelocityModule; + + // Dependent on velocity value + SizeBySpeedModule* m_SizeBySpeedModule; + RotationBySpeedModule* m_RotationBySpeedModule; + ColorBySpeedModule* m_ColorBySpeedModule; + + // Dependent on a position and velocity + CollisionModule* m_CollisionModule; + + SubModule* m_SubModule; + + ParticleSystemThreadScratchPad m_ThreadScratchpad; + + RayBudgetState m_RayBudgetState; + int m_RayBudget; + +private: + int m_EmittersIndex; + +#if UNITY_EDITOR +public: + int m_EditorRandomSeedIndex; + ListNode<ParticleSystem> m_EditorListNode; + friend class ParticleSystemEditor; +#endif + friend class ParticleSystemRenderer; +}; + +#endif // SHURIKEN_H diff --git a/Runtime/Graphics/ParticleSystem/ParticleSystemCommon.h b/Runtime/Graphics/ParticleSystem/ParticleSystemCommon.h new file mode 100644 index 0000000..fd59987 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleSystemCommon.h @@ -0,0 +1,48 @@ +#ifndef PARTICLESYSTEMCOMMON_H +#define PARTICLESYSTEMCOMMON_H + +enum ParticleSystemSubType +{ + kParticleSystemSubTypeBirth, + kParticleSystemSubTypeCollision, + kParticleSystemSubTypeDeath, +}; + +enum ParticleSystemEmitMode +{ + kParticleSystemEMDirect, + kParticleSystemEMStaging, +}; + +enum +{ + kParticleSystemMaxSubBirth = 2, + kParticleSystemMaxSubCollision = 2, + kParticleSystemMaxSubDeath = 2, + kParticleSystemMaxSubTotal = kParticleSystemMaxSubBirth + kParticleSystemMaxSubCollision + kParticleSystemMaxSubDeath, +}; + +// Curve id's needed to offset randomness for curves, to avoid visible patterns due to only storing 1 random value per particle +enum ParticleSystemRandomnessIds +{ + // Curves + kParticleSystemClampVelocityCurveId = 0x13371337, + kParticleSystemForceCurveId = 0x12460f3b, + kParticleSystemRotationCurveId = 0x6aed452e, + kParticleSystemRotationBySpeedCurveId = 0xdec4aea1, + kParticleSystemStartSpeedCurveId = 0x96aa4de3, + kParticleSystemSizeCurveId = 0x8d2c8431, + kParticleSystemSizeBySpeedCurveId = 0xf3857f6f, + kParticleSystemVelocityCurveId = 0xe0fbd834, + kParticleSystemUVCurveId = 0x13740583, + + // Gradient + kParticleSystemColorGradientId = 0x591bc05c, + kParticleSystemColorByVelocityGradientId = 0x40eb95e4, + + // Misc + kParticleSystemMeshSelectionId = 0xbc524e5f, + kParticleSystemUVRowSelectionId = 0xaf502044, +}; + +#endif // PARTICLESYSTEMCOMMON_H diff --git a/Runtime/Graphics/ParticleSystem/ParticleSystemCurves.cpp b/Runtime/Graphics/ParticleSystem/ParticleSystemCurves.cpp new file mode 100644 index 0000000..3ac445c --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleSystemCurves.cpp @@ -0,0 +1,196 @@ +#include "UnityPrefix.h" +#include "ParticleSystemCurves.h" +#include "Runtime/Math/Polynomials.h" +#include "Runtime/Math/Vector2.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" + +// Calculates the min max range of an animation curve analytically +static void CalculateCurveRangesValue(Vector2f& minMaxValue, const AnimationCurve& curve) +{ + const int keyCount = curve.GetKeyCount(); + if (keyCount == 0) + return; + + if (keyCount == 1) + { + CalculateMinMax(minMaxValue, curve.GetKey(0).value); + } + else + { + int segmentCount = keyCount - 1; + CalculateMinMax(minMaxValue, curve.GetKey(0).value); + for (int i = 0;i<segmentCount;i++) + { + DebugAssert(i+1 < keyCount); + AnimationCurve::Cache cache; + curve.CalculateCacheData(cache, i, i + 1, 0.0F); + + // Differentiate polynomial + float a = 3.0f * cache.coeff[0]; + float b = 2.0f * cache.coeff[1]; + float c = 1.0f * cache.coeff[2]; + + const float start = curve.GetKey(i).time; + const float end = curve.GetKey(i+1).time; + + float roots[2]; + int numRoots = QuadraticPolynomialRootsGeneric(a, b, c, roots[0], roots[1]); + for(int r = 0; r < numRoots; r++) + if((roots[r] >= 0.0f) && ((roots[r] + start) < end)) + CalculateMinMax(minMaxValue, Polynomial::EvalSegment(roots[r], cache.coeff)); + CalculateMinMax(minMaxValue, Polynomial::EvalSegment(end-start, cache.coeff)); + } + } +} + +bool BuildCurves (MinMaxOptimizedPolyCurves& polyCurves, const MinMaxAnimationCurves& editorCurves, float scalar, short minMaxState) +{ + bool isOptimizedCurve = polyCurves.max.BuildOptimizedCurve(editorCurves.max, scalar); + if ((minMaxState != kMMCTwoCurves) && (minMaxState != kMMCTwoConstants)) + isOptimizedCurve = isOptimizedCurve && polyCurves.min.BuildOptimizedCurve(editorCurves.max, scalar); + else + isOptimizedCurve = isOptimizedCurve && polyCurves.min.BuildOptimizedCurve(editorCurves.min, scalar); + return isOptimizedCurve; +} + +void BuildCurves (MinMaxPolyCurves& polyCurves, const MinMaxAnimationCurves& editorCurves, float scalar, short minMaxState) +{ + polyCurves.max.BuildCurve(editorCurves.max, scalar); + if ((minMaxState != kMMCTwoCurves) && (minMaxState != kMMCTwoConstants)) + polyCurves.min.BuildCurve(editorCurves.max, scalar); + else + polyCurves.min.BuildCurve(editorCurves.min, scalar); +} + +bool CurvesSupportProcedural (const MinMaxAnimationCurves& editorCurves, short minMaxState) +{ + bool isValid = PolynomialCurve::IsValidCurve(editorCurves.max); + if ((minMaxState != kMMCTwoCurves) && (minMaxState != kMMCTwoConstants)) + return isValid; + else + return isValid && PolynomialCurve::IsValidCurve(editorCurves.min); +} + + +void MinMaxOptimizedPolyCurves::Integrate () +{ + max.Integrate (); + min.Integrate (); +} + +void MinMaxOptimizedPolyCurves::DoubleIntegrate () +{ + max.DoubleIntegrate (); + min.DoubleIntegrate (); +} + +Vector2f MinMaxOptimizedPolyCurves::FindMinMaxIntegrated() +{ + Vector2f minRange = min.FindMinMaxIntegrated(); + Vector2f maxRange = max.FindMinMaxIntegrated(); + Vector2f result = Vector2f(std::min(minRange.x, maxRange.x), std::max(minRange.y, maxRange.y)); + return result; +} + +Vector2f MinMaxOptimizedPolyCurves::FindMinMaxDoubleIntegrated() +{ + Vector2f minRange = min.FindMinMaxDoubleIntegrated(); + Vector2f maxRange = max.FindMinMaxDoubleIntegrated(); + Vector2f result = Vector2f(std::min(minRange.x, maxRange.x), std::max(minRange.y, maxRange.y)); + return result; +} + +void MinMaxPolyCurves::Integrate () +{ + max.Integrate (); + min.Integrate (); +} + +void MinMaxPolyCurves::DoubleIntegrate () +{ + max.DoubleIntegrate (); + min.DoubleIntegrate (); +} + +Vector2f MinMaxPolyCurves::FindMinMaxIntegrated() +{ + Vector2f minRange = min.FindMinMaxIntegrated(); + Vector2f maxRange = max.FindMinMaxIntegrated(); + Vector2f result = Vector2f(std::min(minRange.x, maxRange.x), std::max(minRange.y, maxRange.y)); + return result; +} + +Vector2f MinMaxPolyCurves::FindMinMaxDoubleIntegrated() +{ + Vector2f minRange = min.FindMinMaxDoubleIntegrated(); + Vector2f maxRange = max.FindMinMaxDoubleIntegrated(); + Vector2f result = Vector2f(std::min(minRange.x, maxRange.x), std::max(minRange.y, maxRange.y)); + return result; +} + +MinMaxCurve::MinMaxCurve () +: scalar (1.0f) +, minMaxState (kMMCScalar) +, isOptimizedCurve(false) +{ + SetPolynomialCurveToValue (editorCurves.max, polyCurves.max, 1.0f); + SetPolynomialCurveToValue (editorCurves.min, polyCurves.min, 0.0f); +} + +Vector2f MinMaxCurve::FindMinMax() const +{ + Vector2f result = Vector2f(std::numeric_limits<float>::infinity (), -std::numeric_limits<float>::infinity ()); + CalculateCurveRangesValue(result, editorCurves.max); + if((minMaxState == kMMCTwoCurves) || (minMaxState == kMMCTwoConstants)) + CalculateCurveRangesValue(result, editorCurves.min); + return result * GetScalar(); +} + +Vector2f MinMaxCurve::FindMinMaxIntegrated() const +{ + if(IsOptimized()) + { + MinMaxOptimizedPolyCurves integrated = polyCurves; + integrated.Integrate(); + return integrated.FindMinMaxIntegrated(); + } + else + { + DebugAssert(CurvesSupportProcedural (editorCurves, minMaxState)); + MinMaxPolyCurves integrated; + BuildCurves(integrated, editorCurves, GetScalar(), minMaxState); + integrated.Integrate(); + return integrated.FindMinMaxIntegrated(); + } +} + +Vector2f MinMaxCurve::FindMinMaxDoubleIntegrated() const +{ + if(IsOptimized()) + { + MinMaxOptimizedPolyCurves doubleIntegrated = polyCurves; + doubleIntegrated.DoubleIntegrate(); + return doubleIntegrated.FindMinMaxDoubleIntegrated(); + } + else + { + DebugAssert(CurvesSupportProcedural (editorCurves, minMaxState)); + MinMaxPolyCurves doubleIntegrated; + BuildCurves(doubleIntegrated, editorCurves, GetScalar(), minMaxState); + doubleIntegrated.DoubleIntegrate(); + return doubleIntegrated.FindMinMaxDoubleIntegrated(); + } +} + +template<class TransferFunction> +void MinMaxCurve::Transfer (TransferFunction& transfer) +{ + transfer.Transfer (scalar, "scalar"); + transfer.Transfer (editorCurves.max, "maxCurve"); + transfer.Transfer (editorCurves.min, "minCurve"); + TRANSFER (minMaxState); transfer.Align (); + if (transfer.IsReading ()) + isOptimizedCurve = BuildCurves(polyCurves, editorCurves, scalar, minMaxState); +} +INSTANTIATE_TEMPLATE_TRANSFER(MinMaxCurve) diff --git a/Runtime/Graphics/ParticleSystem/ParticleSystemCurves.h b/Runtime/Graphics/ParticleSystem/ParticleSystemCurves.h new file mode 100644 index 0000000..5d59eb2 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleSystemCurves.h @@ -0,0 +1,187 @@ +#ifndef SHURIKENCURVES_H +#define SHURIKENCURVES_H + +#include "ParticleSystemParticle.h" +#include "PolynomialCurve.h" +#include "Runtime/Math/AnimationCurve.h" + + +struct MinMaxCurve; + +// Some profile numbers from a run with 250,000 particles evaluating 3 velocity properties each on Intel i7-2600 CPU @ 3.4 GHz +// Scalar: 4.6 ms +// Optimized curve: 7.2 ms +// Random between 2 scalars: 9.5 ms +// Random between 2 curves: 9.5 ms +// Non-optimized curve: 10.0 ms +// Random between 2 non-optimized curves: 12.0 ms + +enum ParticleSystemCurveEvalMode +{ + kEMScalar, + kEMOptimized, + kEMOptimizedMinMax, + kEMSlow, +}; + +enum MinMaxCurveState +{ + kMMCScalar = 0, + kMMCCurve = 1, + kMMCTwoCurves = 2, + kMMCTwoConstants = 3 +}; + +struct MinMaxOptimizedPolyCurves +{ + void Integrate(); + void DoubleIntegrate(); + Vector2f FindMinMaxIntegrated(); + Vector2f FindMinMaxDoubleIntegrated(); + + OptimizedPolynomialCurve max; + OptimizedPolynomialCurve min; +}; + +inline float EvaluateIntegrated (const MinMaxOptimizedPolyCurves& curves, float t, float factor) +{ + const float v0 = curves.min.EvaluateIntegrated (t); + const float v1 = curves.max.EvaluateIntegrated (t); + return Lerp (v0, v1, factor); +} + +inline float EvaluateDoubleIntegrated (const MinMaxOptimizedPolyCurves& curves, float t, float factor) +{ + const float v0 = curves.min.EvaluateDoubleIntegrated (t); + const float v1 = curves.max.EvaluateDoubleIntegrated (t); + return Lerp (v0, v1, factor); +} + +struct MinMaxPolyCurves +{ + void Integrate(); + void DoubleIntegrate(); + Vector2f FindMinMaxIntegrated(); + Vector2f FindMinMaxDoubleIntegrated(); + + PolynomialCurve max; + PolynomialCurve min; +}; + +inline float EvaluateIntegrated (const MinMaxPolyCurves& curves, float t, float factor) +{ + const float v0 = curves.min.EvaluateIntegrated (t); + const float v1 = curves.max.EvaluateIntegrated (t); + return Lerp (v0, v1, factor); +} + +inline float EvaluateDoubleIntegrated (const MinMaxPolyCurves& curves, float t, float factor) +{ + const float v0 = curves.min.EvaluateDoubleIntegrated (t); + const float v1 = curves.max.EvaluateDoubleIntegrated (t); + return Lerp (v0, v1, factor); +} + +struct MinMaxAnimationCurves +{ + bool SupportsProcedural (); + + AnimationCurve max; + AnimationCurve min; +}; + +bool BuildCurves (MinMaxOptimizedPolyCurves& polyCurves, const MinMaxAnimationCurves& editorCurves, float scalar, short minMaxState); +void BuildCurves (MinMaxPolyCurves& polyCurves, const MinMaxAnimationCurves& editorCurves, float scalar, short minMaxState); +bool CurvesSupportProcedural (const MinMaxAnimationCurves& editorCurves, short minMaxState); + +struct MinMaxCurve +{ + MinMaxOptimizedPolyCurves polyCurves; +private: + float scalar; // Since scalar is baked into the optimized curve we use the setter function to modify it. + +public: + short minMaxState; // see enum MinMaxCurveState + bool isOptimizedCurve; + + MinMaxAnimationCurves editorCurves; + + MinMaxCurve (); + + inline float GetScalar() const { return scalar; } + inline void SetScalar(float value) { scalar = value; BuildCurves(polyCurves, editorCurves, scalar, minMaxState); } + + bool IsOptimized () const { return isOptimizedCurve; } + bool UsesMinMax () const { return (minMaxState == kMMCTwoCurves) || (minMaxState == kMMCTwoConstants); } + + DEFINE_GET_TYPESTRING (MinMaxCurve) + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + + Vector2f FindMinMax() const; + Vector2f FindMinMaxIntegrated() const; + Vector2f FindMinMaxDoubleIntegrated() const; +}; + +inline float EvaluateSlow (const MinMaxCurve& curve, float t, float factor) +{ + const float v = curve.editorCurves.max.Evaluate(t) * curve.GetScalar (); + if (curve.minMaxState == kMMCTwoCurves) + return Lerp (curve.editorCurves.min.Evaluate(t) * curve.GetScalar (), v, factor); + else + return v; +} + +template<ParticleSystemCurveEvalMode mode> +inline float Evaluate (const MinMaxCurve& curve, float t, float factor = 1.0F) +{ + if(mode == kEMScalar) + { + return curve.GetScalar(); + } + if(mode == kEMOptimized) + { + DebugAssert(curve.isOptimizedCurve); + return curve.polyCurves.max.Evaluate (t); + } + else if (mode == kEMOptimizedMinMax) + { + DebugAssert(curve.isOptimizedCurve); + const float v0 = curve.polyCurves.min.Evaluate (t); + const float v1 = curve.polyCurves.max.Evaluate (t); + return Lerp (v0, v1, factor); + } + else if (mode == kEMSlow) + { + return EvaluateSlow (curve, t, factor); + } +} + +inline float Evaluate (const MinMaxCurve& curve, float t, float randomValue = 1.0F) +{ + if (curve.minMaxState == kMMCScalar) + return curve.GetScalar (); + + if (curve.minMaxState == kMMCTwoConstants) + return Lerp ( curve.editorCurves.min.GetKey(0).value * curve.GetScalar (), + curve.editorCurves.max.GetKey(0).value * curve.GetScalar (), randomValue); + + DebugAssert(t <= 1.0F && t >= 0.0F); + if (curve.isOptimizedCurve) + return Evaluate<kEMOptimizedMinMax> (curve, t, randomValue); + else + return Evaluate<kEMSlow> (curve, t, randomValue); +} + +struct DualMinMax3DPolyCurves +{ + MinMaxOptimizedPolyCurves optX; + MinMaxOptimizedPolyCurves optY; + MinMaxOptimizedPolyCurves optZ; + MinMaxPolyCurves x; + MinMaxPolyCurves y; + MinMaxPolyCurves z; +}; + +#endif // SHURIKENCURVES_H diff --git a/Runtime/Graphics/ParticleSystem/ParticleSystemGradients.cpp b/Runtime/Graphics/ParticleSystem/ParticleSystemGradients.cpp new file mode 100644 index 0000000..efe95c0 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleSystemGradients.cpp @@ -0,0 +1,27 @@ +#include "UnityPrefix.h" +#include "ParticleSystemGradients.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" + +MinMaxGradient::MinMaxGradient() +: minMaxState (kMMGColor), minColor (255,255,255,255), maxColor (255,255,255,255) +{ +} + +void MinMaxGradient::InitializeOptimized(OptimizedMinMaxGradient& g) +{ + maxGradient.InitializeOptimized(g.max); + if(minMaxState == kMMGRandomBetweenTwoGradients) + minGradient.InitializeOptimized(g.min); +} + +template<class TransferFunction> +void MinMaxGradient::Transfer (TransferFunction& transfer) +{ + TRANSFER (maxGradient); + TRANSFER (minGradient); + TRANSFER (minColor); + TRANSFER (maxColor); + TRANSFER (minMaxState); transfer.Align (); +} +INSTANTIATE_TEMPLATE_TRANSFER(MinMaxGradient) diff --git a/Runtime/Graphics/ParticleSystem/ParticleSystemGradients.h b/Runtime/Graphics/ParticleSystem/ParticleSystemGradients.h new file mode 100644 index 0000000..bb74cde --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleSystemGradients.h @@ -0,0 +1,87 @@ +#ifndef SHURIKENGRADIENTS_H +#define SHURIKENGRADIENTS_H + +#include "Runtime/Math/Gradient.h" + +enum MinMaxGradientEvalMode +{ + kGEMGradient, + kGEMGradientMinMax, + kGEMSlow, +}; + +enum MinMaxGradientState +{ + kMMGColor = 0, + kMMGGradient = 1, + kMMGRandomBetweenTwoColors = 2, + kMMGRandomBetweenTwoGradients = 3 +}; + +struct OptimizedMinMaxGradient +{ + OptimizedGradient max; + OptimizedGradient min; +}; + +inline ColorRGBA32 EvaluateGradient (const OptimizedMinMaxGradient& g, float t) +{ + return g.max.Evaluate(t); +} + +inline ColorRGBA32 EvaluateRandomGradient (const OptimizedMinMaxGradient& g, float t, UInt32 factor) +{ + return Lerp (g.min.Evaluate(t), g.max.Evaluate(t), factor); +} + +struct MinMaxGradient +{ + GradientNEW maxGradient; + GradientNEW minGradient; + ColorRGBA32 minColor; // we have the colors separate to prevent destroying the gradients + ColorRGBA32 maxColor; + short minMaxState; // see enum State + + MinMaxGradient(); + + DEFINE_GET_TYPESTRING (MinMaxGradient) + + void InitializeOptimized(OptimizedMinMaxGradient& g); + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); +}; + +inline ColorRGBA32 EvaluateColor (const MinMaxGradient& gradient) +{ + return gradient.maxColor; +} + +inline ColorRGBA32 EvaluateGradient (const MinMaxGradient& gradient, float t) +{ + return gradient.maxGradient.Evaluate(t); +} + +inline ColorRGBA32 EvaluateRandomColor (const MinMaxGradient& gradient, UInt32 factor) +{ + return Lerp (gradient.minColor, gradient.maxColor, factor); +} + +inline ColorRGBA32 EvaluateRandomGradient (const MinMaxGradient& gradient, float t, UInt32 factor) +{ + return Lerp (gradient.minGradient.Evaluate(t), gradient.maxGradient.Evaluate(t), factor); +} + +inline ColorRGBA32 Evaluate (const MinMaxGradient& gradient, float t, UInt32 factor = 0xff) +{ + if (gradient.minMaxState == kMMGColor) + return EvaluateColor(gradient); + else if (gradient.minMaxState == kMMGGradient) + return EvaluateGradient(gradient, t); + else if (gradient.minMaxState == kMMGRandomBetweenTwoColors) + return EvaluateRandomColor(gradient, factor); + else // gradient.minMaxState == kMMGRandomBetweenTwoGradients + return EvaluateRandomGradient(gradient, t, factor); +} + +#endif // SHURIKENGRADIENTS_H diff --git a/Runtime/Graphics/ParticleSystem/ParticleSystemParticle.cpp b/Runtime/Graphics/ParticleSystem/ParticleSystemParticle.cpp new file mode 100644 index 0000000..d10bee0 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleSystemParticle.cpp @@ -0,0 +1,323 @@ +#include "UnityPrefix.h" +#include "ParticleSystemParticle.h" + +#include "Runtime/Dynamics/Collider.h" +#include "Runtime/Scripting/ScriptingUtility.h" +#include "Runtime/Threads/Thread.h" + +void InitializeEmitAccumulator(dynamic_array<float>& emitAccumulator, const size_t count) +{ + emitAccumulator.resize_uninitialized(count); + memset(emitAccumulator.begin(), 0, count * sizeof(float)); +} + +ParticleCollisionEvent::ParticleCollisionEvent (const Vector3f& intersection, const Vector3f& normal, const Vector3f& velocity, int colliderInstanceID, int rigidBodyOrColliderInstanceID) +{ + m_Intersection = intersection; + m_Normal = normal; + m_Velocity = velocity; + m_ColliderInstanceID = colliderInstanceID; + m_RigidBodyOrColliderInstanceID = rigidBodyOrColliderInstanceID; +} + +size_t ParticleSystemParticles::GetParticleSize() +{ + // @TODO: How do we guard against elements we remove + return sizeof(ParticleSystemParticle); +} + +void ParticleSystemParticles::SetUsesAxisOfRotation() +{ + usesAxisOfRotation = true; + const size_t count = position.size(); + axisOfRotation.resize_uninitialized(count); + for(int i = 0; i < count; i++) + axisOfRotation[i] = Vector3f::yAxis; +} + +void ParticleSystemParticles::SetUsesRotationalSpeed() +{ + usesRotationalSpeed = true; + const size_t count = position.size(); + rotationalSpeed.resize_uninitialized(count); + for(int i = 0; i < count; i++) + rotationalSpeed[i] = 0.0f; +} + +void ParticleSystemParticles::SetUsesCollisionEvents (bool wantUsesCollisionEvents) +{ + if (usesCollisionEvents == wantUsesCollisionEvents) return; + usesCollisionEvents = wantUsesCollisionEvents; + if (!usesCollisionEvents) + { + collisionEvents.Clear (); + } +} + +bool ParticleSystemParticles::GetUsesCollisionEvents () const +{ + return usesCollisionEvents; +} + +void ParticleSystemParticles::SetUsesEmitAccumulator(int numAccumulators) +{ + Assert(numAccumulators <= kParticleSystemMaxNumEmitAccumulators); + const size_t count = position.size(); + for(int i = numEmitAccumulators; i < numAccumulators; i++) + InitializeEmitAccumulator(emitAccumulator[i], count); + + numEmitAccumulators = numAccumulators; +} + +void ParticleSystemParticles::CopyFromArrayAOS(ParticleSystemParticle* particles, int size) +{ + Assert(usesAxisOfRotation); + Assert(numEmitAccumulators == kParticleSystemMaxNumEmitAccumulators); + for(int i = 0; i < size; i++) + { + (*this).position[i] = particles[i].position; + (*this).velocity[i] = particles[i].velocity; + (*this).animatedVelocity[i] = particles[i].animatedVelocity; + (*this).axisOfRotation[i] = particles[i].axisOfRotation; + (*this).rotation[i] = particles[i].rotation; + if(usesRotationalSpeed) + (*this).rotationalSpeed[i] = particles[i].rotationalSpeed; + (*this).size[i] = particles[i].size; + (*this).color[i] = particles[i].color; + (*this).randomSeed[i] = particles[i].randomSeed; + (*this).lifetime[i] = particles[i].lifetime; + (*this).startLifetime[i] = particles[i].startLifetime; + for(int acc = 0; acc < kParticleSystemMaxNumEmitAccumulators; acc++) + (*this).emitAccumulator[acc][i] = particles[i].emitAccumulator[acc]; + } +} + +void ParticleSystemParticles::CopyToArrayAOS(ParticleSystemParticle* particles, int size) +{ + Assert(usesAxisOfRotation); + Assert(numEmitAccumulators == kParticleSystemMaxNumEmitAccumulators); + for(int i = 0; i < size; i++) + { + particles[i].position = (*this).position[i]; + particles[i].velocity = (*this).velocity[i]; + particles[i].animatedVelocity = (*this).animatedVelocity[i]; + particles[i].axisOfRotation = (*this).axisOfRotation[i]; + particles[i].rotation = (*this).rotation[i]; + if(usesRotationalSpeed) + particles[i].rotationalSpeed = (*this).rotationalSpeed[i]; + particles[i].size = (*this).size[i]; + particles[i].color = (*this).color[i]; + particles[i].randomSeed = (*this).randomSeed[i]; + particles[i].lifetime = (*this).lifetime[i]; + particles[i].startLifetime = (*this).startLifetime[i]; + for(int acc = 0; acc < kParticleSystemMaxNumEmitAccumulators; acc++) + particles[i].emitAccumulator[acc] = (*this).emitAccumulator[acc][i]; + } +} + +void ParticleSystemParticles::AddParticle(ParticleSystemParticle* particle) +{ + const size_t count = array_size(); + array_resize(count+1); + position[count] = particle->position; + velocity[count] = particle->velocity; + animatedVelocity[count] = Vector3f::zero; + lifetime[count] = particle->lifetime; + startLifetime[count] = particle->startLifetime; + size[count] = particle->size; + rotation[count] = particle->rotation; + if(usesRotationalSpeed) + rotationalSpeed[count] = particle->rotationalSpeed; + color[count] = particle->color; + randomSeed[count] = particle->randomSeed; + if(usesAxisOfRotation) + axisOfRotation[count] = particle->axisOfRotation; + for(int acc = 0; acc < numEmitAccumulators; acc++) + emitAccumulator[acc][count] = 0.0f; +} + +size_t ParticleSystemParticles::array_size () const +{ + return position.size(); +} + +void ParticleSystemParticles::array_resize (size_t i) +{ + position.resize_uninitialized(i); + velocity.resize_uninitialized(i); + animatedVelocity.resize_uninitialized(i); + rotation.resize_uninitialized(i); + if(usesRotationalSpeed) + rotationalSpeed.resize_uninitialized(i); + size.resize_uninitialized(i); + color.resize_uninitialized(i); + randomSeed.resize_uninitialized(i); + lifetime.resize_uninitialized(i); + startLifetime.resize_uninitialized(i); + if(usesAxisOfRotation) + axisOfRotation.resize_uninitialized(i); + for(int acc = 0; acc < numEmitAccumulators; acc++) + emitAccumulator[acc].resize_uninitialized(i); +} + +void ParticleSystemParticles::element_swap(size_t i, size_t j) +{ + std::swap(position[i], position[j]); + std::swap(velocity[i], velocity[j]); + std::swap(animatedVelocity[i], animatedVelocity[j]); + std::swap(rotation[i], rotation[j]); + if(usesRotationalSpeed) + std::swap(rotationalSpeed[i], rotationalSpeed[j]); + std::swap(size[i], size[j]); + std::swap(color[i], color[j]); + std::swap(randomSeed[i], randomSeed[j]); + std::swap(lifetime[i], lifetime[j]); + std::swap(startLifetime[i], startLifetime[j]); + if(usesAxisOfRotation) + std::swap(axisOfRotation[i], axisOfRotation[j]); + for(int acc = 0; acc < numEmitAccumulators; acc++) + std::swap(emitAccumulator[acc][i], emitAccumulator[acc][j]); +} + +void ParticleSystemParticles::element_assign(size_t i, size_t j) +{ + position[i] = position[j]; + velocity[i] = velocity[j]; + animatedVelocity[i] = animatedVelocity[j]; + rotation[i] = rotation[j]; + if(usesRotationalSpeed) + rotationalSpeed[i] = rotationalSpeed[j]; + size[i] = size[j]; + color[i] = color[j]; + randomSeed[i] = randomSeed[j]; + lifetime[i] = lifetime[j]; + startLifetime[i] = startLifetime[j]; + if(usesAxisOfRotation) + axisOfRotation[i] = axisOfRotation[j]; + for(int acc = 0; acc < numEmitAccumulators; acc++) + emitAccumulator[acc][i] = emitAccumulator[acc][j]; +} + +void ParticleSystemParticles::array_assign_external(void* data, const int numParticles) +{ +#define SHURIKEN_INCREMENT_ASSIGN_PTRS(element, type) { beginPtr = endPtr; endPtr += numParticles * sizeof(type); (element).assign_external((type*)beginPtr, (type*)endPtr); } + + UInt8* beginPtr = 0; + UInt8* endPtr = (UInt8*)data; + + SHURIKEN_INCREMENT_ASSIGN_PTRS(position, Vector3f); + SHURIKEN_INCREMENT_ASSIGN_PTRS(velocity, Vector3f); + SHURIKEN_INCREMENT_ASSIGN_PTRS(animatedVelocity, Vector3f); + SHURIKEN_INCREMENT_ASSIGN_PTRS(rotation, float); + if(usesRotationalSpeed) + SHURIKEN_INCREMENT_ASSIGN_PTRS(rotationalSpeed, float); + SHURIKEN_INCREMENT_ASSIGN_PTRS(size, float); + SHURIKEN_INCREMENT_ASSIGN_PTRS(color, ColorRGBA32); + SHURIKEN_INCREMENT_ASSIGN_PTRS(randomSeed, UInt32); + SHURIKEN_INCREMENT_ASSIGN_PTRS(lifetime, float); + SHURIKEN_INCREMENT_ASSIGN_PTRS(startLifetime, float); + if(usesAxisOfRotation) + SHURIKEN_INCREMENT_ASSIGN_PTRS(axisOfRotation, Vector3f); + for(int acc = 0; acc < numEmitAccumulators; acc++) + SHURIKEN_INCREMENT_ASSIGN_PTRS(emitAccumulator[acc], float); +#undef SHURIKEN_INCREMENT_ASSIGN_PTRS +} + +void ParticleSystemParticles::array_merge_preallocated(const ParticleSystemParticles& rhs, const int offset, const bool needAxisOfRotation, const bool needEmitAccumulator) +{ +#define SHURIKEN_COPY_DATA(element, type) memcpy(&element[offset], &rhs.element[0], count * sizeof(type)) + + const size_t count = rhs.array_size(); + if(0 == count) + return; + + Assert((rhs.array_size() + offset) <= array_size()); + SHURIKEN_COPY_DATA(position, Vector3f); + SHURIKEN_COPY_DATA(velocity, Vector3f); + SHURIKEN_COPY_DATA(animatedVelocity, Vector3f); + SHURIKEN_COPY_DATA(rotation, float); + if(usesRotationalSpeed) + SHURIKEN_COPY_DATA(rotationalSpeed, float); + SHURIKEN_COPY_DATA(size, float); + SHURIKEN_COPY_DATA(color, ColorRGBA32); + SHURIKEN_COPY_DATA(randomSeed, UInt32); + SHURIKEN_COPY_DATA(lifetime, float); + SHURIKEN_COPY_DATA(startLifetime, float); + + if(needAxisOfRotation) + { + Assert(usesAxisOfRotation && rhs.usesAxisOfRotation); + SHURIKEN_COPY_DATA(axisOfRotation, Vector3f); + } + if(needEmitAccumulator) + { + Assert(numEmitAccumulators && rhs.numEmitAccumulators); + Assert(numEmitAccumulators == rhs.numEmitAccumulators); + for(int acc = 0; acc < numEmitAccumulators; acc++) + SHURIKEN_COPY_DATA(emitAccumulator[acc], float); + } + +#undef SHURIKEN_COPY_DATA +} + + +void ParticleSystemParticles::array_assign(const ParticleSystemParticles& rhs) +{ + position.assign(rhs.position.begin(), rhs.position.end()); + velocity.assign(rhs.velocity.begin(), rhs.velocity.end()); + animatedVelocity.assign(rhs.animatedVelocity.begin(), rhs.animatedVelocity.end()); + rotation.assign(rhs.rotation.begin(), rhs.rotation.end()); + if(usesRotationalSpeed) + rotationalSpeed.assign(rhs.rotationalSpeed.begin(), rhs.rotationalSpeed.end()); + size.assign(rhs.size.begin(), rhs.size.end()); + color.assign(rhs.color.begin(), rhs.color.end()); + randomSeed.assign(rhs.randomSeed.begin(), rhs.randomSeed.end()); + lifetime.assign(rhs.lifetime.begin(), rhs.lifetime.end()); + startLifetime.assign(rhs.startLifetime.begin(), rhs.startLifetime.end()); + if(usesAxisOfRotation) + axisOfRotation.assign(rhs.axisOfRotation.begin(), rhs.axisOfRotation.end()); + for(int acc = 0; acc < numEmitAccumulators; acc++) + emitAccumulator[acc].assign(rhs.emitAccumulator[acc].begin(), rhs.emitAccumulator[acc].end()); +} + +void ParticleSystemParticles::array_lerp(ParticleSystemParticles& output, const ParticleSystemParticles& a, const ParticleSystemParticles& b, float factor) +{ + DebugAssert(a.array_size() == b.array_size()); // else it doesn't really make sense + DebugAssert(a.usesRotationalSpeed == b.usesRotationalSpeed); + + const int count = a.array_size(); + output.array_resize(count); + + // Note: Not all data is interpolated here intentionally, because it doesn't change anything or because it's incorrect + + #define SHURIKEN_LERP_DATA(element, type) for(int i = 0; i < count; i++) output.element[i] = Lerp(a.element[i], b.element[i], factor) + SHURIKEN_LERP_DATA(position, Vector3f); + SHURIKEN_LERP_DATA(velocity, Vector3f); + SHURIKEN_LERP_DATA(animatedVelocity, Vector3f); + + SHURIKEN_LERP_DATA(rotation, float); + if(a.usesRotationalSpeed) + SHURIKEN_LERP_DATA(rotationalSpeed, float); + SHURIKEN_LERP_DATA(lifetime, float); + + #undef SHURIKEN_LERP_DATA +} + +ParticleSystemParticlesTempData::ParticleSystemParticlesTempData() +:color(0) +,size(0) +,sheetIndex(0) +,particleCount(0) +{} + +void ParticleSystemParticlesTempData::element_swap(size_t i, size_t j) +{ + DebugAssert(i <= particleCount); + DebugAssert(j <= particleCount); + + std::swap(color[i], color[j]); + std::swap(size[i], size[j]); + if(sheetIndex) + std::swap(sheetIndex[i], sheetIndex[j]); +} + diff --git a/Runtime/Graphics/ParticleSystem/ParticleSystemParticle.h b/Runtime/Graphics/ParticleSystem/ParticleSystemParticle.h new file mode 100644 index 0000000..65d6894 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleSystemParticle.h @@ -0,0 +1,116 @@ +#ifndef SHURIKENPARTICLE_H +#define SHURIKENPARTICLE_H + +#include "Runtime/Graphics/ParticleSystem/ParticleCollisionEvents.h" +#include "Runtime/Math/Color.h" +#include "Runtime/Math/Vector3.h" +#include "Runtime/Utilities/dynamic_array.h" + +class Collider; +// Keep in sync with struct ParticleSystem.Particle +enum{ kParticleSystemMaxNumEmitAccumulators = 2 }; + +// TODO: Optimization: +// . Store startLifetime as 1.0f/startLifeTime and store lifetime as 1.0f - lifetime. +// This means that a lot of fdivs turn into muls and mads turns into nothing (NormalizedTime will become cheaper). +// Remember: script must still convert into legacy format. + +// Keep in sync with struct ParticleSystem.Particle +struct ParticleSystemParticle +{ + Vector3f position; + Vector3f velocity; + Vector3f animatedVelocity; + Vector3f axisOfRotation; + float rotation; + float rotationalSpeed; + float size; + ColorRGBA32 color; + UInt32 randomSeed; + float lifetime; + float startLifetime; + float emitAccumulator[kParticleSystemMaxNumEmitAccumulators]; +}; + +typedef dynamic_array<Vector3f> ParticleSystemVector3Array; +typedef dynamic_array<float> ParticleSystemFloatArray; +typedef dynamic_array<ColorRGBA32> ParticleSystemColor32Array; +typedef dynamic_array<UInt32> ParticleSystemUInt32Array; + +// Keep in sync with struct ParticleSystem.Particle +struct ParticleSystemParticles +{ + ParticleSystemParticles() + :numEmitAccumulators(0) + ,usesAxisOfRotation(false) + ,usesRotationalSpeed(false) + ,usesCollisionEvents(false) + ,currentCollisionEventThreadArray(0) + {} + + ParticleSystemVector3Array position; + ParticleSystemVector3Array velocity; + ParticleSystemVector3Array animatedVelocity; // Would actually only need this when modules with force and velocity curves are used + ParticleSystemVector3Array axisOfRotation; + ParticleSystemFloatArray rotation; + ParticleSystemFloatArray rotationalSpeed; + ParticleSystemFloatArray size; + ParticleSystemColor32Array color; + ParticleSystemUInt32Array randomSeed; + ParticleSystemFloatArray lifetime; + ParticleSystemFloatArray startLifetime; + ParticleSystemFloatArray emitAccumulator[kParticleSystemMaxNumEmitAccumulators]; // Usage: Only needed if particle system has time sub emitter + CollisionEvents collisionEvents; + + bool usesAxisOfRotation; + bool usesRotationalSpeed; + bool usesCollisionEvents; + int currentCollisionEventThreadArray; + int numEmitAccumulators; + + void AddParticle(ParticleSystemParticle* particle); + + void SetUsesAxisOfRotation (); + void SetUsesRotationalSpeed(); + + void SetUsesCollisionEvents(bool usesCollisionEvents); + bool GetUsesCollisionEvents() const; + void SetUsesEmitAccumulator (int numAccumulators); + + static size_t GetParticleSize(); + + size_t array_size () const; + void array_resize (size_t i); + void element_swap(size_t i, size_t j); + void element_assign(size_t i, size_t j); + void array_assign_external(void* data, const int numParticles); + void array_merge_preallocated(const ParticleSystemParticles& rhs, const int offset, const bool needAxisOfRotation, const bool needEmitAccumulator); + void array_assign(const ParticleSystemParticles& rhs); + static void array_lerp(ParticleSystemParticles& output, const ParticleSystemParticles& a, const ParticleSystemParticles& b, float factor); + + void CopyFromArrayAOS(ParticleSystemParticle* particles, int size); + void CopyToArrayAOS(ParticleSystemParticle* particles, int size); +}; + +struct ParticleSystemParticlesTempData +{ + ParticleSystemParticlesTempData(); + void element_swap(size_t i, size_t j); + + ColorRGBA32* color; + float* size; + float* sheetIndex; + size_t particleCount; +}; + +inline float NormalizedTime (const ParticleSystemParticles& ps, size_t i) +{ + return (ps.startLifetime[i] - ps.lifetime[i]) / ps.startLifetime[i]; +} + +inline float NormalizedTime (float wholeTime, float currentTime) +{ + return (wholeTime - currentTime) / wholeTime; +} + +#endif // SHURIKENPARTICLE_H diff --git a/Runtime/Graphics/ParticleSystem/ParticleSystemRenderer.cpp b/Runtime/Graphics/ParticleSystem/ParticleSystemRenderer.cpp new file mode 100644 index 0000000..7b14e62 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleSystemRenderer.cpp @@ -0,0 +1,1241 @@ +#include "UnityPrefix.h" +#include "ParticleSystem.h" +#include "ParticleSystemParticle.h" +#include "ParticleSystemRenderer.h" +#include "ParticleSystemUtils.h" +#include "Modules/SubModule.h" +#include "Modules/UVModule.h" +#include "Runtime/Camera/Camera.h" +#include "Runtime/Camera/RenderManager.h" +#include "Runtime/Camera/Renderqueue.h" +#include "Runtime/Shaders/VBO.h" +#include "Runtime/GfxDevice/GfxDevice.h" +#include "Runtime/Graphics/DrawUtil.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "Runtime/Profiler/Profiler.h" +#include "Runtime/Profiler/ExternalGraphicsProfiler.h" +#include "Runtime/Threads/JobScheduler.h" +#include "Runtime/Filters/Misc/LineBuilder.h" +#include "Runtime/Filters/Mesh/LodMesh.h" +#include "Runtime/GfxDevice/ChannelAssigns.h" +#include "Runtime/Graphics/TriStripper.h" +#include "Runtime/Misc/BuildSettings.h" +#include "Runtime/BaseClasses/GameObject.h" + +IMPLEMENT_CLASS_INIT_ONLY (ParticleSystemRenderer) +IMPLEMENT_OBJECT_SERIALIZE (ParticleSystemRenderer) + +PROFILER_INFORMATION(gParticlesSort, "ParticleSystem.Sort", kProfilerParticles) +PROFILER_INFORMATION(gParticlesSingleProfile, "ParticleSystem.RenderSingle", kProfilerParticles) +PROFILER_INFORMATION(gParticlesBatchProfile, "ParticleSystem.RenderBatch", kProfilerParticles) +PROFILER_INFORMATION(gSubmitVBOParticleProfile, "Mesh.SubmitVBO", kProfilerRender) + +#define DEBUG_PARTICLE_SORTING (0) +#if UNITY_WII +#define kMaxNumParticlesPerBatch (65536/6) +#else +#define kMaxNumParticlesPerBatch (min<int>(kDynamicBatchingIndicesThreshold/6, VBO::kMaxQuads)) +#endif + +struct ParticleSystemVertex +{ + Vector3f vert; + Vector3f normal; + ColorRGBA32 color; + Vector2f uv; + Vector4f tangent; // Here, we put 2nd uv + blend factor +}; + +struct ParticleSystemGeomConstInputData +{ + Matrix4x4f m_ViewMatrix; + Vector3f m_CameraVelocity; + Object* m_Renderer; + UInt16 const* m_MeshIndexBuffer[ParticleSystemRendererData::kMaxNumParticleMeshes]; + int m_MeshIndexCount[ParticleSystemRendererData::kMaxNumParticleMeshes]; + int m_NumTilesX; + int m_NumTilesY; + float maxPlaneScale; + float maxOrthoSize; + float numUVFrame; + float animUScale; + float animVScale; + Vector3f xSpan; + Vector3f ySpan; + bool usesSheetIndex; + float bentNormalFactor; + Vector3f bentNormalVector; +}; + +inline void ScaleMatrix(Matrix4x4f& matrix, float scale) +{ + matrix.m_Data[0] *= scale; + matrix.m_Data[1] *= scale; + matrix.m_Data[2] *= scale; + matrix.m_Data[4] *= scale; + matrix.m_Data[5] *= scale; + matrix.m_Data[6] *= scale; + matrix.m_Data[8] *= scale; + matrix.m_Data[9] *= scale; + matrix.m_Data[10] *= scale; +} + +struct ParticleSort +{ + inline static void SetValues(ParticleSort& sort, UInt32 inIndex, int inIntValue) + { + sort.index = inIndex; + sort.intValue = inIntValue; + } + + inline static bool CompareValue (const ParticleSort& left, const ParticleSort& right) + { + return (left.intValue < right.intValue); + } + + inline static void Swap(ParticleSort* oneOfThem, ParticleSort* theOtherOne) + { + ParticleSort temp = *oneOfThem; + *oneOfThem = *theOtherOne; + *theOtherOne = temp; + } + + UInt32 index; + int intValue; +}; + +void GenerateSortIndices(ParticleSort* indices, const Vector3f& distFactor, const ParticleSystemParticles& ps, ParticleSystemSortMode sortMode) +{ + const size_t particleCount = ps.array_size(); + if(IS_CONTENT_NEWER_OR_SAME (kUnityVersion4_0_a1)) + { + if(sortMode == kSSMByDistance) + for(int i = 0; i < particleCount; i++) + ParticleSort::SetValues(indices[i], i, (int)(Dot (distFactor, ps.position[i]) * 40000.0f)); + else if(sortMode == kSSMOldestFirst) + for(int i = 0; i < particleCount; i++) + ParticleSort::SetValues(indices[i], i, (int)((ps.startLifetime[i]- ps.lifetime[i]) * -40000.0f)); + else if(sortMode == kSSMYoungestFirst) + for(int i = 0; i < particleCount; i++) + ParticleSort::SetValues(indices[i], i, (int)((ps.startLifetime[i]- ps.lifetime[i]) * 40000.0f)); + } + else + { + // 3.5 used lifetime - this is pretty broken if you have random lifetimes, as you get random sorting + if(sortMode == kSSMByDistance) + for(int i = 0; i < particleCount; i++) + ParticleSort::SetValues(indices[i], i, (int)(Dot (distFactor, ps.position[i]) * 40000.0f)); + else if(sortMode == kSSMOldestFirst) + for(int i = 0; i < particleCount; i++) + ParticleSort::SetValues(indices[i], i, (int)(ps.lifetime[i] * 40000.0f)); + else if(sortMode == kSSMYoungestFirst) + for(int i = 0; i < particleCount; i++) + ParticleSort::SetValues(indices[i], i, (int)(ps.lifetime[i] * -40000.0f)); + } +} + +template<bool sortTempData> +void ApplySortRemap(ParticleSort* particleSortIndexBuffer, ParticleSystemParticlesTempData* tempData, ParticleSystemParticles& ps) +{ + const size_t count = ps.array_size(); + for(int i = 0; i < count; i++) + { + int dst = particleSortIndexBuffer[i].intValue; + while(i != dst) + { + ParticleSort::Swap(&particleSortIndexBuffer[i], &particleSortIndexBuffer[dst]); + ps.element_swap(i, dst); + if(sortTempData) + tempData->element_swap(i, dst); + + dst = particleSortIndexBuffer[i].intValue; + } + } +} + +void Sort (const Matrix4x4f& matrix, ParticleSystemParticles& ps, ParticleSystemSortMode mode, ParticleSystemParticlesTempData* tempData, bool sortTempData) +{ + PROFILER_AUTO_GFX(gParticlesSort, 0); + + DebugAssert(mode != kSSMNone); + + const Vector3f distFactor = Vector3f (matrix.Get (2, 0), matrix.Get (2, 1), + matrix.Get (2, 2)); + const size_t count = ps.array_size(); + + ParticleSort* particleSortIndexBuffer; + ALLOC_TEMP(particleSortIndexBuffer, ParticleSort, count); + GenerateSortIndices(&particleSortIndexBuffer[0], distFactor, ps, mode); + + // Sort + std::sort(&particleSortIndexBuffer[0], &particleSortIndexBuffer[0] + count, ParticleSort::CompareValue); + + // Create inverse mapping + for(int i = 0; i < count; i++) + particleSortIndexBuffer[particleSortIndexBuffer[i].index].intValue = i; + + if(sortTempData) + ApplySortRemap<true>(particleSortIndexBuffer, tempData, ps); + else + ApplySortRemap<false>(particleSortIndexBuffer, tempData, ps); +} + +struct ParticleMeshData +{ + int vertexCount; + StrideIterator<Vector3f> positions; + StrideIterator<Vector3f> normals; + StrideIterator<Vector4f> tangents; + StrideIterator<ColorRGBA32> colors; + StrideIterator<Vector2f> texCoords; + int indexCount; + const UInt16* indexBuffer; +}; + +template<bool hasNormals, bool hasTangents> +void TransformParticleMesh(const ParticleMeshData& src, ColorRGBA32 particleColor, + const Matrix4x4f& xform, const Matrix4x4f& xformNoScale, UInt8** dest) +{ + for(int vertex = 0; vertex < src.vertexCount; vertex++) + { + // Vertex format is position, color, uv, and optional normals and tangents + xform.MultiplyPoint3(src.positions[vertex], *reinterpret_cast<Vector3f*>(*dest)); + *dest += sizeof(Vector3f); + if (hasNormals) + { + xformNoScale.MultiplyVector3(src.normals[vertex], *reinterpret_cast<Vector3f*>(*dest)); + *dest += sizeof(Vector3f); + } + *reinterpret_cast<ColorRGBA32*>(*dest) = particleColor * src.colors[vertex]; + *dest += sizeof(ColorRGBA32); + *reinterpret_cast<Vector2f*>(*dest) = src.texCoords[vertex]; + *dest += sizeof(Vector2f); + // Tangent is last in vertex format + if (hasTangents) + { + Vector3f newTangent = xformNoScale.MultiplyVector3((const Vector3f&)src.tangents[vertex]); + *reinterpret_cast<Vector4f*>(*dest) = Vector4f(newTangent, src.tangents[vertex].w); + *dest += sizeof(Vector4f); + } + } +} + + +ParticleSystemRenderer::ParticleSystemRenderer (MemLabelId label, ObjectCreationMode mode) +: Super(kRendererParticleSystem, label, mode) +, m_LocalSpaceAABB (Vector3f::zero, Vector3f::zero) +{ + SetVisible (false); + + for (int i = 0; i < ParticleSystemRendererData::kMaxNumParticleMeshes; ++i) + m_Data.cachedMeshUserNode[i].SetData (this); + +#if UNITY_EDITOR + m_EditorEnabled = true; +#endif +} + +ParticleSystemRenderer::~ParticleSystemRenderer () +{ +} + +void ParticleSystemRenderer::InitializeClass () +{ + REGISTER_MESSAGE_PTR (ParticleSystemRenderer, kDidDeleteMesh, OnDidDeleteMesh, Mesh); +} + +void ParticleSystemRenderer::AwakeFromLoad (AwakeFromLoadMode awakeMode) +{ + Super::AwakeFromLoad (awakeMode); + UpdateCachedMesh (); +} + +void ParticleSystemRenderer::UpdateCachedMesh () +{ + int dst = 0; + for(int src = 0; src < ParticleSystemRendererData::kMaxNumParticleMeshes; src++) + { + m_Data.cachedMesh[src] = NULL; + m_Data.cachedMeshUserNode[src].RemoveFromList (); + + Mesh* mesh = m_Mesh[src]; + if (mesh) + { + if (mesh->GetSubMeshCount() == 1) + { + m_Data.cachedMesh[dst] = mesh; + const SubMesh& sm = mesh->GetSubMeshFast(0); + const UInt16* buffer = mesh->GetSubMeshBuffer16(0); + + if (sm.topology == kPrimitiveTriangleStripDeprecated) + { + const int capacity = CountTrianglesInStrip(buffer, sm.indexCount) * 3; + m_CachedIndexBuffer[dst].resize_uninitialized(capacity); + Destripify(buffer, sm.indexCount, m_CachedIndexBuffer[dst].begin(), capacity); + } + else if (sm.topology == kPrimitiveTriangles) + { + const int capacity = sm.indexCount; + m_CachedIndexBuffer[dst].resize_uninitialized(capacity); + memcpy(m_CachedIndexBuffer[dst].begin(), buffer, capacity*kVBOIndexSize); + } + else + { + m_CachedIndexBuffer[dst].resize_uninitialized(0); + } + + // Hook into mesh's user notifications. + mesh->AddObjectUser (m_Data.cachedMeshUserNode[dst]); + + dst++; + } + else + { + m_Data.cachedMesh[src] = NULL; + m_CachedIndexBuffer[src].resize_uninitialized(0); + AssertString ("Particle system meshes will only work with exactly one (1) sub mesh"); + } + } + } +} + +void ParticleSystemRenderer::OnDidDeleteMesh (Mesh* mesh) +{ + // Clear out cached pointer to mesh. + for (int i = 0; i < ParticleSystemRendererData::kMaxNumParticleMeshes; ++i) + { + if (m_Data.cachedMesh[i] != mesh) + continue; + + m_Data.cachedMesh[i] = NULL; + m_Data.cachedMeshUserNode[i].RemoveFromList (); + } +} + +void ParticleSystemRenderer::GetLocalAABB (AABB& result) +{ + result = m_LocalSpaceAABB; +} + +void ParticleSystemRenderer::GetWorldAABB (AABB& result) +{ + TransformAABB (m_LocalSpaceAABB, GetTransform ().GetPosition (), GetTransform ().GetRotation (), result); +} + +float ParticleSystemRenderer::GetSortingFudge () const +{ + return m_Data.sortingFudge; +} + +void ParticleSystemRenderer::CheckConsistency () +{ + Super::CheckConsistency (); + m_Data.maxParticleSize = std::max (0.0F, m_Data.maxParticleSize); + m_Data.normalDirection = clamp<float>(m_Data.normalDirection, 0.0f, 1.0f); +} + +void ParticleSystemRenderer::Reset () +{ + Super::Reset (); + m_Data.renderMode = kSRMBillboard; + m_Data.lengthScale = 2.0F; + m_Data.velocityScale = 0.0F; + m_Data.cameraVelocityScale = 0.0F; + m_Data.maxParticleSize = 0.5F; + m_Data.sortingFudge = 0.0F; + m_Data.sortMode = kSSMNone; + m_Data.normalDirection = 1.0f; + + for(int i = 0; i < ParticleSystemRendererData::kMaxNumParticleMeshes; i++) + m_Mesh[i] = NULL; + m_LocalSpaceAABB.SetCenterAndExtent (Vector3f::zero, Vector3f::zero); + + + +#if UNITY_EDITOR + m_EditorEnabled = true; +#endif +} + +void ParticleSystemRenderer::UpdateRenderer () +{ + ParticleSystem* system = QueryComponent(ParticleSystem); + if (system) + { + SetVisible (true); + BoundsChanged(); + } + else + { + UpdateManagerState (false); + } + + Super::UpdateRenderer (); +} + +void ParticleSystemRenderer::Update (const AABB& aabb) +{ + m_LocalSpaceAABB = aabb; + UpdateManagerState (true); +} + +void ParticleSystemRenderer::RendererBecameVisible() +{ + Super::RendererBecameVisible(); + + ParticleSystem* system = QueryComponent(ParticleSystem); + if(system) + system->RendererBecameVisible(); +} + +void ParticleSystemRenderer::RendererBecameInvisible() +{ + Super::RendererBecameInvisible(); + + ParticleSystem* system = QueryComponent(ParticleSystem); + if(system) + system->RendererBecameInvisible(); +} + +void ParticleSystemRenderer::UpdateLocalAABB() +{ + AABB aabb; + GetLocalAABB(aabb); + m_TransformInfo.localAABB = aabb; +} + +inline Rectf GetFrameUV (int index, int tilesX, float animUScale, float animVScale) +{ + int vIdx = index / tilesX; + int uIdx = index - vIdx * tilesX; // slightly faster than index % m_UVAnimation.xTile + float uOffset = (float)uIdx * animUScale; + float vOffset = 1.0f - animVScale - (float)vIdx * animVScale; + + return Rectf(uOffset, vOffset, animUScale, animVScale); +} + +template<ParticleSystemRenderMode renderMode> +void GenerateParticleGeometry (ParticleSystemVertex* vbPtr, + const ParticleSystemGeomConstInputData& constData, + const ParticleSystemRendererData& rendererData, + const ParticleSystemParticles& ps, + const ParticleSystemParticlesTempData& psTemp, + size_t startIndex, + size_t endIndex, + const Matrix4x4f& worldViewMatrix, + const Matrix4x4f& viewToWorldMatrix) +{ + float maxPlaneScale = constData.maxPlaneScale; + float maxOrthoSize = constData.maxOrthoSize; + float numUVFrame = constData.numUVFrame; + Vector3f xSpan = constData.xSpan; + Vector3f ySpan = constData.ySpan; + Vector3f cameraVelocity = constData.m_CameraVelocity * rendererData.cameraVelocityScale; + int numTilesX = constData.m_NumTilesX; + float animUScale = constData.animUScale; + float animVScale = constData.animVScale; + bool usesSheetIndex = constData.usesSheetIndex; + float lengthScale = rendererData.lengthScale; + float velocityScale = rendererData.velocityScale; + + float bentNormalFactor = constData.bentNormalFactor; + Vector3f bentNormalVector = constData.bentNormalVector; + + Vector2f uv[4] = { Vector2f(0.0f, 1.0f), + Vector2f(1.0f, 1.0f), + Vector2f(1.0f, 0.0f), + Vector2f(0.0f, 0.0f)}; + Vector4f uv2[4] = { Vector4f(0.0f, 1.0f, 0.0f, 0.0f), + Vector4f(1.0f, 1.0f, 0.0f, 0.0f), + Vector4f(1.0f, 0.0f, 0.0f, 0.0f), + Vector4f(0.0f, 0.0f, 0.0f, 0.0f)}; + + float invAnimVScale = 1.0f - animVScale; + + for( int i = startIndex; i < endIndex; ++i ) + { + Vector3f vert[4]; + Vector3f n0, n1; + + Vector3f position; + worldViewMatrix.MultiplyPoint3 (ps.position[i], position); + + // Constrain the size to be a fraction of the viewport size. + // v[0].z * / farPlaneZ * farPlaneWorldSpaceLength * maxLength[0...1] + // Also all valid z's are negative so we just negate the whole equation + float maxWorldSpaceLength = position.z * maxPlaneScale + maxOrthoSize; + float hsize = std::min (psTemp.size[i], maxWorldSpaceLength) * 0.5f; + if (renderMode == kSRMBillboard) + { + float s = Sin (ps.rotation[i]); + float c = Cos (ps.rotation[i]); + n0 = Vector3f(-c+s, s+c, 0.0f); + n1 = Vector3f( c+s, -s+c, 0.0f); + vert[0] = position + n0 * hsize; + vert[1] = position + n1 * hsize; + vert[2] = position - n0 * hsize; + vert[3] = position - n1 * hsize; + } + else if (renderMode == kSRMBillboardFixedHorizontal || renderMode == kSRMBillboardFixedVertical) + { + float s = Sin (ps.rotation[i]+0.78539816339744830961566084581988f); + float c = Cos (ps.rotation[i]+0.78539816339744830961566084581988f); + n0 = xSpan*c + ySpan*s; + n1 = ySpan*c - xSpan*s; + vert[0] = position + n0 * hsize; + vert[1] = position + n1 * hsize; + vert[2] = position - n0 * hsize; + vert[3] = position - n1 * hsize; + } + else if (renderMode == kSRMStretch3D) + { + //RH BUG FOR LATER: Here we see the stretching bug as described by case no 434115...this is a Flash VM error, where a writeFloat (or readFloat) fails. + Vector3f velocity; + worldViewMatrix.MultiplyVector3(ps.velocity[i] + ps.animatedVelocity[i], velocity); + velocity -= cameraVelocity; + float sqrVelocity = SqrMagnitude (velocity); + + Vector2f delta; + Vector3f endProj; + bool nonZeroVelocity = sqrVelocity > Vector3f::epsilon; + if (nonZeroVelocity) + { + endProj = position - velocity * (velocityScale + FastInvSqrt (sqrVelocity) * (lengthScale * psTemp.size[i])); + delta.x = position.z*endProj.y - position.y*endProj.z; + delta.y = position.x*endProj.z - position.z*endProj.x; + delta = NormalizeFast(delta); + } + else + { + endProj = position; + delta = Vector2f::xAxis; + } + n0 = n1 = Vector3f(delta.x, delta.y, 0.0f); + vert[0] = position + n0 * hsize; + vert[1] = endProj + n1 * hsize; + vert[2] = endProj - n0 * hsize; + vert[3] = position - n1 * hsize; + } + + // UV animation + float sheetIndex; + if(usesSheetIndex) + { + // TODO: Pretty much the perfect candidate for SIMD + + sheetIndex = psTemp.sheetIndex[i] * numUVFrame; + Assert (psTemp.sheetIndex[i] >= 0.0f && psTemp.sheetIndex[i] <= 1.0f); + + const int index0 = FloorfToIntPos (sheetIndex); + const int index1 = index0 + 1; + Vector2f offset0, offset1; + const float blend = sheetIndex - (float)index0; + + int vIdx = index0 / numTilesX; + int uIdx = index0 - vIdx * numTilesX; + offset0.x = (float)uIdx * animUScale; + offset0.y = invAnimVScale - (float)vIdx * animVScale; + + vIdx = index1 / numTilesX; + uIdx = index1 - vIdx * numTilesX; + offset1.x = (float)uIdx * animUScale; + offset1.y = invAnimVScale - (float)vIdx * animVScale; + + uv[0].Set(offset0.x, offset0.y + animVScale ); + uv[1].Set(offset0.x + animUScale, offset0.y + animVScale ); + uv[2].Set(offset0.x + animUScale, offset0.y ); + uv[3].Set(offset0.x, offset0.y ); + + uv2[0].Set(offset1.x, offset1.y + animVScale, blend, 0.0f ); + uv2[1].Set(offset1.x + animUScale, offset1.y + animVScale, blend, 0.0f ); + uv2[2].Set(offset1.x + animUScale, offset1.y, blend, 0.0f ); + uv2[3].Set(offset1.x, offset1.y, blend, 0.0f ); + } + + n0 = viewToWorldMatrix.MultiplyVector3(n0 * bentNormalFactor); + n1 = viewToWorldMatrix.MultiplyVector3(n1 * bentNormalFactor); + + ColorRGBA32 color = psTemp.color[i]; + + vbPtr[0].vert = vert[0]; + vbPtr[0].normal = bentNormalVector + n0; + vbPtr[0].color = color; + vbPtr[0].uv = uv[0]; + vbPtr[0].tangent = uv2[0]; + + vbPtr[1].vert = vert[1]; + vbPtr[1].normal = bentNormalVector + n1; + vbPtr[1].color = color; + vbPtr[1].uv = uv[1]; + vbPtr[1].tangent = uv2[1]; + + vbPtr[2].vert = vert[2]; + vbPtr[2].normal = bentNormalVector - n0; + vbPtr[2].color = color; + vbPtr[2].uv = uv[2]; + vbPtr[2].tangent = uv2[2]; + + vbPtr[3].vert = vert[3]; + vbPtr[3].normal = bentNormalVector - n1; + vbPtr[3].color = color; + vbPtr[3].uv = uv[3]; + vbPtr[3].tangent = uv2[3]; + + // Next four vertices + vbPtr += 4; + } +} + +static void DrawMeshParticles (const ParticleSystemGeomConstInputData& constInput, const ParticleSystemRendererData& rendererData, const Matrix4x4f& worldMatrix, const ParticleSystemParticles& ps, const ParticleSystemParticlesTempData& psTemp, const ChannelAssigns& channels) +{ + int numMeshes = 0; + ParticleMeshData particleMeshes[ParticleSystemRendererData::kMaxNumParticleMeshes]; + Vector3f defaultNormal(0, 0, 0); + Vector4f defaultTangent(0, 0, 0, 0); + ColorRGBA32 defaultColor(255, 255, 255, 255); + Vector2f defaultTexCoords(0, 0); + for(int i = 0; i < ParticleSystemRendererData::kMaxNumParticleMeshes; i++) + { + if(constInput.m_MeshIndexCount[i] == 0) + break; + const Mesh* mesh = rendererData.cachedMesh[i]; + if(mesh == NULL || !mesh->HasVertexData()) + break; + ParticleMeshData& dest = particleMeshes[i]; + dest.vertexCount = mesh->GetVertexCount(); + dest.positions = mesh->GetVertexBegin(); + dest.normals = mesh->GetNormalBegin(); + if (dest.normals.IsNull()) + dest.normals = StrideIterator<Vector3f>(&defaultNormal, 0); + dest.tangents = mesh->GetTangentBegin(); + if (dest.tangents.IsNull()) + dest.tangents = StrideIterator<Vector4f>(&defaultTangent, 0); + dest.texCoords = mesh->GetUvBegin(); + if (dest.texCoords.IsNull()) + dest.texCoords = StrideIterator<Vector2f>(&defaultTexCoords, 0); + dest.colors = mesh->GetColorBegin(); + if (dest.colors.IsNull()) + dest.colors = StrideIterator<ColorRGBA32>(&defaultColor, 0); + dest.indexCount = constInput.m_MeshIndexCount[i]; + dest.indexBuffer = constInput.m_MeshIndexBuffer[i]; + numMeshes++; + } + + if(0 == numMeshes) + return; + + GfxDevice& device = GetGfxDevice(); + + Matrix4x4f viewMatrix; + CopyMatrix (device.GetViewMatrix (), viewMatrix.GetPtr ()); + + const size_t particleCount = ps.array_size (); + + float probability = 1.0f / (float)numMeshes; + + // @TODO: We should move all these platform dependent numbers into Gfx specific code and get it from there. + const int kMaxVertices = 65536; + +#if UNITY_WII + const int kMaxIndices = 65536; +#else + const int kMaxIndices = kDynamicBatchingIndicesThreshold; +#endif + + int particleOffset = 0; + while (particleOffset < particleCount) + { + int numVertices = 0; + int numIndices = 0; + int particleCountBatch = 0; + + // Figure out batch size + for(int i = particleOffset; i < particleCount; i++) + { + const float randomValue = GenerateRandom(ps.randomSeed[i] + kParticleSystemMeshSelectionId); + int lastNumVertices = 0; + int lastNumIndices = 0; + for(int j = 0; j < numMeshes; j++) + { + const float lower = probability * j; + const float upper = probability * (j + 1); + if((randomValue >= lower) && (randomValue <= upper)) + { + lastNumVertices = particleMeshes[j].vertexCount; + lastNumIndices = particleMeshes[j].indexCount; + break; + } + } + if((numVertices >= kMaxVertices) || (numIndices >= kMaxIndices)) + { + break; + } + else + { + numVertices += lastNumVertices; + numIndices += lastNumIndices; + particleCountBatch++; + } + } + + const int vertexCount = numVertices; + const int indexCount = numIndices; + + // Figure out if normals and tangents are needed by shader + UInt32 normalTangentMask = channels.GetSourceMap() & VERTEX_FORMAT2(Normal, Tangent); + + // Tangents requires normals + if( normalTangentMask & VERTEX_FORMAT1(Tangent) ) + normalTangentMask |= VERTEX_FORMAT1(Normal); + + // Get VBO chunk + DynamicVBO& vbo = device.GetDynamicVBO(); + UInt8* vbPtr = NULL; + UInt16* ibPtr = NULL; + const UInt32 mandatoryChannels = VERTEX_FORMAT3(Vertex, Color, TexCoord0); + if( !vbo.GetChunk( mandatoryChannels | normalTangentMask, + vertexCount, indexCount, + DynamicVBO::kDrawIndexedTriangles, + (void**)&vbPtr, (void**)&ibPtr ) ) + { + return; + } + + int vertexOffset = 0; + int indexOffset = 0; + const int startIndex = particleOffset; + const int endIndex = particleOffset + particleCountBatch; + for( int i = startIndex; i < endIndex; ++i ) + { + const Vector3f position = ps.position[i]; + const float rotation = ps.rotation[i]; + const float size = psTemp.size[i]; + const Vector3f axisOfRotation = NormalizeSafe (ps.axisOfRotation[i], Vector3f::yAxis); + const ColorRGBA32 particleColor = psTemp.color[i]; + + // Only shared part is actually rotation. xformNoScale doesn't need a translation, so no need to copy that data + Matrix4x4f xformNoScale; + xformNoScale.SetTR (position, AxisAngleToQuaternion (axisOfRotation, rotation)); + + Matrix4x4f xform = xformNoScale; + ScaleMatrix(xform, size); + + // Figure out which mesh to use + const float randomValue = GenerateRandom(ps.randomSeed[i] + kParticleSystemMeshSelectionId); + int meshIndex = 0; + for(int j = 0; j < numMeshes; j++) + { + const float lower = probability * j; + const float upper = probability * (j + 1); + if((randomValue >= lower) && (randomValue <= upper)) + { + meshIndex = j; + break; + } + } + + const ParticleMeshData& mesh = particleMeshes[meshIndex]; + + // Fill up vbo here + if( normalTangentMask == VERTEX_FORMAT2(Normal, Tangent) ) + TransformParticleMesh<true, true>(mesh, particleColor, xform, xformNoScale, &vbPtr); + else if( normalTangentMask == VERTEX_FORMAT1(Normal) ) + TransformParticleMesh<true, false>(mesh, particleColor, xform, xformNoScale, &vbPtr); + else if( normalTangentMask == 0 ) + TransformParticleMesh<false, false>(mesh, particleColor, xform, xformNoScale, &vbPtr); + else + ErrorString("Invalid normalTangentMask"); + + const int meshIndexMax = mesh.indexCount - 2; + for(int index = 0; index < meshIndexMax; index+=3) + { + ibPtr[index+0] = mesh.indexBuffer[index+0] + vertexOffset; + ibPtr[index+1] = mesh.indexBuffer[index+1] + vertexOffset; + ibPtr[index+2] = mesh.indexBuffer[index+2] + vertexOffset; + } + ibPtr += mesh.indexCount; + + vertexOffset += mesh.vertexCount; + indexOffset += mesh.indexCount; + } + + vbo.ReleaseChunk (vertexCount, indexCount); + device.SetViewMatrix(viewMatrix.GetPtr()); + device.SetWorldMatrix(worldMatrix.GetPtr()); + vbo.DrawChunk (channels); + GPU_TIMESTAMP(); + + particleOffset += particleCountBatch; + } +} + +static void DrawParticlesInternal(const ParticleSystemGeomConstInputData& constData, const ParticleSystemRendererData& rendererData, const Matrix4x4f& worldViewMatrix, const Matrix4x4f& viewToWorldMatrix, const ParticleSystemParticles& ps, const ParticleSystemParticlesTempData& psTemp, ParticleSystemVertex* vbPtr, const size_t particleOffset, const size_t numParticles, int renderMode) +{ + const size_t endIndex = particleOffset + numParticles; + + if (renderMode == kSRMBillboard) + GenerateParticleGeometry<kSRMBillboard> (vbPtr, constData, rendererData, ps, psTemp, particleOffset, endIndex, worldViewMatrix, viewToWorldMatrix); + if (renderMode == kSRMStretch3D) + GenerateParticleGeometry<kSRMStretch3D> (vbPtr, constData, rendererData, ps, psTemp, particleOffset, endIndex, worldViewMatrix, viewToWorldMatrix); + if (renderMode == kSRMBillboardFixedHorizontal) + GenerateParticleGeometry<kSRMBillboardFixedHorizontal> (vbPtr, constData, rendererData, ps, psTemp, particleOffset, endIndex, worldViewMatrix, viewToWorldMatrix); + if (renderMode == kSRMBillboardFixedVertical) + GenerateParticleGeometry<kSRMBillboardFixedVertical> (vbPtr, constData, rendererData, ps, psTemp, particleOffset, endIndex, worldViewMatrix, viewToWorldMatrix); +} + +static void DrawParticles(const ParticleSystemGeomConstInputData& constData, const ParticleSystemRendererData& rendererData, const Matrix4x4f& worldViewMatrix, const Matrix4x4f& viewToWorldMatrix, const ParticleSystemParticles& ps, const ParticleSystemParticlesTempData& psTemp, const ChannelAssigns& channels, ParticleSystemVertex* vbPtr) +{ + GfxDevice& device = GetGfxDevice(); + const size_t particleCount = ps.array_size(); + + if(vbPtr) + { + DrawParticlesInternal(constData, rendererData, worldViewMatrix, viewToWorldMatrix, ps, psTemp, vbPtr, 0, particleCount, rendererData.renderMode); + } + else + { + int particleOffset = 0; + while (particleOffset < particleCount) + { + const int particleCountBatch = min(kMaxNumParticlesPerBatch, (int)particleCount - particleOffset); + + // Get VBO chunk + DynamicVBO& vbo = device.GetDynamicVBO(); + if( !vbo.GetChunk( (1<<kShaderChannelVertex) | (1<<kShaderChannelNormal) | (1<<kShaderChannelTexCoord0) | (1<<kShaderChannelColor) | (1<<kShaderChannelTangent), + particleCountBatch * 4, 0, + DynamicVBO::kDrawQuads, + (void**)&vbPtr, NULL ) ) + { + continue; + } + + DrawParticlesInternal(constData, rendererData, worldViewMatrix, viewToWorldMatrix, ps, psTemp, vbPtr, particleOffset, particleCountBatch, rendererData.renderMode); + particleOffset += particleCountBatch; + + vbo.ReleaseChunk (particleCountBatch * 4, 0); + + // Draw + device.SetViewMatrix (Matrix4x4f::identity.GetPtr()); // implicitly sets world to identity + + PROFILER_BEGIN(gSubmitVBOParticleProfile, constData.m_Renderer) + vbo.DrawChunk (channels); + GPU_TIMESTAMP(); + PROFILER_END + + device.SetViewMatrix(constData.m_ViewMatrix.GetPtr ()); + } + } +} + +void ParticleSystemRenderer::CalculateTotalParticleCount(UInt32& totalNumberOfParticles, ParticleSystem& system, bool first) +{ + ParticleSystemRenderer* renderer = system.QueryComponent(ParticleSystemRenderer); + if(!renderer || first) + { + totalNumberOfParticles += system.GetParticleCount(); + + Transform* t = system.QueryComponent (Transform); + if (t == NULL) + return; + for (Transform::iterator i=t->begin ();i != t->end ();i++) + { + ParticleSystem* child = (**i).QueryComponent(ParticleSystem); + if (child) + CalculateTotalParticleCount(totalNumberOfParticles, *child, false); + } + } +} + +void ParticleSystemRenderer::CombineParticleBuffersRec(int& offset, ParticleSystemParticles& ps, ParticleSystemParticlesTempData& psTemp, ParticleSystem& system, bool first, bool needsAxisOfRotation) +{ + ParticleSystemRenderer* renderer = system.QueryComponent(ParticleSystemRenderer); + if(!renderer || first) + { + int particleCount = system.GetParticleCount(); + ps.array_merge_preallocated(system.GetParticles(), offset, needsAxisOfRotation, false); + + if(system.m_ReadOnlyState->useLocalSpace) + { + Matrix4x4f localToWorld = system.GetComponent (Transform).GetLocalToWorldMatrixNoScale (); + + int endIndex = offset + particleCount; + for(int i = offset; i < endIndex; i++) + ps.position[i] = localToWorld.MultiplyPoint3(ps.position[i]); + for(int i = offset; i < endIndex; i++) + ps.velocity[i] = localToWorld.MultiplyVector3(ps.velocity[i]); + for(int i = offset; i < endIndex; i++) + ps.animatedVelocity[i] = localToWorld.MultiplyVector3(ps.animatedVelocity[i]); + if(ps.usesAxisOfRotation) + for(int i = offset; i < endIndex; i++) + ps.axisOfRotation[i] = localToWorld.MultiplyVector3(ps.axisOfRotation[i]); + } + + ParticleSystem::UpdateModulesNonIncremental(system, ps, psTemp, offset, offset + particleCount); + offset += particleCount; + + Transform* t = system.QueryComponent (Transform); + if (t == NULL) + return; + for (Transform::iterator i=t->begin ();i != t->end ();i++) + { + ParticleSystem* child = (**i).QueryComponent(ParticleSystem); + if (child) + CombineParticleBuffersRec(offset, ps, psTemp, *child, false, needsAxisOfRotation); + } + } +} + +void ParticleSystemRenderer::SetUsesAxisOfRotationRec(ParticleSystem& shuriken, bool first) +{ + ParticleSystemRenderer* renderer = shuriken.QueryComponent(ParticleSystemRenderer); + if(!renderer || first) + { + shuriken.SetUsesAxisOfRotation(); + + Transform* t = shuriken.QueryComponent (Transform); + if (t == NULL) + return; + for (Transform::iterator i=t->begin ();i != t->end ();i++) + { + ParticleSystem* shuriken = (**i).QueryComponent(ParticleSystem); + if (shuriken) + SetUsesAxisOfRotationRec(*shuriken, false); + } + } +} + +void ParticleSystemRenderer::CombineBoundsRec(ParticleSystem& shuriken, MinMaxAABB& aabb, bool first) +{ + ParticleSystemRenderer* renderer = shuriken.QueryComponent(ParticleSystemRenderer); + if(!renderer || first) + { + AABB result = shuriken.m_State->minMaxAABB; + if(!shuriken.m_ReadOnlyState->useLocalSpace) + InverseTransformAABB (result, renderer->GetTransform().GetPosition (), renderer->GetTransform().GetRotation (), result); + + if(first) + aabb = result; + else + aabb.Encapsulate(result); + + Transform* t = shuriken.QueryComponent (Transform); + if (t == NULL) + return; + for (Transform::iterator i=t->begin ();i != t->end ();i++) + { + ParticleSystem* shuriken = (**i).QueryComponent(ParticleSystem); + if (shuriken) + CombineBoundsRec(*shuriken, aabb, false); + } + } +} + +void ParticleSystemRenderer::Render (int/* materialIndex*/, const ChannelAssigns& channels) +{ + ParticleSystem::SyncJobs(); + + ParticleSystem* system = QueryComponent(ParticleSystem); + if(!system) + return; + + // Can't render without an active camera (case 568930) + // Can remove check when we finally kill Renderer.Render() + if (!GetCurrentCameraPtr()) + return; + + PROFILER_AUTO_GFX(gParticlesSingleProfile, this); + + UInt32 numParticles = 0; + CalculateTotalParticleCount(numParticles, *system, true); + if(numParticles) + RenderInternal(*system, *this, channels, 0, numParticles); +} + +void ParticleSystemRenderer::RenderMultiple (const BatchInstanceData* instances, size_t count, const ChannelAssigns& channels) +{ + ParticleSystem::SyncJobs(); + + size_t numParticlesBatch = 0; + + BatchInstanceData const* instancesEnd = instances + count; + BatchInstanceData const* iBatchBegin = instances; + BatchInstanceData const* iBatchEnd = instances; + while(iBatchEnd != instancesEnd) + { + Assert(iBatchEnd->renderer->GetRendererType() == kRendererParticleSystem); + ParticleSystemRenderer* psRenderer = (ParticleSystemRenderer*)iBatchEnd->renderer; + Assert(psRenderer->GetRenderMode() != kSRMMesh); + ParticleSystem* system = psRenderer->QueryComponent(ParticleSystem); + if (!system ) + { + iBatchEnd++; + continue; + } + UInt32 numParticles = 0; + psRenderer->CalculateTotalParticleCount(numParticles, *system, true); + + if((numParticlesBatch + numParticles) <= kMaxNumParticlesPerBatch) + { + numParticlesBatch += numParticles; + iBatchEnd++; + } + else + { + if(numParticlesBatch) + { + RenderBatch(iBatchBegin, iBatchEnd - iBatchBegin, numParticlesBatch, channels); + numParticlesBatch = 0; + iBatchBegin = iBatchEnd; + } + else // Can't fit in one draw call + { + RenderBatch(iBatchEnd, 1, numParticles, channels); + iBatchEnd++; + iBatchBegin = iBatchEnd; + } + } + } + + if((iBatchBegin != iBatchEnd) && numParticlesBatch) + RenderBatch(iBatchBegin, iBatchEnd - iBatchBegin, numParticlesBatch, channels); +} + +void ParticleSystemRenderer::RenderBatch (const BatchInstanceData* instances, size_t count, size_t numParticles, const ChannelAssigns& channels) +{ + DebugAssert(numParticles); + + GfxDevice& device = GetGfxDevice(); + + const MaterialPropertyBlock* customProps = count > 0 ? instances[0].renderer->GetCustomProperties() : NULL; + if (customProps) + device.SetMaterialProperties (*customProps); + + Matrix4x4f viewMatrix; + CopyMatrix (device.GetViewMatrix (), viewMatrix.GetPtr ()); + + ParticleSystemVertex* vbPtr = 0; + DynamicVBO& vbo = device.GetDynamicVBO(); + if(numParticles <= kMaxNumParticlesPerBatch) + { + if( !vbo.GetChunk( (1<<kShaderChannelVertex) | (1<<kShaderChannelNormal) | (1<<kShaderChannelTexCoord0) | (1<<kShaderChannelColor) | (1<<kShaderChannelTangent), + numParticles * 4, 0, + DynamicVBO::kDrawQuads, + (void**)&vbPtr, NULL ) ) + { + return; + } + } + + PROFILER_AUTO_GFX(gParticlesBatchProfile, 0); + + // Allocate VBO if count is not above threshold. Else just pass null down + BatchInstanceData const* iBatchBegin = instances; + BatchInstanceData const* instancesEnd = instances + count; + size_t particleOffset = 0; + while (iBatchBegin != instancesEnd) + { + Assert(iBatchBegin->renderer->GetRendererType() == kRendererParticleSystem); + ParticleSystemRenderer* psRenderer = (ParticleSystemRenderer*)iBatchBegin->renderer; + Assert(psRenderer->GetRenderMode() != kSRMMesh); + ParticleSystem* system = psRenderer->QueryComponent(ParticleSystem); + UInt32 particleCountTotal = 0; + if (system) + { + // It would be nice to filter out NULL particle systems earlier, but we don't (case 504744) + CalculateTotalParticleCount(particleCountTotal, *system, true); + if(particleCountTotal) + RenderInternal(*system, *psRenderer, channels, vbPtr + particleOffset * 4, particleCountTotal); + } + iBatchBegin++; + particleOffset += particleCountTotal; + } + + if(vbPtr) + { + vbo.ReleaseChunk (numParticles * 4, 0); + + // Draw + device.SetViewMatrix (Matrix4x4f::identity.GetPtr()); // implicitly sets world to identity + + PROFILER_BEGIN(gSubmitVBOParticleProfile, 0) + vbo.DrawChunk (channels); + GPU_TIMESTAMP(); + PROFILER_END + + if (count > 1) + device.AddBatchingStats(numParticles * 2, numParticles * 4, count); + + device.SetViewMatrix(viewMatrix.GetPtr()); + } +} + +void ParticleSystemRenderer::RenderInternal (ParticleSystem& system, const ParticleSystemRenderer& renderer, const ChannelAssigns& channels, ParticleSystemVertex* vbPtr, UInt32 particleCountTotal) +{ + Assert(particleCountTotal); + +#if UNITY_EDITOR + if (!renderer.m_EditorEnabled) + return; +#endif + + GfxDevice& device = GetGfxDevice(); + + // Render matrix + Matrix4x4f viewMatrix; + CopyMatrix (device.GetViewMatrix (), viewMatrix.GetPtr ()); + + ParticleSystemParticles* ps = &system.GetParticles (); + size_t particleCount = ps->array_size (); + AssertBreak(particleCountTotal >= particleCount); + + UInt8* combineBuffer = 0; + ParticleSystemParticles combineParticles; + if(particleCountTotal > particleCount) + { + particleCount = particleCountTotal; + combineBuffer = ALLOC_TEMP_MANUAL(UInt8, particleCountTotal * ParticleSystemParticles::GetParticleSize()); + combineParticles.array_assign_external((void*)&combineBuffer[0], particleCountTotal); + ps = &combineParticles; + } + + if(!particleCount) + return; + + const bool needsAxisOfRotation = !renderer.GetScreenSpaceRotation(); + if(needsAxisOfRotation) + { + if ( IS_CONTENT_NEWER_OR_SAME (GetNumericVersion ("4.0.0f7")) && !IS_CONTENT_NEWER_OR_SAME (GetNumericVersion ("4.1.1a1")) ) + { + // this was introduced in 4.0 and is wrong, it will effectively force the rotation axis to be the y-axis for all particles, the function is meant only + // to initialize the axis array (once) and should only be called from SetUsesAxisOfRotationRec + ps->SetUsesAxisOfRotation (); + } + else + { + // this is the intended functionality, but was broken for 4.0 and 4.0.1 see above + SetUsesAxisOfRotationRec (system, true); + } + } + + ParticleSystemSortMode sortMode = (ParticleSystemSortMode)renderer.m_Data.sortMode; + bool isInLocalSpace = system.m_ReadOnlyState->useLocalSpace && !combineBuffer; + Matrix4x4f worldMatrix = Matrix4x4f::identity; + if(isInLocalSpace) + worldMatrix = system.GetComponent (Transform).GetLocalToWorldMatrixNoScale (); + + ParticleSystemParticlesTempData psTemp; + psTemp.color = ALLOC_TEMP_MANUAL(ColorRGBA32, particleCount); + psTemp.size = ALLOC_TEMP_MANUAL(float, particleCount); + psTemp.sheetIndex = 0; + psTemp.particleCount = particleCount; + if(combineBuffer) + { + int offset = 0; + CombineParticleBuffersRec(offset, *ps, psTemp, system, true, needsAxisOfRotation); + if (kSSMNone != sortMode) + Sort(viewMatrix, *ps, sortMode, &psTemp, true); + } + else + { + if(system.m_UVModule->GetEnabled()) + psTemp.sheetIndex = ALLOC_TEMP_MANUAL(float, particleCount); + + if (kSSMNone != sortMode) + { + Matrix4x4f objectToViewMatrix; + MultiplyMatrices3x4(viewMatrix, worldMatrix, objectToViewMatrix); + Sort(objectToViewMatrix, *ps, sortMode, 0, false); + } + ParticleSystem::UpdateModulesNonIncremental(system, *ps, psTemp, 0, particleCount); + } + + // Constrain the size to be a fraction of the viewport size. + // In perspective case, max size is (z*factorA). In ortho case, max size is just factorB. To have both + // without branches, we do (z*factorA+factorB) and set one of factors to zero. + float maxPlaneScale = 0.0f; + float maxOrthoSize = 0.0f; + // Getting the camera isn't totally free, so do it once. + const Camera& camera = GetCurrentCamera(); + if (!camera.GetOrthographic()) + maxPlaneScale = -camera.CalculateFarPlaneWorldSpaceLength() * renderer.m_Data.maxParticleSize / camera.GetFar(); + else + maxOrthoSize = camera.CalculateFarPlaneWorldSpaceLength() * renderer.m_Data.maxParticleSize; + + int numMeshes = 0; + for(int i = 0; i < ParticleSystemRendererData::kMaxNumParticleMeshes; i++) + if(renderer.m_Data.cachedMesh[i]) + numMeshes++; + + ParticleSystemGeomConstInputData constData; + constData.m_ViewMatrix = viewMatrix; + constData.m_CameraVelocity = viewMatrix.MultiplyVector3(camera.GetVelocity ()); + constData.m_Renderer = (Object*)&renderer; + for(int i = 0; i < numMeshes; i++) + { + constData.m_MeshIndexBuffer[i] = renderer.m_CachedIndexBuffer[i].begin(); + constData.m_MeshIndexCount[i] = renderer.m_CachedIndexBuffer[i].size(); + AssertBreak((constData.m_MeshIndexCount[i] % 3) == 0); + } + + system.GetNumTiles(constData.m_NumTilesX, constData.m_NumTilesY); + constData.maxPlaneScale = maxPlaneScale; + constData.maxOrthoSize = maxOrthoSize; + constData.numUVFrame = constData.m_NumTilesX * constData.m_NumTilesY; + constData.animUScale = 1.0f / (float)constData.m_NumTilesX; + constData.animVScale = 1.0f / (float)constData.m_NumTilesY; + constData.xSpan = Vector3f(-1.0f,0.0f,0.0f); + constData.ySpan = Vector3f(0.0f,0.0f,1.0f); + if (renderer.m_Data.renderMode == kSRMBillboardFixedVertical) + { + constData.ySpan = Vector3f(0.0f,1.0f,0.0f); + const Vector3f zSpan = viewMatrix.MultiplyVector3 (Vector3f::zAxis);// (RotateVectorByQuat (cameraRotation, Vector3f(0.0f,0.0f,1.0f)); + constData.xSpan = NormalizeSafe (Cross (constData.ySpan, zSpan)); + } + constData.xSpan = viewMatrix.MultiplyVector3(constData.xSpan); + constData.ySpan = viewMatrix.MultiplyVector3(constData.ySpan); + constData.usesSheetIndex = psTemp.sheetIndex != NULL; + + const float bentNormalAngle = renderer.m_Data.normalDirection * 90.0f * kDeg2Rad; + const float scale = (renderer.m_Data.renderMode == kSRMBillboard) ? 0.707106781f : 1.0f; + + Matrix4x4f viewToWorldMatrix; + Matrix4x4f::Invert_General3D(viewMatrix, viewToWorldMatrix); + + Matrix4x4f worldViewMatrix; + MultiplyMatrices4x4(&viewMatrix, &worldMatrix, &worldViewMatrix); + + Vector3f billboardNormal = Vector3f::zAxis; + if((renderer.m_Data.renderMode == kSRMBillboardFixedHorizontal) || (renderer.m_Data.renderMode == kSRMBillboardFixedVertical)) + billboardNormal = viewMatrix.MultiplyVector3 (NormalizeSafe (Cross (constData.xSpan, constData.ySpan))); + constData.bentNormalVector = viewToWorldMatrix.MultiplyVector3(Sin(bentNormalAngle) * billboardNormal); + constData.bentNormalFactor = Cos(bentNormalAngle) * scale; + + if (renderer.m_Data.renderMode == kSRMMesh) + DrawMeshParticles (constData, renderer.m_Data, worldMatrix, *ps, psTemp, channels); + else + DrawParticles(constData, renderer.m_Data, worldViewMatrix, viewToWorldMatrix, *ps, psTemp, channels, vbPtr); + + FREE_TEMP_MANUAL(psTemp.color); + FREE_TEMP_MANUAL(psTemp.size); + if(psTemp.sheetIndex) + FREE_TEMP_MANUAL(psTemp.sheetIndex); + if(combineBuffer) + FREE_TEMP_MANUAL(combineBuffer); +} + +template<class TransferFunction> inline +void ParticleSystemRenderer::Transfer (TransferFunction& transfer) +{ + Super::Transfer (transfer); + transfer.Transfer (m_Data.renderMode, "m_RenderMode"); + transfer.Transfer (m_Data.maxParticleSize, "m_MaxParticleSize"); + transfer.Transfer (m_Data.cameraVelocityScale, "m_CameraVelocityScale"); + transfer.Transfer (m_Data.velocityScale, "m_VelocityScale"); + transfer.Transfer (m_Data.lengthScale, "m_LengthScale"); + transfer.Transfer (m_Data.sortingFudge, "m_SortingFudge"); + transfer.Transfer (m_Data.normalDirection, "m_NormalDirection"); + transfer.Transfer (m_Data.sortMode, "m_SortMode"); + transfer.Transfer (m_Mesh[0], "m_Mesh"); + transfer.Transfer (m_Mesh[1], "m_Mesh1"); + transfer.Transfer (m_Mesh[2], "m_Mesh2"); + transfer.Transfer (m_Mesh[3], "m_Mesh3"); +} diff --git a/Runtime/Graphics/ParticleSystem/ParticleSystemRenderer.h b/Runtime/Graphics/ParticleSystem/ParticleSystemRenderer.h new file mode 100644 index 0000000..07bcb0d --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleSystemRenderer.h @@ -0,0 +1,134 @@ +#ifndef SHURIKENRENDERER_H +#define SHURIKENRENDERER_H + +#include "Runtime/Filters/Renderer.h" +#include "Runtime/Math/Color.h" +#include "Runtime/Math/Rect.h" +#include "Runtime/Math/Vector3.h" +#include "Runtime/Math/Vector2.h" + + + + +class Mesh; +class MinMaxAABB; +struct ParticleSystemVertex; + +struct ParticleSystemRendererData +{ + // Must match the one in RendererModuleUI.cs + enum { kMaxNumParticleMeshes = 4 }; + + int renderMode; ///< enum { Billboard = 0, Stretched = 1, Horizontal Billboard = 2, Vertical Billboard = 3, Mesh = 4 } + int sortMode; ///< enum { None = 0, By Distance = 1, Youngest First = 2, Oldest First = 3 } + float maxParticleSize; ///< How large is a particle allowed to be on screen at most? 1 is entire viewport. 0.5 is half viewport. + float cameraVelocityScale; ///< How much the camera motion is factored in when determining particle stretching. + float velocityScale; ///< When Stretch Particles is enabled, defines the length of the particle compared to its velocity. + float lengthScale; ///< When Stretch Particles is enabled, defines the length of the particle compared to its width. + float sortingFudge; ///< Lower the number, most likely that these particles will appear in front of other transparent objects, including other particles. + float normalDirection; ///< Value between 0.0 and 1.0. If 1.0 is used, normals will point towards camera. If 0.0 is used, normals will point out in the corner direction of the particle. + Mesh* cachedMesh[kMaxNumParticleMeshes]; + + /// Node hooked into the mesh user list of cached meshes so we get notified + /// when a mesh goes away. + /// + /// NOTE: Must be initialized properly after construction to point to the + /// ParticleSystemRenderer. + ListNode<Object> cachedMeshUserNode[kMaxNumParticleMeshes]; +}; + + +enum ParticleSystemRenderMode { + kSRMBillboard = 0, + kSRMStretch3D = 1, + kSRMBillboardFixedHorizontal = 2, + kSRMBillboardFixedVertical = 3, + kSRMMesh = 4, +}; + +enum ParticleSystemSortMode +{ + kSSMNone, + kSSMByDistance, + kSSMYoungestFirst, + kSSMOldestFirst, +}; + +struct ParticleSystemParticles; +struct ParticleSystemParticlesTempData; +class ParticleSystem; +struct ParticleSystemGeomConstInputData; +class ParticleSystemRenderer : public Renderer { +public: + REGISTER_DERIVED_CLASS (ParticleSystemRenderer, Renderer) + DECLARE_OBJECT_SERIALIZE (ParticleSystemRenderer) + static void InitializeClass (); + + ParticleSystemRenderer (MemLabelId label, ObjectCreationMode mode); + // ParticleSystemRenderer(); declared-by-macro + + virtual void Render (int materialIndex, const ChannelAssigns& channels); + static void RenderMultiple (const BatchInstanceData* instances, size_t count, const ChannelAssigns& channels); + + virtual void GetLocalAABB (AABB& result); + virtual void GetWorldAABB (AABB& result); + virtual float GetSortingFudge () const; + + virtual void CheckConsistency (); + virtual void Reset (); + + virtual void AwakeFromLoad (AwakeFromLoadMode awakeMode); + + void Update (const AABB& aabb); + void UpdateLocalAABB(); + + virtual void RendererBecameVisible(); + virtual void RendererBecameInvisible(); + + static void SetUsesAxisOfRotationRec(ParticleSystem& system, bool first); + static void CombineBoundsRec(ParticleSystem& shuriken, MinMaxAABB& aabb, bool first); + + GET_SET_DIRTY (ParticleSystemRenderMode, RenderMode, m_Data.renderMode) ; + GET_SET_DIRTY (ParticleSystemSortMode, SortMode, m_Data.sortMode) ; + GET_SET_DIRTY (float, MaxParticleSize, m_Data.maxParticleSize) ; + GET_SET_DIRTY (float, CameraVelocityScale, m_Data.cameraVelocityScale) ; + GET_SET_DIRTY (float, VelocityScale, m_Data.velocityScale) ; + GET_SET_DIRTY (float, LengthScale, m_Data.lengthScale) ; + void SetMesh (PPtr<Mesh> mesh) { m_Mesh[0] = mesh; SetDirty(); UpdateCachedMesh (); } + PPtr<Mesh> GetMesh () const { return m_Mesh[0]; } + + const PPtr<Mesh>* GetMeshes () const { return m_Mesh; } + const ParticleSystemRendererData& GetData() const { return m_Data; } + +#if UNITY_EDITOR + bool GetEditorEnabled() const { return m_EditorEnabled; } + void SetEditorEnabled(bool value) { m_EditorEnabled = value; } +#endif + + // For mesh we use world space rotation, else screen space + bool GetScreenSpaceRotation() const { return m_Data.renderMode != kSRMMesh; }; +private: + // from Renderer + virtual void UpdateRenderer (); + void UpdateCachedMesh (); + void OnDidDeleteMesh (Mesh* mesh); + +private: + static void CalculateTotalParticleCount(UInt32& totalNumberOfParticles, ParticleSystem& shuriken, bool first); + static void CombineParticleBuffersRec(int& offset, ParticleSystemParticles& particles, ParticleSystemParticlesTempData& psTemp, ParticleSystem& shuriken, bool first, bool needsAxisOfRotation); + + static void RenderBatch (const BatchInstanceData* instances, size_t count, size_t numParticles, const ChannelAssigns& channels); + static void RenderInternal (ParticleSystem& system, const ParticleSystemRenderer& renderer, const ChannelAssigns& channels, ParticleSystemVertex* vbPtr, UInt32 particleCountTotal); + + ParticleSystemRendererData m_Data; + dynamic_array<UInt16> m_CachedIndexBuffer[ParticleSystemRendererData::kMaxNumParticleMeshes]; + PPtr<Mesh> m_Mesh[ParticleSystemRendererData::kMaxNumParticleMeshes]; + + AABB m_LocalSpaceAABB; + +#if UNITY_EDITOR + bool m_EditorEnabled; +#endif +}; + +#endif // SHURIKENRENDERER_H diff --git a/Runtime/Graphics/ParticleSystem/ParticleSystemRendererTests.cpp b/Runtime/Graphics/ParticleSystem/ParticleSystemRendererTests.cpp new file mode 100644 index 0000000..47db7ee --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleSystemRendererTests.cpp @@ -0,0 +1,29 @@ +#include "UnityPrefix.h" + +#if ENABLE_UNIT_TESTS + +#include "External/UnitTest++/src/UnitTest++.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemRenderer.h" +#include "Runtime/Filters/Mesh/LodMesh.h" +#include "Runtime/Testing/TestFixtures.h" + + +SUITE (ParticleSystemRendererTests) +{ + typedef ObjectTestFixture<ParticleSystemRenderer> Fixture; + + TEST_FIXTURE (Fixture, DeletingMeshClearsOutCachedMeshPointers) + { + // Arrange. + PPtr<Mesh> mesh (NEW_OBJECT_RESET_AND_AWAKE (Mesh)); + m_ObjectUnderTest->SetMesh (mesh); + + // Act. + DestroySingleObject (mesh); + + // Assert. + CHECK (m_ObjectUnderTest->GetData().cachedMesh[0] == NULL); + } +} + +#endif diff --git a/Runtime/Graphics/ParticleSystem/ParticleSystemUtils.cpp b/Runtime/Graphics/ParticleSystem/ParticleSystemUtils.cpp new file mode 100644 index 0000000..032cd19 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleSystemUtils.cpp @@ -0,0 +1,113 @@ +#include "UnityPrefix.h" +#include "ParticleSystemUtils.h" +#include "ParticleSystem.h" +#include "ParticleSystemCurves.h" +#include "Modules/ParticleSystemModule.h" +#include "Runtime/BaseClasses/IsPlaying.h" + +#if UNITY_EDITOR +#include "Editor/Src/ParticleSystem/ParticleSystemEditor.h" +#endif + +UInt32 randomSeed = 0x1337; + +UInt32 GetGlobalRandomSeed () +{ + return ++randomSeed; +} + +void ResetGlobalRandomSeed () +{ + randomSeed = 0x1337; +} + +Vector2f CalculateInverseLerpOffsetScale (const Vector2f& range) +{ + Assert (range.x < range.y); + float scale = 1.0F / (range.y - range.x); + return Vector2f (scale, -range.x * scale); +} + +void CalculatePositionAndVelocity(Vector3f& initialPosition, Vector3f& initialVelocity, const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, const ParticleSystemParticles& ps, const size_t index) +{ + initialPosition = ps.position[index]; + initialVelocity = ps.velocity[index] + ps.animatedVelocity[index]; + if(roState.useLocalSpace) + { + // If we are in local space, transform to world space to make independent of this emitters transform + initialPosition = state.localToWorld.MultiplyPoint3(initialPosition); + initialVelocity = state.localToWorld.MultiplyVector3(initialVelocity); + } +} + +void KillParticle(const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, size_t index, size_t& particleCount) +{ + Assert(particleCount > 0); + + for(int i = 0; i < state.numCachedSubDataDeath; i++) + { + ParticleSystemEmissionState emissionState; + RecordEmit(emissionState, state.cachedSubDataDeath[i], roState, state, ps, kParticleSystemSubTypeDeath, i, index, 0.0f, 0.0001f, 1.0f); + } + + ps.element_assign (index, particleCount - 1); + --particleCount; + +} + +void RecordEmit(ParticleSystemEmissionState& emissionState, const ParticleSystemSubEmitterData& subEmitterData, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, ParticleSystemSubType type, int subEmitterIndex, size_t particleIndex, float t, float dt, float length) +{ + size_t numContinuous = 0; + Vector3f initialPosition; + Vector3f initialVelocity; + CalculatePositionAndVelocity(initialPosition, initialVelocity, roState, state, ps, particleIndex); + int amountOfParticlesToEmit = ParticleSystem::EmitFromData (emissionState, numContinuous, subEmitterData.emissionData, initialVelocity, t, std::min(t + dt, length), dt, length); + if(amountOfParticlesToEmit) + { + if(!state.recordSubEmits) + ParticleSystem::Emit(*subEmitterData.emitter, SubEmitterEmitCommand(emissionState, initialPosition, initialVelocity, type, subEmitterIndex, amountOfParticlesToEmit, numContinuous, t, dt), kParticleSystemEMStaging); + else if(!state.subEmitterCommandBuffer.IsFull()) + state.subEmitterCommandBuffer.AddCommand(emissionState, initialPosition, initialVelocity, type, subEmitterIndex, amountOfParticlesToEmit, numContinuous, t, dt); + } +} + +bool GetTransformationMatrix(Matrix4x4f& output, const bool isSystemInWorld, const bool isCurveInWorld, const Matrix4x4f& localToWorld) +{ + if(isCurveInWorld != isSystemInWorld) + { + if(isSystemInWorld) + output = localToWorld; + else + Matrix4x4f::Invert_General3D(localToWorld, output); + return true; + } + else + { + output = Matrix4x4f::identity; + return false; + } +} + +bool GetTransformationMatrices(Matrix4x4f& output, Matrix4x4f& outputInverse, const bool isSystemInWorld, const bool isCurveInWorld, const Matrix4x4f& localToWorld) +{ + if(isCurveInWorld != isSystemInWorld) + { + if(isSystemInWorld) + { + output = localToWorld; + Matrix4x4f::Invert_General3D(localToWorld, outputInverse); + } + else + { + Matrix4x4f::Invert_General3D(localToWorld, output); + outputInverse = localToWorld; + } + return true; + } + else + { + output = Matrix4x4f::identity; + outputInverse = Matrix4x4f::identity; + return false; + } +} diff --git a/Runtime/Graphics/ParticleSystem/ParticleSystemUtils.h b/Runtime/Graphics/ParticleSystem/ParticleSystemUtils.h new file mode 100644 index 0000000..45ec0a3 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleSystemUtils.h @@ -0,0 +1,48 @@ +#ifndef SHURIKENUTILS_H +#define SHURIKENUTILS_H + +#include "ParticleSystemCommon.h" +#include "Runtime/Math/Vector2.h" +#include "Runtime/Math/Random/Random.h" + +class Matrix4x4f; +struct ParticleSystemParticles; +struct ParticleSystemReadOnlyState; +struct ParticleSystemState; +struct ParticleSystemEmissionState; +struct ParticleSystemSubEmitterData; + +inline float InverseLerpFast01 (const Vector2f& scaleOffset, float v) +{ + return clamp01 (v * scaleOffset.x + scaleOffset.y); +} + +inline float GenerateRandom(UInt32 randomIn) +{ + Rand rand(randomIn); + return Random01(rand); +} + +inline void GenerateRandom3(Vector3f& randomOut, UInt32 randomIn) +{ + Rand rand(randomIn); + randomOut.x = Random01(rand); + randomOut.y = Random01(rand); + randomOut.z = Random01(rand); +} + +inline UInt8 GenerateRandomByte (UInt32 seed) +{ + Rand rand (seed); + return Rand::GetByteFromInt (rand.Get ()); +} + +UInt32 GetGlobalRandomSeed (); +void ResetGlobalRandomSeed (); +Vector2f CalculateInverseLerpOffsetScale (const Vector2f& range); +void KillParticle(const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, size_t index, size_t& particleCount); +void RecordEmit(ParticleSystemEmissionState& emissionState, const ParticleSystemSubEmitterData& subEmitterData, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, ParticleSystemSubType type, int subEmitterIndex, size_t particleIndex, float t, float dt, float length); +bool GetTransformationMatrix(Matrix4x4f& output, const bool isSystemInWorld, const bool isCurveInWorld, const Matrix4x4f& localToWorld); +bool GetTransformationMatrices(Matrix4x4f& output, Matrix4x4f& outputInverse, const bool isSystemInWorld, const bool isCurveInWorld, const Matrix4x4f& localToWorld); + +#endif // SHURIKENUTILS_H diff --git a/Runtime/Graphics/ParticleSystem/PolynomialCurve.cpp b/Runtime/Graphics/ParticleSystem/PolynomialCurve.cpp new file mode 100644 index 0000000..f20b0ac --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/PolynomialCurve.cpp @@ -0,0 +1,405 @@ +#include "UnityPrefix.h" +#include "PolynomialCurve.h" +#include "Runtime/Math/Vector2.h" +#include "Runtime/Math/Polynomials.h" +#include "Runtime/Math/AnimationCurve.h" + +static void DoubleIntegrateSegment (float* coeff) +{ + coeff[0] /= 20.0F; + coeff[1] /= 12.0F; + coeff[2] /= 6.0F; + coeff[3] /= 2.0F; +} + +static void IntegrateSegment (float* coeff) +{ + coeff[0] /= 4.0F; + coeff[1] /= 3.0F; + coeff[2] /= 2.0F; + coeff[3] /= 1.0F; +} + +void CalculateMinMax(Vector2f& minmax, float value) +{ + minmax.x = std::min(minmax.x, value); + minmax.y = std::max(minmax.y, value); +} + +void ConstrainToPolynomialCurve (AnimationCurve& curve) +{ + const int max = OptimizedPolynomialCurve::kMaxPolynomialKeyframeCount; + + // Maximum 3 keys + if (curve.GetKeyCount () > max) + curve.RemoveKeys(curve.begin() + max, curve.end()); + + // Clamp begin and end to 0...1 range + if (curve.GetKeyCount () >= 2) + { + curve.GetKey(0).time = 0; + curve.GetKey(curve.GetKeyCount ()-1).time = 1; + } +} + +bool IsValidPolynomialCurve (const AnimationCurve& curve) +{ + // Maximum 3 keys + if (curve.GetKeyCount () > OptimizedPolynomialCurve::kMaxPolynomialKeyframeCount) + return false; + // One constant key can always be representated + else if (curve.GetKeyCount () <= 1) + return true; + // First and last keyframe must be at 0 and 1 time + else + { + float beginTime = curve.GetKey(0).time; + float endTime = curve.GetKey(curve.GetKeyCount ()-1).time; + + return CompareApproximately(beginTime, 0.0F, 0.0001F) && CompareApproximately(endTime, 1.0F, 0.0001F); + } +} + +void SetPolynomialCurveToValue (AnimationCurve& a, OptimizedPolynomialCurve& c, float value) +{ + AnimationCurve::Keyframe keys[2] = { AnimationCurve::Keyframe(0.0f, value), AnimationCurve::Keyframe(1.0f, value) }; + a.Assign(keys, keys + 2); + c.BuildOptimizedCurve(a, 1.0f); +} + +void SetPolynomialCurveToLinear (AnimationCurve& a, OptimizedPolynomialCurve& c) +{ + AnimationCurve::Keyframe keys[2] = { AnimationCurve::Keyframe(0.0f, 0.0f), AnimationCurve::Keyframe(1.0f, 1.0f) }; + keys[0].inSlope = 0.0f; keys[0].outSlope = 1.0f; + keys[1].inSlope = 1.0f; keys[1].outSlope = 0.0f; + a.Assign(keys, keys + 2); + c.BuildOptimizedCurve(a, 1.0f); +} + +bool OptimizedPolynomialCurve::BuildOptimizedCurve (const AnimationCurve& editorCurve, float scale) +{ + if (!IsValidPolynomialCurve(editorCurve)) + return false; + + const size_t keyCount = editorCurve.GetKeyCount (); + + timeValue = 1.0F; + memset(segments, 0, sizeof(segments)); + + // Handle corner case 1 & 0 keyframes + if (keyCount == 0) + ; + else if (keyCount == 1) + { + // Set constant value coefficient + for (int i=0;i<kSegmentCount;i++) + segments[i].coeff[3] = editorCurve.GetKey(0).value * scale; + } + else + { + float segmentStartTime[kSegmentCount]; + + for (int i=0;i<kSegmentCount;i++) + { + bool hasSegment = i+1 < keyCount; + if (hasSegment) + { + AnimationCurve::Cache cache; + editorCurve.CalculateCacheData(cache, i, i + 1, 0.0F); + + memcpy(segments[i].coeff, cache.coeff, sizeof(Polynomial)); + segmentStartTime[i] = editorCurve.GetKey(i).time; + } + else + { + memcpy(segments[i].coeff, segments[i-1].coeff, sizeof(Polynomial)); + segmentStartTime[i] = 1.0F;//timeValue[i-1]; + } + } + + // scale curve + for (int i=0;i<kSegmentCount;i++) + { + segments[i].coeff[0] *= scale; + segments[i].coeff[1] *= scale; + segments[i].coeff[2] *= scale; + segments[i].coeff[3] *= scale; + } + + // Timevalue 0 is always 0.0F. No need to store it. + timeValue = segmentStartTime[1]; + + #if !UNITY_RELEASE + // Check that UI editor curve matches polynomial curve except if the + // scale happens to be infinite (trying to subtract infinity values from + // each other yields NaN with IEEE floats). + if (scale != std::numeric_limits<float>::infinity () && + scale != -std::numeric_limits<float>::infinity()) + { + for (int i=0;i<=50;i++) + { + // The very last element at 1.0 can be different when using step curves. + // The AnimationCurve implementation will sample the position of the last key. + // The OptimizedPolynomialCurve will keep value of the previous key (Continuing the trajectory of the segment) + // In practice this is probably not a problem, since you don't sample 1.0 because then the particle will be dead. + // thus we just don't do the automatic assert when the curves are not in sync in that case. + float t = std::min(i / 50.0F, 0.99999F); + float dif; + + DebugAssert((dif = Abs(Evaluate(t) - editorCurve.Evaluate(t) * scale)) < 0.01F); + } + } + #endif + } + + return true; +} + +void OptimizedPolynomialCurve::Integrate () +{ + for (int i=0;i<kSegmentCount;i++) + IntegrateSegment(segments[i].coeff); +} + +void OptimizedPolynomialCurve::DoubleIntegrate () +{ + Polynomial velocity0 = segments[0]; + IntegrateSegment (velocity0.coeff); + + velocityValue = Polynomial::EvalSegment(timeValue, velocity0.coeff) * timeValue; + + for (int i=0;i<kSegmentCount;i++) + DoubleIntegrateSegment(segments[i].coeff); +} + +Vector2f OptimizedPolynomialCurve::FindMinMaxDoubleIntegrated() const +{ + // Because of velocityValue * t, this becomes a quartic polynomial (4th order polynomial). + // TODO: Find all roots of quartic polynomial + Vector2f result = Vector2f::zero; + const int numSteps = 20; + const float delta = 1.0f / float(numSteps); + float acc = delta; + for(int i = 0; i < numSteps; i++) + { + CalculateMinMax(result, EvaluateDoubleIntegrated(acc)); + acc += delta; + } + return result; +} + +// Find the maximum of the integrated curve (x: min, y: max) +Vector2f OptimizedPolynomialCurve::FindMinMaxIntegrated() const +{ + Vector2f result = Vector2f::zero; + + float start[kSegmentCount] = {0.0f, timeValue}; + float end[kSegmentCount] = {timeValue, 1.0f}; + for(int i = 0; i < kSegmentCount; i++) + { + // Differentiate coefficients + float a = 4.0f*segments[i].coeff[0]; + float b = 3.0f*segments[i].coeff[1]; + float c = 2.0f*segments[i].coeff[2]; + float d = 1.0f*segments[i].coeff[3]; + + float roots[3]; + int numRoots = CubicPolynomialRootsGeneric(roots, a, b, c, d); + for(int r = 0; r < numRoots; r++) + { + float root = roots[r] + start[i]; + if((root >= start[i]) && (root < end[i])) + CalculateMinMax(result, EvaluateIntegrated(root)); + } + + // TODO: Don't use eval integrated, use eval segment (and integrate in loop) + CalculateMinMax(result, EvaluateIntegrated(end[i])); + } + return result; +} + +// Find the maximum of a double integrated curve (x: min, y: max) +Vector2f PolynomialCurve::FindMinMaxDoubleIntegrated() const +{ + // Because of velocityValue * t, this becomes a quartic polynomial (4th order polynomial). + // TODO: Find all roots of quartic polynomial + Vector2f result = Vector2f::zero; + const int numSteps = 20; + const float delta = 1.0f / float(numSteps); + float acc = delta; + for(int i = 0; i < numSteps; i++) + { + CalculateMinMax(result, EvaluateDoubleIntegrated(acc)); + acc += delta; + } + return result; +} + +Vector2f PolynomialCurve::FindMinMaxIntegrated() const +{ + Vector2f result = Vector2f::zero; + + float prevTimeValue = 0.0f; + for(int i = 0; i < segmentCount; i++) + { + // Differentiate coefficients + float a = 4.0f*segments[i].coeff[0]; + float b = 3.0f*segments[i].coeff[1]; + float c = 2.0f*segments[i].coeff[2]; + float d = 1.0f*segments[i].coeff[3]; + + float roots[3]; + int numRoots = CubicPolynomialRootsGeneric(roots, a, b, c, d); + for(int r = 0; r < numRoots; r++) + { + float root = roots[r] + prevTimeValue; + if((root >= prevTimeValue) && (root < times[i])) + CalculateMinMax(result, EvaluateIntegrated(root)); + } + + // TODO: Don't use eval integrated, use eval segment (and integrate in loop) + CalculateMinMax(result, EvaluateIntegrated(times[i])); + prevTimeValue = times[i]; + } + return result; +} + +bool PolynomialCurve::IsValidCurve(const AnimationCurve& editorCurve) +{ + int keyCount = editorCurve.GetKeyCount(); + int segmentCount = keyCount - 1; + if(editorCurve.GetKey(0).time != 0.0f) + segmentCount++; + if(editorCurve.GetKey(keyCount-1).time != 1.0f) + segmentCount++; + return segmentCount <= kMaxNumSegments; +} + +bool PolynomialCurve::BuildCurve(const AnimationCurve& editorCurve, float scale) +{ + int keyCount = editorCurve.GetKeyCount(); + segmentCount = 1; + + const float kMaxTime = 1.01f; + + memset(segments, 0, sizeof(segments)); + memset(integrationCache, 0, sizeof(integrationCache)); + memset(doubleIntegrationCache, 0, sizeof(doubleIntegrationCache)); + memset(times, 0, sizeof(times)); + times[0] = kMaxTime; + + // Handle corner case 1 & 0 keyframes + if (keyCount == 0) + ; + else if (keyCount == 1) + { + // Set constant value coefficient + segments[0].coeff[3] = editorCurve.GetKey(0).value * scale; + } + else + { + segmentCount = keyCount - 1; + int segmentOffset = 0; + + // Add extra key to start if it doesn't match up + if(editorCurve.GetKey(0).time != 0.0f) + { + segments[0].coeff[3] = editorCurve.GetKey(0).value; + times[0] = editorCurve.GetKey(0).time; + segmentOffset = 1; + } + + for (int i = 0;i<segmentCount;i++) + { + DebugAssert(i+1 < keyCount); + AnimationCurve::Cache cache; + editorCurve.CalculateCacheData(cache, i, i + 1, 0.0F); + memcpy(segments[i+segmentOffset].coeff, cache.coeff, 4 * sizeof(float)); + times[i+segmentOffset] = editorCurve.GetKey(i+1).time; + } + segmentCount += segmentOffset; + + // Add extra key to start if it doesn't match up + if(editorCurve.GetKey(keyCount-1).time != 1.0f) + { + segments[segmentCount].coeff[3] = editorCurve.GetKey(keyCount-1).value; + segmentCount++; + } + + // Fixup last key time value + times[segmentCount-1] = kMaxTime; + + for (int i = 0;i<segmentCount;i++) + { + segments[i].coeff[0] *= scale; + segments[i].coeff[1] *= scale; + segments[i].coeff[2] *= scale; + segments[i].coeff[3] *= scale; + } + } + + DebugAssert(segmentCount <= kMaxNumSegments); + +#if !UNITY_RELEASE + // Check that UI editor curve matches polynomial curve + for (int i=0;i<=10;i++) + { + // The very last element at 1.0 can be different when using step curves. + // The AnimationCurve implementation will sample the position of the last key. + // The PolynomialCurve will keep value of the previous key (Continuing the trajectory of the segment) + // In practice this is probably not a problem, since you don't sample 1.0 because then the particle will be dead. + // thus we just don't do the automatic assert when the curves are not in sync in that case. + float t = std::min(i / 50.0F, 0.99999F); + float dif; + DebugAssert((dif = Abs(Evaluate(t) - editorCurve.Evaluate(t) * scale)) < 0.01F); + } +#endif + return true; +} + +void GenerateIntegrationCache(PolynomialCurve& curve) +{ + curve.integrationCache[0] = 0.0f; + float prevTimeValue0 = curve.times[0]; + float prevTimeValue1 = 0.0f; + for (int i=1;i<curve.segmentCount;i++) + { + float coeff[4]; + memcpy(coeff, curve.segments[i-1].coeff, 4*sizeof(float)); + IntegrateSegment (coeff); + float time = prevTimeValue0 - prevTimeValue1; + curve.integrationCache[i] = curve.integrationCache[i-1] + Polynomial::EvalSegment(time, coeff) * time; + prevTimeValue1 = prevTimeValue0; + prevTimeValue0 = curve.times[i]; + } +} + +// Expects double integrated segments and valid integration cache +void GenerateDoubleIntegrationCache(PolynomialCurve& curve) +{ + float sum = 0.0f; + float prevTimeValue = 0.0f; + for(int i = 0; i < curve.segmentCount; i++) + { + curve.doubleIntegrationCache[i] = sum; + float time = curve.times[i] - prevTimeValue; + time = std::max(time, 0.0f); + sum += Polynomial::EvalSegment(time, curve.segments[i].coeff) * time * time + curve.integrationCache[i] * time; + prevTimeValue = curve.times[i]; + } +} + +void PolynomialCurve::Integrate () +{ + GenerateIntegrationCache(*this); + for (int i=0;i<segmentCount;i++) + IntegrateSegment(segments[i].coeff); +} + +void PolynomialCurve::DoubleIntegrate () +{ + GenerateIntegrationCache(*this); + for (int i=0;i<segmentCount;i++) + DoubleIntegrateSegment(segments[i].coeff); + GenerateDoubleIntegrationCache(*this); +} diff --git a/Runtime/Graphics/ParticleSystem/PolynomialCurve.h b/Runtime/Graphics/ParticleSystem/PolynomialCurve.h new file mode 100644 index 0000000..d9e7270 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/PolynomialCurve.h @@ -0,0 +1,229 @@ +#ifndef POLYONOMIAL_CURVE_H +#define POLYONOMIAL_CURVE_H + +template<class T> +class AnimationCurveTpl; +typedef AnimationCurveTpl<float> AnimationCurve; +class Vector2f; + +struct Polynomial +{ + static float EvalSegment (float t, const float* coeff) + { + return (t * (t * (t * coeff[0] + coeff[1]) + coeff[2])) + coeff[3]; + } + + float coeff[4]; +}; + +// Smaller, optimized version +struct OptimizedPolynomialCurve +{ + enum { kMaxPolynomialKeyframeCount = 3, kSegmentCount = kMaxPolynomialKeyframeCount-1, }; + + Polynomial segments[kSegmentCount]; + + float timeValue; + float velocityValue; + + // Evaluate double integrated Polynomial curve. + // Example: position = EvaluateDoubleIntegrated (normalizedTime) * startEnergy^2 + // Use DoubleIntegrate function to for example turn a force curve into a position curve. + // Expects that t is in the 0...1 range. + float EvaluateDoubleIntegrated (float t) const + { + DebugAssert(t >= -0.01F && t <= 1.01F); + float res0, res1; + + // All segments are added together. At t = 0, the integrated curve is always zero. + + // 0 segment is sampled up to the 1 keyframe + // First key is always assumed to be at 0 time + float t1 = std::min(t, timeValue); + + // 1 segment is sampled from 1 key to 2 key + // Last key is always assumed to be at 1 time + float t2 = std::max(0.0F, t - timeValue); + + res0 = Polynomial::EvalSegment(t1, segments[0].coeff) * t1 * t1; + res1 = Polynomial::EvalSegment(t2, segments[1].coeff) * t2 * t2; + + float finalResult = res0 + res1; + + // Add velocity of previous segments + finalResult += velocityValue * std::max(t - timeValue, 0.0F); + + return finalResult; + } + + // Evaluate integrated Polynomial curve. + // Example: position = EvaluateIntegrated (normalizedTime) * startEnergy + // Use Integrate function to for example turn a velocity curve into a position curve. + // Expects that t is in the 0...1 range. + float EvaluateIntegrated (float t) const + { + DebugAssert(t >= -0.01F && t <= 1.01F); + float res0, res1; + + // All segments are added together. At t = 0, the integrated curve is always zero. + + // 0 segment is sampled up to the 1 keyframe + // First key is always assumed to be at 0 time + float t1 = std::min(t, timeValue); + + // 1 segment is sampled from 1 key to 2 key + // Last key is always assumed to be at 1 time + float t2 = std::max(0.0F, t - timeValue); + + res0 = Polynomial::EvalSegment(t1, segments[0].coeff) * t1; + res1 = Polynomial::EvalSegment(t2, segments[1].coeff) * t2; + + return (res0 + res1); + } + + // Evaluate the curve + // extects that t is in the 0...1 range + float Evaluate (float t) const + { + DebugAssert(t >= -0.01F && t <= 1.01F); + + float res0 = Polynomial::EvalSegment(t, segments[0].coeff); + float res1 = Polynomial::EvalSegment(t - timeValue, segments[1].coeff); + + float result; + if (t > timeValue) + result = res1; + else + result = res0; + + return result; + } + + // Find the maximum of a double integrated curve (x: min, y: max) + Vector2f FindMinMaxDoubleIntegrated() const; + + // Find the maximum of the integrated curve (x: min, y: max) + Vector2f FindMinMaxIntegrated() const; + + // Precalculates polynomials from the animation curve and a scale factor + bool BuildOptimizedCurve (const AnimationCurve& editorCurve, float scale); + + // Integrates a velocity curve to be a position curve. + // You have to call EvaluateIntegrated to evaluate the curve + void Integrate (); + + // Integrates a velocity curve to be a position curve. + // You have to call EvaluateDoubleIntegrated to evaluate the curve + void DoubleIntegrate (); + + // Add a constant force to a velocity curve + // Assumes that you have already called Integrate on the velocity curve. + void AddConstantForceToVelocityCurve (float gravity) + { + for (int i=0;i<kSegmentCount;i++) + segments[i].coeff[1] += 0.5F * gravity; + } +}; + +// Bigger, not so optimized version +struct PolynomialCurve +{ + enum{ kMaxNumSegments = 8 }; + + Polynomial segments[kMaxNumSegments]; // Cached polynomial coefficients + float integrationCache[kMaxNumSegments]; // Cache of integrated values up until start of segments + float doubleIntegrationCache[kMaxNumSegments]; // Cache of double integrated values up until start of segments + float times[kMaxNumSegments]; // Time value for end of segment + + int segmentCount; + + // Find the maximum of a double integrated curve (x: min, y: max) + Vector2f FindMinMaxDoubleIntegrated() const; + + // Find the maximum of the integrated curve (x: min, y: max) + Vector2f FindMinMaxIntegrated() const; + + // Precalculates polynomials from the animation curve and a scale factor + bool BuildCurve(const AnimationCurve& editorCurve, float scale); + + // Integrates a velocity curve to be a position curve. + // You have to call EvaluateIntegrated to evaluate the curve + void Integrate (); + + // Integrates a velocity curve to be a position curve. + // You have to call EvaluateDoubleIntegrated to evaluate the curve + void DoubleIntegrate (); + + // Evaluates if it is possible to represent animation curve as PolynomialCurve + static bool IsValidCurve(const AnimationCurve& editorCurve); + + // Evaluate double integrated Polynomial curve. + // Example: position = EvaluateDoubleIntegrated (normalizedTime) * startEnergy^2 + // Use DoubleIntegrate function to for example turn a force curve into a position curve. + // Expects that t is in the 0...1 range. + float EvaluateDoubleIntegrated (float t) const + { + DebugAssert(t >= -0.01F && t <= 1.01F); + + float prevTimeValue = 0.0f; + for(int i = 0; i < segmentCount; i++) + { + if(t <= times[i]) + { + const float time = t - prevTimeValue; + return doubleIntegrationCache[i] + integrationCache[i] * time + Polynomial::EvalSegment(time, segments[i].coeff) * time * time; + } + prevTimeValue = times[i]; + } + DebugAssert(!"PolyCurve: Outside segment range!"); + return 1.0f; + } + + // Evaluate integrated Polynomial curve. + // Example: position = EvaluateIntegrated (normalizedTime) * startEnergy + // Use Integrate function to for example turn a velocity curve into a position curve. + // Expects that t is in the 0...1 range. + float EvaluateIntegrated (float t) const + { + DebugAssert(t >= -0.01F && t <= 1.01F); + + float prevTimeValue = 0.0f; + for(int i = 0; i < segmentCount; i++) + { + if(t <= times[i]) + { + const float time = t - prevTimeValue; + return integrationCache[i] + Polynomial::EvalSegment(time, segments[i].coeff) * time; + } + prevTimeValue = times[i]; + } + DebugAssert(!"PolyCurve: Outside segment range!"); + return 1.0f; + } + + // Evaluate the curve + // extects that t is in the 0...1 range + float Evaluate(float t) const + { + DebugAssert(t >= -0.01F && t <= 1.01F); + + float prevTimeValue = 0.0f; + for(int i = 0; i < segmentCount; i++) + { + if(t <= times[i]) + return Polynomial::EvalSegment(t - prevTimeValue, segments[i].coeff); + prevTimeValue = times[i]; + } + DebugAssert(!"PolyCurve: Outside segment range!"); + return 1.0f; + } +}; + + +void SetPolynomialCurveToValue (AnimationCurve& a, OptimizedPolynomialCurve& c, float value); +void SetPolynomialCurveToLinear (AnimationCurve& a, OptimizedPolynomialCurve& c); +void ConstrainToPolynomialCurve (AnimationCurve& curve); +bool IsValidPolynomialCurve (const AnimationCurve& curve); +void CalculateMinMax(Vector2f& minmax, float value); + +#endif // POLYONOMIAL_CURVE_H diff --git a/Runtime/Graphics/ParticleSystem/PolynomialCurveTests.cpp b/Runtime/Graphics/ParticleSystem/PolynomialCurveTests.cpp new file mode 100644 index 0000000..2a74710 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/PolynomialCurveTests.cpp @@ -0,0 +1,259 @@ +#include "UnityPrefix.h" +#include "Configuration/UnityConfigure.h" + +#if ENABLE_UNIT_TESTS + +#include "External/UnitTest++/src/UnitTest++.h" +#include "Runtime/Animation/AnimationCurveUtility.h" +#include "Runtime/Math/Vector2.h" +#include "Runtime/Graphics/ParticleSystem/PolynomialCurve.h" + +static float PhysicsSimulate (float gravity, float velocity, float time) +{ + return velocity * time + gravity * time * time * 0.5F; +} + +SUITE (PolynomialCurveTests) +{ +TEST (PolynomialCurve_ConstantIntegrate) +{ + AnimationCurve editorCurve; + OptimizedPolynomialCurve curve; + PolynomialCurve polyCurve; + + SetPolynomialCurveToValue(editorCurve, curve, 1.0f); + curve.Integrate(); + polyCurve.BuildCurve(editorCurve, 1.0f); + polyCurve.Integrate(); + + CHECK_CLOSE(0.0F, curve.EvaluateIntegrated(0.0F), 0.0001F); + CHECK_CLOSE(0.5F, curve.EvaluateIntegrated(0.5F), 0.0001F); + CHECK_CLOSE(1.0F, curve.EvaluateIntegrated(1.0F), 0.0001F); + + CHECK_CLOSE(0.0F, polyCurve.EvaluateIntegrated(0.0F), 0.0001F); + CHECK_CLOSE(0.5F, polyCurve.EvaluateIntegrated(0.5F), 0.0001F); + CHECK_CLOSE(1.0F, polyCurve.EvaluateIntegrated(1.0F), 0.0001F); + + CHECK_EQUAL(true, CompareApproximately(Vector2f(0.0f, 1.0f), curve.FindMinMaxIntegrated(), 0.0001F)); + CHECK_EQUAL(true, CompareApproximately(Vector2f(0.0f, 1.01f), polyCurve.FindMinMaxIntegrated(), 0.0001F)); + + + SetPolynomialCurveToLinear(editorCurve, curve); + curve.Integrate(); + polyCurve.BuildCurve(editorCurve, 1.0f); + polyCurve.Integrate(); + + CHECK_CLOSE(0.0F, curve.EvaluateIntegrated(0.0F), 0.0001F); + CHECK_CLOSE(0.125F, curve.EvaluateIntegrated(0.5F), 0.0001F); + CHECK_CLOSE(0.5F, curve.EvaluateIntegrated(1.0F), 0.0001F); + + CHECK_CLOSE(0.0F, polyCurve.EvaluateIntegrated(0.0F), 0.0001F); + CHECK_CLOSE(0.125F, polyCurve.EvaluateIntegrated(0.5F), 0.0001F); + CHECK_CLOSE(0.5F, polyCurve.EvaluateIntegrated(1.0F), 0.0001F); + + CHECK_EQUAL(true, CompareApproximately(Vector2f(0.0f, 0.5f), curve.FindMinMaxIntegrated(), 0.0001F)); + CHECK_EQUAL(true, CompareApproximately(Vector2f(0.0f, 0.51005f), polyCurve.FindMinMaxIntegrated(), 0.0001F)); +} + +// @TODO: Add generic poly +TEST (PolynomialCurve_ConstantDoubleIntegrate) +{ + AnimationCurve editorCurve; + OptimizedPolynomialCurve curve; + PolynomialCurve polyCurve; + + SetPolynomialCurveToValue(editorCurve, curve, 1.0f); + curve.DoubleIntegrate(); + + polyCurve.BuildCurve(editorCurve, 1.0f); + polyCurve.DoubleIntegrate(); + + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.00F), curve.EvaluateDoubleIntegrated(0.00F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.25F), curve.EvaluateDoubleIntegrated(0.25F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.50F), curve.EvaluateDoubleIntegrated(0.50F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.75F), curve.EvaluateDoubleIntegrated(0.75F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 1.00F), curve.EvaluateDoubleIntegrated(1.00F), 0.0001F); + + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.00F), polyCurve.EvaluateDoubleIntegrated(0.00F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.25F), polyCurve.EvaluateDoubleIntegrated(0.25F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.50F), polyCurve.EvaluateDoubleIntegrated(0.50F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.75F), polyCurve.EvaluateDoubleIntegrated(0.75F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 1.00F), polyCurve.EvaluateDoubleIntegrated(1.00F), 0.0001F); + + CHECK_EQUAL(true, CompareApproximately(Vector2f(0.0f, 0.5f), curve.FindMinMaxDoubleIntegrated(), 0.0001F)); + CHECK_EQUAL(true, CompareApproximately(Vector2f(0.0f, 0.5f), polyCurve.FindMinMaxDoubleIntegrated(), 0.0001F)); + + AnimationCurve::Keyframe keys[3] = { AnimationCurve::Keyframe(0.0f, 1.0f), AnimationCurve::Keyframe(0.5f, 1.0f), AnimationCurve::Keyframe(1.0f, 1.0f) }; + editorCurve.Assign(keys, keys + 3); + curve.BuildOptimizedCurve(editorCurve, 1); + curve.DoubleIntegrate(); + + polyCurve.BuildCurve(editorCurve, 1.0f); + polyCurve.DoubleIntegrate(); + + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.00F), curve.EvaluateDoubleIntegrated(0.00F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.25F), curve.EvaluateDoubleIntegrated(0.25F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.50F), curve.EvaluateDoubleIntegrated(0.50F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.75F), curve.EvaluateDoubleIntegrated(0.75F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 1.00F), curve.EvaluateDoubleIntegrated(1.00F), 0.0001F); + + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.00F), polyCurve.EvaluateDoubleIntegrated(0.00F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.25F), polyCurve.EvaluateDoubleIntegrated(0.25F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.50F), polyCurve.EvaluateDoubleIntegrated(0.50F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.75F), polyCurve.EvaluateDoubleIntegrated(0.75F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 1.00F), polyCurve.EvaluateDoubleIntegrated(1.00F), 0.0001F); + + CHECK_EQUAL(true, CompareApproximately(Vector2f(0.0f, 0.5f), curve.FindMinMaxDoubleIntegrated(), 0.0001F)); + CHECK_EQUAL(true, CompareApproximately(Vector2f(0.0f, 0.5f), polyCurve.FindMinMaxDoubleIntegrated(), 0.0001F)); +} + +static float EvaluateGravityIntegrated (OptimizedPolynomialCurve& gravityCurve, float time, float maximumRange) +{ + float res = gravityCurve.EvaluateDoubleIntegrated(time / maximumRange) * maximumRange * maximumRange; + return res; +} + +TEST (PolynomialCurve_GravityIntegrate) +{ + const float kGravity = -9.81f; + AnimationCurve editorCurve; + OptimizedPolynomialCurve gravityCurve; + SetPolynomialCurveToValue(editorCurve, gravityCurve, kGravity); + gravityCurve.DoubleIntegrate(); + + const float kMaxRange = 5.0F; + + CHECK_CLOSE(PhysicsSimulate(kGravity, 0.0F, 0.0F), EvaluateGravityIntegrated(gravityCurve, 0.0F, kMaxRange), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(kGravity, 0.0F, 0.5F), EvaluateGravityIntegrated(gravityCurve, 0.5F, kMaxRange), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(kGravity, 0.0F, 1.0F), EvaluateGravityIntegrated(gravityCurve, 1.0F, kMaxRange), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(kGravity, 0.0F, 5.0F), EvaluateGravityIntegrated(gravityCurve, 5.0F, kMaxRange), 0.0001F); +} + +float TriangleShapeIntegralFirstHalf (float x) +{ + return (2.0F / 3.0F) *x*x*x - 0.5F*x*x; +} + +// Test that a simple triangle shape 0,-1 to 0.5,1 to 1,-1 +// Gives expected results during double integration +TEST (PolynomialCurve_TriangleShapeDoubleIntegrate) +{ + OptimizedPolynomialCurve curve; + AnimationCurve::Keyframe keys[3] = { AnimationCurve::Keyframe(0.0f, -1.0f), AnimationCurve::Keyframe(0.5f, 1.0f), AnimationCurve::Keyframe(1.0f, -1.0f) }; + AnimationCurve editorCurve; + editorCurve.Assign(keys, keys + 3); + + RecalculateSplineSlopeLinear(editorCurve); + + curve.BuildOptimizedCurve(editorCurve, 1.0F); + + CHECK_CLOSE(-1, curve.Evaluate(0), 0.0001F); + CHECK_CLOSE(0, curve.Evaluate(0.25F), 0.0001F); + CHECK_CLOSE(1.0F, curve.Evaluate(0.5F), 0.0001F); + CHECK_CLOSE(0.0F, curve.Evaluate(0.75F), 0.0001F); + CHECK_CLOSE(-1, curve.Evaluate(1), 0.0001F); + + curve.DoubleIntegrate(); + + CHECK_CLOSE(TriangleShapeIntegralFirstHalf(0), EvaluateGravityIntegrated(curve, 0.0F , 1.0F), 0.0001F); + CHECK_CLOSE(TriangleShapeIntegralFirstHalf(0.25F), EvaluateGravityIntegrated(curve, 0.25F, 1.0F), 0.0001F); + CHECK_CLOSE(TriangleShapeIntegralFirstHalf(0.5F), EvaluateGravityIntegrated(curve, 0.5F , 1.0F), 0.0001F); + CHECK_CLOSE(-0.0208333, EvaluateGravityIntegrated(curve, 0.75F, 1.0F), 0.0001F); + CHECK_CLOSE(0, EvaluateGravityIntegrated(curve, 1.0F , 1.0F), 0.0001F); +} + +void CompareIntegrateCurve (const AnimationCurve& curve, const OptimizedPolynomialCurve& integratedCurve) +{ + CHECK_CLOSE(0, integratedCurve.EvaluateIntegrated(0), 0.0001F); + + int intervals = 100; + + float sum = 0.0F; + for (int i=1;i<intervals;i++) + { + float t = (float)i / (float)intervals; + float dt = 1.0F / (float)intervals; + + float avgValue = curve.Evaluate(t - dt * 0.5F); + sum += avgValue * dt; + + float integratedValue = integratedCurve.EvaluateIntegrated(t); + CHECK_CLOSE(sum, integratedValue, 0.0001F); + } +} + +void CompareDoubleIntegrateCurve (const AnimationCurve& curve, const OptimizedPolynomialCurve& integratedCurve) +{ + CHECK_CLOSE(0, integratedCurve.EvaluateIntegrated(0), 0.0001F); + + int intervals = 1000; + + float integratedSum = 0.0F; + float sum = 0.0F; + for (int i=1;i<intervals;i++) + { + float t = (float)i / (float)intervals; + float dt = 1.0F / (float)intervals; + + float avgValue = curve.Evaluate(t - dt * 0.5F); + sum += avgValue * dt; + integratedSum += sum * dt; + + float integratedValue = integratedCurve.EvaluateDoubleIntegrated(t); + CHECK_CLOSE(integratedSum, integratedValue, 0.001F); + } +} + +void CompareIntegrateCurve (const AnimationCurve::Keyframe* keys, int size) +{ + AnimationCurve sourceCurve; + sourceCurve.Assign(keys, keys + size); + + OptimizedPolynomialCurve curve; + AnimationCurve editorCurve; + editorCurve = sourceCurve; + curve.BuildOptimizedCurve(editorCurve, 1); + curve.Integrate(); + + CompareIntegrateCurve(sourceCurve, curve); +} + +void CompareDoubleIntegrateCurve (const AnimationCurve::Keyframe* keys, int size) +{ + AnimationCurve sourceCurve; + sourceCurve.Assign(keys, keys + size); + + OptimizedPolynomialCurve curve; + AnimationCurve editorCurve; + editorCurve = sourceCurve; + curve.BuildOptimizedCurve(editorCurve, 1); + curve.DoubleIntegrate(); + + CompareDoubleIntegrateCurve(sourceCurve, curve); +} + +TEST (PolynomialCurve_TriangleCurve) +{ + AnimationCurve sourceCurve; + AnimationCurve::Keyframe keys[3] = { AnimationCurve::Keyframe(0.0f, 0.0f), AnimationCurve::Keyframe(0.5f, 1.0f), AnimationCurve::Keyframe(1.0f, 0.0f) }; + sourceCurve.Assign(keys, keys + 3); + RecalculateSplineSlopeLinear(sourceCurve); + + CompareIntegrateCurve(&sourceCurve.GetKey(0), sourceCurve.GetKeyCount()); + CompareDoubleIntegrateCurve(&sourceCurve.GetKey(0), sourceCurve.GetKeyCount()); +} + + +TEST (PolynomialCurve_LineCurve) +{ + AnimationCurve sourceCurve; + AnimationCurve::Keyframe keys[2] = { AnimationCurve::Keyframe(0.0f, 0.0f), AnimationCurve::Keyframe(1.0f, 1.0f) }; + sourceCurve.Assign(keys, keys + 2); + RecalculateSplineSlopeLinear(sourceCurve); + + CompareIntegrateCurve(&sourceCurve.GetKey(0), sourceCurve.GetKeyCount()); + CompareDoubleIntegrateCurve(&sourceCurve.GetKey(0), sourceCurve.GetKeyCount()); +} +} + +#endif diff --git a/Runtime/Graphics/Polygon2D.cpp b/Runtime/Graphics/Polygon2D.cpp new file mode 100644 index 0000000..9985d65 --- /dev/null +++ b/Runtime/Graphics/Polygon2D.cpp @@ -0,0 +1,145 @@ +#include "UnityPrefix.h" +#include "Polygon2D.h" + +#if ENABLE_SPRITES +#include "Runtime/Geometry/Intersection.h" +#include "Runtime/Graphics/SpriteFrame.h" + +Polygon2D::Polygon2D() +{ + Reset(); +} + +void Polygon2D::SetPoints (const Vector2f* points, size_t count) +{ + m_Paths.resize(1, TPath(kMemPhysics)); + m_Paths[0].clear(); + m_Paths[0].assign(points, points + count); +} + +void Polygon2D::SetPathCount (int pathCount) +{ + m_Paths.resize(pathCount); +} + +void Polygon2D::SetPath (int index, const TPath& path) +{ + if (index == 0 && GetPathCount() == 0) + { + m_Paths.resize(1); + } + else if (index >= GetPathCount()) + { + ErrorString("Failed setting path. Index is out of bounds."); + return; + } + + m_Paths[index] = path; +} + +void Polygon2D::CopyFrom (const Polygon2D& paths) +{ + const int pathCount = paths.GetPathCount (); + if (pathCount == 0) + { + m_Paths.clear (); + return; + } + + // Transfer paths. + m_Paths.resize (pathCount); + for (int index = 0; index < pathCount; ++index) + m_Paths[index] = paths.GetPath (index); +} + +void Polygon2D::GenerateFrom(Sprite* sprite, const Vector2f& offset, float detail, unsigned char alphaTolerance, bool holeDetection) +{ + m_Paths.clear(); + sprite->GenerateOutline(detail, alphaTolerance, holeDetection, m_Paths, 0); + + if (offset.x != 0.0f || offset.y != 0.0f) + { + for (TPaths::iterator pit = m_Paths.begin(); pit != m_Paths.end(); ++pit) + { + TPath& path = *pit; + for (TPath::iterator it = path.begin(); it != path.end(); ++it) + { + Vector2f& point = *it; + point += offset; + } + } + } +} + +void Polygon2D::Reset() +{ + m_Paths.resize(1); + m_Paths[0].clear(); + m_Paths[0].reserve(4); + m_Paths[0].push_back(Vector2f(-1, -1)); + m_Paths[0].push_back(Vector2f(-1, 1)); + m_Paths[0].push_back(Vector2f( 1, 1)); + m_Paths[0].push_back(Vector2f( 1, -1)); +} + +bool Polygon2D::GetNearestPoint(const Vector2f& point, int& pathIndex, int& pointIndex, float& distance) const +{ + bool ret = false; + float sqrDist = std::numeric_limits<float>::max(); + + const int pathCount = m_Paths.size(); + for (int i = 0; i < pathCount; ++i) + { + const Polygon2D::TPath& path = m_Paths[i]; + const int pointCount = path.size(); + for (int j = 0; j < pointCount; ++j) + { + const Vector2f& testPoint = path[j]; + float d = SqrMagnitude(testPoint - point); + if (d < sqrDist) + { + sqrDist = d; + ret = true; + + pathIndex = i; + pointIndex = j; + distance = SqrtImpl(d); + } + } + } + + return ret; +} + +bool Polygon2D::GetNearestEdge(const Vector2f& point, int& pathIndex, int& pointIndex0, int& pointIndex1, float& distance, bool loop) const +{ + bool ret = false; + float dist = std::numeric_limits<float>::max(); + + const int pathCount = m_Paths.size(); + for (int i = 0; i < pathCount; ++i) + { + const Polygon2D::TPath& path = m_Paths[i]; + const int pointCount = path.size(); + const int edgeCount = loop ? pointCount : pointCount - 1; + for (int p0 = 0; p0 < edgeCount; ++p0) + { + int p1 = (p0 + 1) % pointCount; + float d = DistancePointLine<Vector2f>(point, path[p0], path[p1]); + if (d < dist) + { + dist = d; + ret = true; + + pathIndex = i; + pointIndex0 = p0; + pointIndex1 = p1; + distance = d; + } + } + } + + return ret; +} + +#endif //ENABLE_SPRITES diff --git a/Runtime/Graphics/Polygon2D.h b/Runtime/Graphics/Polygon2D.h new file mode 100644 index 0000000..76af1cf --- /dev/null +++ b/Runtime/Graphics/Polygon2D.h @@ -0,0 +1,66 @@ +#pragma once +#include "Configuration/UnityConfigure.h" + +#if ENABLE_SPRITES + +#include "Runtime/Math/Vector2.h" +#include "Runtime/Utilities/dynamic_array.h" +#include "Runtime/Serialize/SerializeUtility.h" + +class Sprite; + +class Polygon2D +{ +public: + DECLARE_SERIALIZE_NO_PPTR (Polygon2D) + + typedef dynamic_array<Vector2f> TPath; + typedef std::vector<TPath> TPaths; + + Polygon2D(); + + bool IsEmpty() const { return (m_Paths.size() == 0 || m_Paths[0].size() < 3); } + void Reset(); + void Clear() { m_Paths.clear(); } + + // Simple API + void SetPoints (const Vector2f* points, size_t count); + const Vector2f* GetPoints() const { return (m_Paths.size() > 0) ? m_Paths[0].data() : NULL; } + size_t GetPointCount() const { return (m_Paths.size() > 0) ? m_Paths[0].size() : 0; } + + // Advanced API + void SetPathCount(int pathCount); + int GetPathCount() const { return m_Paths.size(); } + + void SetPath(int index, const TPath& path); + const TPath& GetPath(int index) const { return m_Paths[index]; } + TPath& GetPath(int index) { return m_Paths[index]; } + + void CopyFrom (const Polygon2D& paths); + + size_t GetTotalPointCount() const + { + size_t count = 0; + for (size_t i = 0; i < m_Paths.size(); ++i) + count += m_Paths[i].size(); + return count; + } + + // Generation + void GenerateFrom(Sprite* sprite, const Vector2f& offset, float detail, unsigned char alphaTolerance, bool holeDetection); + + // Math + bool GetNearestPoint(const Vector2f& point, int& pathIndex, int& pointIndex, float& distance) const; + bool GetNearestEdge(const Vector2f& point, int& pathIndex, int& pointIndex0, int& pointIndex1, float& distance, bool loop) const; + +private: + TPaths m_Paths; +}; + +template<class TransferFunction> +void Polygon2D::Transfer(TransferFunction& transfer) +{ + TRANSFER(m_Paths); +} + +#endif //ENABLE_SPRITES diff --git a/Runtime/Graphics/ProceduralCache.cpp b/Runtime/Graphics/ProceduralCache.cpp new file mode 100644 index 0000000..9ae3176 --- /dev/null +++ b/Runtime/Graphics/ProceduralCache.cpp @@ -0,0 +1,154 @@ +#include "UnityPrefix.h" +#include "ProceduralMaterial.h" +#include "SubstanceSystem.h" +#include "Runtime/Misc/CachingManager.h" +#include "Runtime/Utilities/FileUtilities.h" +#include "Runtime/Graphics/Image.h" + +#if ENABLE_SUBSTANCE && ENABLE_CACHING && !UNITY_EDITOR +bool ProceduralMaterial::PreProcess(std::set<unsigned int>& cachedTextureIDs) +{ + if (m_LoadingBehavior!=ProceduralLoadingBehavior_Cache) + return false; + + std::string cacheFolder = GetCacheFolder(); + std::vector<string> fileNames; + CachingManager::ReadInfoFile(cacheFolder, NULL, &fileNames); + if (fileNames.size()==0) + return false; + + bool success = true; + std::map<ProceduralTexture*, SubstanceTexture> cachedTextures; + for (PingedTextures::iterator it=m_PingedTextures.begin();it!=m_PingedTextures.end() ; ++it) + { + if (*it!=NULL) + { + std::string fileName; + if (ReadCachedTexture(fileName, cachedTextures, cacheFolder, **it)) + { + cachedTextureIDs.insert((*it)->GetSubstanceBaseTextureUID()); + } + else + { + success = false; + DeleteFileOrDirectory(fileName); + std::vector<string>::iterator it = std::find(fileNames.begin(), fileNames.end(), fileName); + if (it!=fileNames.end()) + fileNames.erase(it); + } + } + } + GetSubstanceSystem().ForceSubstanceResults(cachedTextures); + + if (fileNames.size()==0) + { + DeleteFileOrDirectory(cacheFolder); + return false; + } + + time_t timestamp = CachingManager::GenerateTimeStamp(); + CachingManager::WriteInfoFile(cacheFolder, fileNames, timestamp); + GetCachingManager().GetCurrentCache().UpdateTimestamp(cacheFolder, timestamp); + + if (success) + m_LoadingBehavior = ProceduralLoadingBehavior_Generate; + return success; +} + +void ProceduralMaterial::PostProcess(const std::map<ProceduralTexture*, SubstanceTexture>& textures, const std::set<unsigned int>& cachedTextureIDs) +{ + if (m_LoadingBehavior!=ProceduralLoadingBehavior_Cache) + return; + m_LoadingBehavior = ProceduralLoadingBehavior_Generate; + + std::string cacheFolder = GetCacheFolder(); + std::vector<string> fileNames; + CachingManager::ReadInfoFile(cacheFolder, NULL, &fileNames); + for (std::map<ProceduralTexture*, SubstanceTexture>::const_iterator it=textures.begin() ; it!=textures.end() ; ++it) + { + if (it->first!=NULL && cachedTextureIDs.find(it->first->GetSubstanceBaseTextureUID())==cachedTextureIDs.end()) + { + std::string fileName; + bool result = WriteCachedTexture(fileName, cacheFolder, *it->first, it->second); + std::vector<string>::iterator it = std::find(fileNames.begin(), fileNames.end(), fileName); + if (result) + { + if (it==fileNames.end()) + fileNames.push_back(fileName); + } + else + { + DeleteFileOrDirectory(fileName); + if (it!=fileNames.end()) + fileNames.erase(it); + } + } + } + if (fileNames.size()==0) + { + DeleteFileOrDirectory(cacheFolder); + } + else + { + GetCachingManager().WriteInfoFile(cacheFolder, fileNames); + } +} + +std::string ProceduralMaterial::GetCacheFolder() const +{ + std::string hash; + char hashValue[6]; + for (int i=0 ; i<16 ; ++i) + { + snprintf(hashValue, 6, "%d", (int)m_Hash.hashData.bytes[i]); + hash += hashValue; + } + return GetCachingManager().GetCurrentCache().GetFolder(hash.c_str(), true); +} + +std::string ProceduralMaterial::GetCacheFilename(const ProceduralTexture& texture) const +{ + char filename[24]; + snprintf(filename, 24, "%u.cache", texture.GetSubstanceBaseTextureUID()); + return filename; +} + +bool ProceduralMaterial::ReadCachedTexture(string& fileName, std::map<ProceduralTexture*, SubstanceTexture>& cachedTextures, const std::string& folder, const ProceduralTexture& texture) +{ + fileName = folder+"/"+GetCacheFilename(texture); + File file; + if (!file.Open(fileName, File::kReadPermission, File::kSilentReturnOnOpenFail)) + return false; + unsigned int infoSize = sizeof(SubstanceTexture); + unsigned int textureSize; + SubstanceTexture& data = cachedTextures[(ProceduralTexture*)&texture]; + bool result = file.Read(&data, infoSize) + && file.Read(&textureSize, 4) + && (data.buffer=SubstanceSystem::OnMalloc(textureSize))!=NULL + && file.Read(data.buffer, textureSize); + file.Close(); + return result; +} + +bool ProceduralMaterial::WriteCachedTexture(string& fileName, const std::string& folder, const ProceduralTexture& texture, const SubstanceTexture& data) +{ + fileName = folder+"/"+GetCacheFilename(texture); + if (IsFileCreated(fileName) && texture.HasBeenUploaded()) + return true; + unsigned int infoSize = sizeof(SubstanceTexture); + int mipLevels = data.mipmapCount; + if (mipLevels==0) + mipLevels = CalculateMipMapCount3D(data.level0Width, data.level0Height, 1); + unsigned int textureSize = CalculateMipMapOffset(data.level0Width, data.level0Height, texture.GetSubstanceFormat(), mipLevels+1); + if (!GetCachingManager().GetCurrentCache().FreeSpace(infoSize+4+textureSize)) + return false; + File file; + if (!file.Open(fileName, File::kWritePermission, File::kSilentReturnOnOpenFail)) + return false; + bool result = file.Write(&data, infoSize) + && file.Write(&textureSize, 4) + && file.Write(data.buffer, textureSize); + file.Close(); + return result; +} +#endif diff --git a/Runtime/Graphics/ProceduralLinker.cpp b/Runtime/Graphics/ProceduralLinker.cpp new file mode 100644 index 0000000..402756d --- /dev/null +++ b/Runtime/Graphics/ProceduralLinker.cpp @@ -0,0 +1,521 @@ +#include "UnityPrefix.h" +#include "ProceduralMaterial.h" +#include "SubstanceArchive.h" +#include "SubstanceSystem.h" + +#if ENABLE_SUBSTANCE +bool ProceduralShuffleOutputs (SubstanceLinkerHandle* linkerHandle, ProceduralMaterial::PingedTextures& textures, SubstanceInputs& inputs) +{ + for (ProceduralMaterial::PingedTextures::iterator it=textures.begin();it!=textures.end();++it) + { + unsigned int rI = 0, gI = 1, bI = 2, aI = 3; + ProceduralTexture* texture = *it; + ProceduralTexture* alpha = NULL; + SubstanceLinkerShuffle shuffle; + unsigned int baseOutputUid; + unsigned int shuffledOutputUid; + + texture->SetSubstancePreShuffleUID(texture->GetSubstanceTextureUID()); + texture->EnableFlag(ProceduralTexture::Flag_Binded, false); + + if (texture->GetSubstanceFormat()==kTexFormatARGB32) + { + rI = 1; + gI = 2; + bI = 3; + aI = 0; + } + + baseOutputUid = texture->GetSubstanceTextureUID(); + + // RGB normal map + if (texture->GetUsageMode() == kTexUsageNormalmapPlain) + { + shuffle.useLevels = 1; + shuffle.channels[rI].outputUID = baseOutputUid; + shuffle.channels[rI].channelIndex = 0; + shuffle.channels[rI].levelMin = 0.0f; + shuffle.channels[rI].levelMax = 1.0f; + shuffle.channels[gI].outputUID = baseOutputUid; + shuffle.channels[gI].channelIndex = 1; + shuffle.channels[gI].levelMin = 1.0f; + shuffle.channels[gI].levelMax = 0.0f; + shuffle.channels[bI].outputUID = baseOutputUid; + shuffle.channels[bI].channelIndex = 2; + shuffle.channels[bI].levelMin = 0.0f; + shuffle.channels[bI].levelMax = 1.0f; + shuffle.channels[aI].outputUID = baseOutputUid; + shuffle.channels[aI].channelIndex = 3; + shuffle.channels[aI].levelMin = 0.0f; + shuffle.channels[aI].levelMax = 1.0f; + } + // platform uses optimized normal map (Z reconstruction) + else if (texture->GetUsageMode() == kTexUsageNormalmapDXT5nm) + { + shuffle.useLevels = 1; + shuffle.channels[rI].outputUID = baseOutputUid; + shuffle.channels[rI].channelIndex = 1; + shuffle.channels[rI].levelMin = 1.0f; + shuffle.channels[rI].levelMax = 0.0f; + shuffle.channels[gI].outputUID = baseOutputUid; + shuffle.channels[gI].channelIndex = 1; + shuffle.channels[gI].levelMin = 1.0f; + shuffle.channels[gI].levelMax = 0.0f; + shuffle.channels[bI].outputUID = baseOutputUid; + shuffle.channels[bI].channelIndex = 1; + shuffle.channels[bI].levelMin = 1.0f; + shuffle.channels[bI].levelMax = 0.0f; + shuffle.channels[aI].outputUID = baseOutputUid; + shuffle.channels[aI].channelIndex = 0; + shuffle.channels[aI].levelMin = 0.0f; + shuffle.channels[aI].levelMax = 1.0f; + } + else + { + shuffle.useLevels = 0; + shuffle.channels[rI].outputUID = baseOutputUid; + shuffle.channels[rI].channelIndex = 0; + shuffle.channels[gI].outputUID = baseOutputUid; + shuffle.channels[gI].channelIndex = 1; + shuffle.channels[bI].outputUID = baseOutputUid; + shuffle.channels[bI].channelIndex = 2; + shuffle.channels[aI].outputUID = baseOutputUid; + shuffle.channels[aI].channelIndex = texture->GetType()==Substance_OType_Specular?0:3; + + if (texture->GetAlphaSource()!=Substance_OType_Unknown) + { + for (ProceduralMaterial::PingedTextures::iterator i=textures.begin();i!=textures.end();++i) + { + ProceduralTexture* alphaTexture = *i; + if (alphaTexture->GetType() == texture->GetAlphaSource()) + { + alpha = alphaTexture; + shuffle.channels[aI].outputUID = alphaTexture->GetSubstanceTextureUID(); + shuffle.channels[aI].channelIndex = 0; + break; + } + } + } + } + + unsigned int substance_format; + switch( texture->GetSubstanceFormat() ) + { + case kTexFormatDXT5: + substance_format = Substance_PF_DXT5; + break; + case kTexFormatETC_RGB4: + substance_format = Substance_PF_ETC1; + break; + case kTexFormatPVRTC_RGBA4: + substance_format = Substance_PF_PVRTC4; + break; + default: + substance_format = Substance_PF_RGBA; + } + if (substanceLinkerHandleCreateOutput(linkerHandle, &shuffledOutputUid, + substance_format, 0, Substance_Linker_Flip_Vertical, &shuffle)!=0) + { + return false; + } + + texture->SetSubstanceShuffledUID(shuffledOutputUid); + + for (SubstanceInputs::iterator i=inputs.begin();i!=inputs.end();++i) + { + // look if main texture is used + { + std::set<unsigned int>::iterator ai = std::find( + i->alteredTexturesUID.begin(), i->alteredTexturesUID.end(), texture->GetSubstanceBaseTextureUID()); + if (ai!=i->alteredTexturesUID.end()) + { + texture->EnableFlag(ProceduralTexture::Flag_Binded, true); + } + } + + // look if alpha texture is used + if (alpha!=NULL) + { + std::set<unsigned int>::iterator ai = std::find( + i->alteredTexturesUID.begin(), i->alteredTexturesUID.end(), alpha->GetSubstanceBaseTextureUID()); + if (ai!=i->alteredTexturesUID.end()) + { + i->alteredTexturesUID.insert(texture->GetSubstanceBaseTextureUID()); + texture->EnableFlag(ProceduralTexture::Flag_Binded, true); + } + } + } + } + return true; +} + +ProceduralMaterial* ProceduralMaterial::m_PackedSubstance = NULL; + +void SUBSTANCE_CALLBACK ProceduralHandleUIDConflict(SubstanceLinkerHandle* handle, SubstanceLinkerUIDCollisionType conflict, unsigned int previousUID, unsigned int newUID) +{ + SubstanceInputs& inputs = ProceduralMaterial::m_PackedSubstance->GetSubstanceInputs(); + ProceduralMaterial::PingedTextures& textures = ProceduralMaterial::m_PackedSubstance->GetPingedTextures(); + if (conflict==Substance_Linker_UIDCollision_Input) + { + for (SubstanceInputs::iterator it=inputs.begin() ; it!=inputs.end() ; ++it) + { + if (previousUID==it->internalIdentifier) + { + it->shuffledIdentifier = newUID; + break; + } + } + } + else if (conflict==Substance_Linker_UIDCollision_Output) + { + for (ProceduralMaterial::PingedTextures::iterator it=textures.begin() ; it!=textures.end() ; ++it) + { + ProceduralTexture* texture = *it; + if (texture!=NULL && previousUID==texture->GetSubstanceBaseTextureUID()) + { + texture->SetSubstanceShuffledUID(newUID); + break; + } + } + } +} + +void ProceduralMaterial::PackSubstances(std::vector<ProceduralMaterial*>& materials) +{ + if (materials.size()==0) + return; + + // Create linker context + SubstanceEngineIDEnum currentEngine(GetSubstanceEngineID()); + SubstanceLinkerContext* linkerContext; + SubstanceLinkerHandle* linkerHandle; + if (substanceLinkerContextInit(&linkerContext, SUBSTANCE_LINKER_API_VERSION, currentEngine)!=0 + || substanceLinkerContextSetCallback(linkerContext, Substance_Linker_Callback_UIDCollision, (void*)&ProceduralHandleUIDConflict)!=0 + || substanceLinkerHandleInit(&linkerHandle, linkerContext)!=0) + { + ErrorString("Failed to initialize substance linker"); + for (std::vector<ProceduralMaterial*>::iterator it=materials.begin();it!=materials.end();++it) + (*it)->EnableFlag(Flag_Broken); + return; + } + + // Make the list of materials to pack + std::vector<ProceduralMaterial*> packedMaterials; + packedMaterials.reserve(materials.size()); + + // Try to push all substances assembly + for (std::vector<ProceduralMaterial*>::iterator it=materials.begin();it!=materials.end();++it) + { + if ((*it)->IsFlagEnabled(Flag_Broken)) + continue; + + SubstanceArchive* package = (*it)->m_PingedPackage; + Assert(package!=NULL); + + unsigned size = package->GetBufferSize(); + UInt8* bufferData = package->GetBufferData(); + + m_PackedSubstance = *it; + + // Prepare texture UIDs + { + for (ProceduralMaterial::PingedTextures::iterator i=m_PackedSubstance->m_PingedTextures.begin();i!=m_PackedSubstance->m_PingedTextures.end();++i) + (*i)->SetSubstanceShuffledUID((*i)->GetSubstanceBaseTextureUID()); + } + + // Prepare input UIDs + { + // Check if it's the old format + if (m_PackedSubstance->m_Inputs.size()>0 + && m_PackedSubstance->m_Inputs[0].internalIdentifier==0) + { + SubstanceLinkerContext *context; + SubstanceLinkerHandle* linkHandle; + size_t substanceDataSize = 0; + UInt8* buffer = NULL; + if (substanceLinkerContextInit(&context, SUBSTANCE_LINKER_API_VERSION, currentEngine)==0 + && substanceLinkerHandleInit(&linkHandle, context)==0 + && substanceLinkerHandlePushAssemblyMemory(linkHandle, (const char*)bufferData, size)==0 + && substanceLinkerHandleLink(linkHandle, (const unsigned char**)&buffer, &substanceDataSize)==0) + { + SubstanceHandle* substanceHandle = NULL; + if (substanceHandleInit( &substanceHandle, GetSubstanceSystem().GetContext(), buffer, substanceDataSize, NULL )==0) + { + for (SubstanceInputs::iterator i=m_PackedSubstance->m_Inputs.begin();i!=m_PackedSubstance->m_Inputs.end();++i) + { + SubstanceInputDesc inputDescription; + if (substanceHandleGetInputDesc(substanceHandle, i->internalIndex, &inputDescription)==0) + i->internalIdentifier = inputDescription.inputId; + } + substanceHandleRelease(substanceHandle); + } + + substanceLinkerHandleRelease(linkHandle); + substanceLinkerContextRelease(context); + } + else + { + ErrorStringObject("Failed to initialize substance linker", m_PackedSubstance); + m_PackedSubstance->EnableFlag(Flag_Broken); + continue; + } + } + + // Prepare shuffled ID + for (SubstanceInputs::iterator i=m_PackedSubstance->m_Inputs.begin();i!=m_PackedSubstance->m_Inputs.end();++i) + { + i->shuffledIdentifier = i->internalIdentifier; + } + } + + // Push assembly if it has not already been pushed + // If we're creating another instance of a graph that has already been pushed, then we still need to push the SBSASM, + // otherwise we'll just get copies of the previous instance's outputs. + if ((!package->m_isPushed) || (package->m_generatedGraphs.count(m_PackedSubstance->m_PrototypeName) == 1)) + { + if (substanceLinkerHandlePushAssemblyMemory(linkerHandle, (const char*)bufferData, size)!=0) + { + ErrorStringObject("Failed to pack substance (substanceLinkerHandlePushAssemblyMemory)", *it); + m_PackedSubstance->EnableFlag(Flag_Broken); + continue; + } + package->m_isPushed = true; + } + package->m_generatedGraphs.insert(m_PackedSubstance->m_PrototypeName); + + // Set output formats & create shuffled outputs if needed + if (!ProceduralShuffleOutputs(linkerHandle, (*it)->m_PingedTextures, (*it)->m_Inputs)) + { + ErrorStringObject("Failed to pack substance (ShuffleOutputs)", *it); + m_PackedSubstance->EnableFlag(Flag_Broken); + continue; + } + + // Set inputs constness + #if !UNITY_EDITOR + if ((*it)->IsFlagEnabled(Flag_ConstSize)) + { + SubstanceInputs& inputs = (*it)->m_Inputs; + bool broken=false; + for (SubstanceInputs::iterator i=inputs.begin();i!=inputs.end();++i) + { + if (i->name=="$outputsize" || i->name=="$randomseed") + { + SubstanceLinkerInputValue value; + value.integer[0] = (int)i->value.scalar[0]; + value.integer[1] = (int)i->value.scalar[1]; + value.integer[2] = (int)i->value.scalar[2]; + value.integer[3] = (int)i->value.scalar[3]; + if (substanceLinkerConstifyInput(linkerHandle, i->shuffledIdentifier, &value)!=0) + break; + } + } + if (broken) + { + ErrorStringObject("Failed to pack substance (substanceLinkerConstifyInput)", *it); + m_PackedSubstance->EnableFlag(Flag_Broken); + continue; + } + } + #endif + + packedMaterials.push_back(*it); + } + + // Select all used outputs + if (packedMaterials.size()>0 && substanceLinkerHandleSelectOutputs(linkerHandle, Substance_Linker_Select_UnselectAll, 0)!=0) + { + ErrorString("Failed to pack substances (substanceLinkerHandleSelectOutputs)"); + for (int i=packedMaterials.size()-1;i>=0;--i) + packedMaterials[i]->EnableFlag(Flag_Broken); + return; + } + for (int i=packedMaterials.size()-1;i>=0;--i) + { + ProceduralMaterial* material = packedMaterials[i]; + for (ProceduralMaterial::PingedTextures::iterator it=material->m_PingedTextures.begin();it!=material->m_PingedTextures.end();++it) + { + ProceduralTexture* texture = *it; + if (substanceLinkerHandleSelectOutputs(linkerHandle, Substance_Linker_Select_Select, texture->GetSubstancePreShuffleUID())!=0) + { + ErrorStringObject("Failed to pack substance (Substance_Linker_Select_Select)", material); + material->EnableFlag(Flag_Broken); + packedMaterials.erase(packedMaterials.begin()+i); + break; + } + } + } + + // Nothing to pack ? + if (packedMaterials.size()==0) + { + substanceLinkerHandleRelease(linkerHandle); + substanceLinkerContextRelease(linkerContext); + return; + } + + // Initialize substance handle + SubstanceData* pack = (SubstanceData*) UNITY_MALLOC_ALIGNED_NULL(kMemSubstance, sizeof(SubstanceData), 32); + if (!pack) + { + ErrorString("Could not allocate memory for Substance data (PackSubstances)"); + for (int i=packedMaterials.size()-1;i>=0;--i) + packedMaterials[i]->EnableFlag(Flag_Broken); + return; + } + + pack->substanceHandle = NULL; + pack->instanceCount = 0; + pack->memorySleepBudget = ProceduralCacheSize_None; + pack->memoryWorkBudget = ProceduralCacheSize_Tiny; + + + // Do we need to link the SBSASM? + // Only case where we DON'T need to link is when the three following conditions are met: + // - we are only linking a single substance + // - we are linking a clone + // - we have linked the same standalone substance clone once and cached the SBSBIN data + // If one of the above conditions is not met, we need to link. + size_t substanceDataSize = 0; + UInt8* buffer = NULL; + const ProceduralMaterial* currentMat = packedMaterials[0]; + const UnityStr& prototypeName = currentMat->m_PrototypeName; + if (!((packedMaterials.size() == 1) + && (currentMat->IsFlagEnabled(Flag_Clone)) + && (currentMat->m_PingedPackage->IsCloneDataAvailable(prototypeName)))) + { + if (substanceLinkerHandleLink(linkerHandle, (const unsigned char**)&buffer, &substanceDataSize )!=0) + { + ErrorStringObject("Failed to pack substances (substanceLinkerHandleLink)", currentMat); + for (int i=packedMaterials.size()-1;i>=0;--i) + packedMaterials[i]->EnableFlag(Flag_Broken); + return; + } + } + + // Handle the "cloning of a single substance" situation + if ((packedMaterials.size() == 1) && (currentMat->IsFlagEnabled(Flag_Clone))) + { + // In that case, we link and cache the SBSBIN inside the SubstancePackage. + // When the same substance is cloned again, this vanilla SBSBIN data is reused to avoid reallocating memory + SubstanceArchive* package = currentMat->m_PingedPackage; + if (package->IsCloneDataAvailable(prototypeName)) + { + pack->substanceData = package->GetLinkedBinaryData(prototypeName); + } + else + { + if (package->SaveLinkedBinaryData(prototypeName, (const UInt8*) buffer, (const int) substanceDataSize)) + { + pack->substanceData = package->GetLinkedBinaryData(prototypeName); + } + else + { + WarningStringMsg("Failed to save SBSBIN data for material %s", currentMat->GetName()); + pack->substanceData = (UInt8*) UNITY_MALLOC_ALIGNED_NULL(kMemSubstance, substanceDataSize, 32); + if (pack->substanceData) + { + memcpy(pack->substanceData, buffer, substanceDataSize); + } + else + { + ErrorString("Could not allocate memory for Substance linked data"); + for (int i=packedMaterials.size()-1;i>=0;--i) + packedMaterials[i]->EnableFlag(Flag_Broken); + return; + } + } + } + } + else + { + // Either we're packing more than one substance, or this is not a clone, in + // which case the SBSBIN is not reusable because of constified inputs. + pack->substanceData = (UInt8*) UNITY_MALLOC_ALIGNED_NULL(kMemSubstance, substanceDataSize, 32); + if (pack->substanceData) + { + memcpy(pack->substanceData, buffer, substanceDataSize); + } + else + { + ErrorString("Could not allocate memory for Substance linked data"); + for (int i=packedMaterials.size()-1;i>=0;--i) + packedMaterials[i]->EnableFlag(Flag_Broken); + return; + } + } + + // Reset the isPushed flags and the list of generated graphs of the packages that have been pushed and linked + for (std::vector<ProceduralMaterial*>::iterator it=materials.begin();it!=materials.end();++it) + { + SubstanceArchive *package = (*it)->m_PingedPackage; + package->m_isPushed = false; + package->m_generatedGraphs.clear(); + } + + // Release linker + substanceLinkerHandleRelease(linkerHandle); + substanceLinkerContextRelease(linkerContext); + + // Create substance handle + if (substanceHandleInit(&pack->substanceHandle, GetSubstanceSystem().GetContext(), pack->substanceData, substanceDataSize, NULL)!=0) + { + ErrorStringObject("Failed to pack substances (substanceHandleInit)", packedMaterials[0]); + for (int i=packedMaterials.size()-1;i>=0;--i) + packedMaterials[i]->EnableFlag(Flag_Broken); + return; + } + + // Bind all substances + for (std::vector<ProceduralMaterial*>::iterator it=packedMaterials.begin();it!=packedMaterials.end();++it) + { + bool broken(false); + + // Update input identifiers (todo: improve this if doable) + for (SubstanceInputs::iterator i=(*it)->m_Inputs.begin();i!=(*it)->m_Inputs.end();++i) + { + i->internalIndex = -1; + int id=0; + SubstanceInputDesc inputDescription; + while (substanceHandleGetInputDesc(pack->substanceHandle, id, &inputDescription)==0) + { + if (i->shuffledIdentifier==inputDescription.inputId) + { + i->internalIndex = id; + break; + } + ++id; + } + if (i->internalIndex==-1) + { +#if !UNITY_EDITOR + // Don't break since the input may be removed + if ((*it)->IsFlagEnabled(Flag_ConstSize) && (i->name=="$outputsize" || i->name=="$randomseed")) + { + continue; + } +#endif + broken = true; + } + } + + if (broken) + { + ErrorStringObject("Failed to pack substances (SubstanceInputDesc)", *it); + (*it)->EnableFlag(Flag_Broken); + continue; + } + + (*it)->m_SubstanceData = pack; + ++pack->instanceCount; + } + + if (pack->instanceCount==0) + { + substanceHandleRelease(pack->substanceHandle); + UNITY_FREE(kMemSubstance, pack->substanceData); + UNITY_FREE(kMemSubstance, pack); + } +} + +#endif // ENABLE_SUBSTANCE diff --git a/Runtime/Graphics/ProceduralMaterial.cpp b/Runtime/Graphics/ProceduralMaterial.cpp new file mode 100644 index 0000000..69b616a --- /dev/null +++ b/Runtime/Graphics/ProceduralMaterial.cpp @@ -0,0 +1,1339 @@ +#include "UnityPrefix.h" +#include "ProceduralMaterial.h" +#include "SubstanceArchive.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "Runtime/GfxDevice/GfxDevice.h" +#include "Runtime/Threads/Thread.h" +#include "Runtime/Input/TimeManager.h" +#include "SubstanceSystem.h" +#include "Runtime/Graphics/Image.h" + +#if ENABLE_SUBSTANCE +#include "Runtime/BaseClasses/IsPlaying.h" +#endif + +#if UNITY_EDITOR +#include "Editor/Src/AssetPipeline/SubstanceImporter.h" +#include "Editor/Src/EditorUserBuildSettings.h" +#endif + +size_t GetProceduralMemoryBudget(ProceduralCacheSize budget) +{ + switch(budget) + { + case ProceduralCacheSize_NoLimit: + return 0; + case ProceduralCacheSize_Heavy: + return 512 * 1024 * 1024; + case ProceduralCacheSize_Medium: + return 256 * 1024 * 1024; + case ProceduralCacheSize_Tiny: + return 128 * 1024 * 1024; + case ProceduralCacheSize_None: + default: + return 1; + } +} + +Mutex ProceduralMaterial::m_InputMutex; + +IMPLEMENT_CLASS( ProceduralMaterial ) +IMPLEMENT_OBJECT_SERIALIZE( ProceduralMaterial ) + +ProceduralMaterial::ProceduralMaterial( MemLabelId label, ObjectCreationMode mode ) : + Super( label, mode ), + m_SubstanceData( NULL ), + m_Flags( 0 ), + m_LoadingBehavior( ProceduralLoadingBehavior_Generate ), + m_Width( 9 ), + m_Height( 9 ), +#if UNITY_EDITOR + m_isAlreadyLoadedInCurrentScene( false ), +#endif + m_AnimationUpdateRate( 42 ), // 24fps + m_AnimationTime( 0.0f ), + m_PingedPackage( NULL ), + m_PrototypeName( "" ) +{ +#if ENABLE_SUBSTANCE + integrationTimeStamp = GetSubstanceSystem().integrationTimeStamp; +#endif +} + +ProceduralMaterial::~ProceduralMaterial() +{ +#if ENABLE_SUBSTANCE + + /////@TODO: This is an incredible hack. Waiting for another process to complete in the destructor is a big no no, especially when it involves a Thread::Sleep ()... + UnlockObjectCreation(); + GetSubstanceSystem().NotifySubstanceDestruction(this); + LockObjectCreation(); + + Clean(); + + // Delete texture inputs (TODO: try to remove std::vector<Image*>) + for (std::vector<TextureInput>::iterator it=m_TextureInputs.begin();it!=m_TextureInputs.end();++it) + { + delete it->inputParameters; + delete it->image; + + if (it->buffer) + { + UNITY_FREE(kMemSubstance, it->buffer); + } + } +#endif +} + +void ProceduralMaterial::Clean() +{ +#if ENABLE_SUBSTANCE + if (m_SubstanceData!=NULL) + { + if (m_SubstanceData->instanceCount==1) + { + substanceHandleRelease(m_SubstanceData->substanceHandle); + // Clones do not delete their SBSBIN data, this is done in the destructor of the SubstanceArchive + // the clone was created from. + if (!IsFlagEnabled(Flag_Clone)) + { + UNITY_FREE( kMemSubstance, m_SubstanceData->substanceData ); + } + UNITY_FREE( kMemSubstance, m_SubstanceData ); + } + else + { + --m_SubstanceData->instanceCount; + } + } + m_SubstanceData = NULL; + // Awake inputs + for (SubstanceInputs::iterator i=m_Inputs.begin();i != m_Inputs.end();i++) + { + i->EnableFlag(SubstanceInput::Flag_Awake, true); + } +#endif +} + +ProceduralMaterial* ProceduralMaterial::Clone() +{ + ProceduralMaterial* clone = CreateObjectFromCode<ProceduralMaterial>(); + clone->m_SubstancePackage = m_SubstancePackage; + clone->m_PrototypeName = m_PrototypeName; + clone->m_Width = m_Width; + clone->m_Height = m_Height; + clone->m_Textures = m_Textures; + // Rename cloned textures + for (int idx=0 ; idx<m_Textures.size() ; ++idx) + { + clone->m_Textures[idx]->SetName(m_Textures[idx]->GetName()); + } + clone->m_AnimationUpdateRate = m_AnimationUpdateRate; + clone->m_Inputs = m_Inputs; + clone->m_Flags = ((m_Flags | Flag_Clone | Flag_AwakeClone) & (~Flag_ConstSize)); + clone->m_LoadingBehavior = m_LoadingBehavior; + clone->AwakeDependencies(false); + return clone; +} + +void ProceduralMaterial::RebuildClone() +{ + EnableFlag(Flag_AwakeClone, false); + + // Clone textures +#if ENABLE_SUBSTANCE + if (!IsWorldPlaying() || m_LoadingBehavior!=ProceduralLoadingBehavior_BakeAndDiscard) + { + for (Textures::iterator it=m_Textures.begin();it!=m_Textures.end();++it) + { + *it = (*it)->Clone(this); + } + + // Awake inputs + for (SubstanceInputs::iterator i=m_Inputs.begin();i != m_Inputs.end();i++) + { + i->EnableFlag(SubstanceInput::Flag_Awake, true); + } + + AwakeDependencies(false); + + // Force generation if required + if (m_LoadingBehavior==ProceduralLoadingBehavior_None) + EnableFlag(ProceduralMaterial::Flag_ForceGenerate); + + GetSubstanceSystem().QueueLoading(this); + } +#endif +} + +#if UNITY_EDITOR +void ProceduralMaterial::Init( SubstanceArchive& substancePackage, const UnityStr& prototypeName, const SubstanceInputs& inputs, const Textures& textures ) +{ + Assert(m_SubstancePackage.GetInstanceID () == 0); + + m_SubstancePackage = &substancePackage; + m_PrototypeName = prototypeName; + m_Inputs = inputs; + m_Textures = textures; + + EnableFlag(Flag_ConstSize); + + // Update Flag_Animated + EnableFlag(Flag_Animated, HasSubstanceProperty("$time")); +} + +const char* ProceduralMaterial::GetSubstancePackageName() +{ + if (m_PingedPackage) + { + return m_PingedPackage->GetName(); + } + else + { + return m_SubstancePackage->GetName(); + } +} + +#endif + +SubstanceHandle* ProceduralMaterial::GetSubstanceHandle() +{ + if (m_SubstanceData!=NULL) + return m_SubstanceData->substanceHandle; + return NULL; +} + +void ProceduralMaterial::SetSize(int width, int height) +{ + m_Width = width; + m_Height = height; + + Mutex::AutoLock locker(m_InputMutex); + SubstanceInput* input = FindSubstanceInput("$outputsize"); + if (input!=NULL) + { + input->value.scalar[0] = (float)m_Width; + input->value.scalar[1] = (float)m_Height; + } +} + +void ProceduralMaterial::AwakeFromLoadThreaded() +{ + Super::AwakeFromLoadThreaded(); + AwakeDependencies(true); +#if ENABLE_SUBSTANCE +#if UNITY_EDITOR + EnableFlag(Flag_Awake); +#endif + // Neither Baked (keep or discard) nor DoNothing substances must be generated at this time + ProceduralLoadingBehavior behavior = GetLoadingBehavior(); + if (behavior != ProceduralLoadingBehavior_BakeAndKeep && + behavior != ProceduralLoadingBehavior_BakeAndDiscard && + behavior != ProceduralLoadingBehavior_None) + { + GetSubstanceSystem().QueueLoading(this); + } +#endif +} + +void ProceduralMaterial::AwakeFromLoad( AwakeFromLoadMode awakeMode ) +{ + Super::AwakeFromLoad( awakeMode ); + + if ((awakeMode & kDidLoadThreaded)==0) + { + AwakeDependencies(false); + +#if ENABLE_SUBSTANCE + +#if UNITY_EDITOR + EnableFlag(Flag_Awake); + if (awakeMode!=kInstantiateOrCreateFromCodeAwakeFromLoad + && SubstanceImporter::OnLoadSubstance(*this) + && !(IsWorldPlaying() && m_LoadingBehavior==ProceduralLoadingBehavior_None)) +#else + // Don't link and render the DoNothing substances when loading them + // This is done when calling RebuildTextures instead + if (m_LoadingBehavior!=ProceduralLoadingBehavior_None) +#endif + { + GetSubstanceSystem().QueueSubstance(this); + } + +#endif + } + +#if ENABLE_SUBSTANCE + GetSubstanceSystem().NotifySubstanceCreation(this); +#endif +} + +#if ENABLE_SUBSTANCE +void ProceduralMaterial::ApplyInputs (bool& it_has_changed, bool asHint, std::set<unsigned int>& modifiedOutputsUID) +{ + int textureInputIndex(0); + + // Handle input alteration + for (SubstanceInputs::iterator i=m_Inputs.begin();i != m_Inputs.end();i++) + { + SubstanceInput& input = *i; + + // Skip const inputs at runtime +#if !UNITY_EDITOR + if (IsFlagEnabled(Flag_ConstSize) + && (input.name=="$outputsize" || input.name=="$randomseed")) + { + continue; + } +#endif + + // Check texture instance hasn't changed, in the editor in case of texture changes +#if UNITY_EDITOR + if (!input.IsFlagEnabled(SubstanceInput::Flag_Modified) && (input.internalType==Substance_IType_Image) + && textureInputIndex<m_TextureInputs.size()) + { + Texture2D* checkTexture = dynamic_pptr_cast<Texture2D*>( + InstanceIDToObjectThreadSafe(input.value.texture.GetInstanceID())); + if (m_TextureInputs[textureInputIndex].texture!=checkTexture) + m_TextureInputs[textureInputIndex].texture = checkTexture; + } +#endif + + if (asHint) + { + // Push hint if needed + if (input.IsFlagEnabled(SubstanceInput::Flag_Modified) || input.IsFlagEnabled(SubstanceInput::Flag_Cached)) + { + if (!input.IsFlagEnabled(SubstanceInput::Flag_SkipHint)) + { + if (substanceHandlePushSetInput( m_SubstanceData->substanceHandle, Substance_PushOpt_HintOnly, input.internalIndex, input.internalType, 0, 0 )!=0) + ErrorStringObject("Failed to apply substance input as hint", this); + } + it_has_changed = true; + input.EnableFlag(SubstanceInput::Flag_Modified, false); + } + + if (input.IsFlagEnabled(SubstanceInput::Flag_Awake)) + { + it_has_changed = true; + input.EnableFlag(SubstanceInput::Flag_Awake, false); + } + } + else if (input.IsFlagEnabled(SubstanceInput::Flag_Modified) || input.IsFlagEnabled(SubstanceInput::Flag_Awake)) + { + int error = 0; + + // Apply Float values + if (IsSubstanceAnyFloatType(input.internalType)) + { + error = substanceHandlePushSetInput( m_SubstanceData->substanceHandle, Substance_PushOpt_NotAHint, input.internalIndex, input.internalType, input.value.scalar, 0 ); + } + // Apply integer values + else if (IsSubstanceAnyIntType(input.internalType)) + { + int intValue[4]; + intValue[0] = (int)input.value.scalar[0]; + intValue[1] = (int)input.value.scalar[1]; + intValue[2] = (int)input.value.scalar[2]; + intValue[3] = (int)input.value.scalar[3]; + error = substanceHandlePushSetInput( m_SubstanceData->substanceHandle, Substance_PushOpt_NotAHint, input.internalIndex, input.internalType, intValue, 0 ); + } + // Apply image values + else if (input.internalType == Substance_IType_Image) + { + if (textureInputIndex>=m_TextureInputs.size()) + { + ErrorStringObject("failed to apply substance input image", this); + } + else + { + error = substanceHandlePushSetInput( m_SubstanceData->substanceHandle, Substance_PushOpt_NotAHint, input.internalIndex, input.internalType, m_TextureInputs[textureInputIndex].inputParameters, 0 ); + } + } + else + { + ErrorStringObject("unsupported substance input type", this); + } + if (error != 0) + ErrorStringObject("Failed to apply substance input", this); + + // Add modified output + modifiedOutputsUID.insert(input.alteredTexturesUID.begin(), input.alteredTexturesUID.end()); + } + + // Keep the texture input index up to date + if (input.internalType == Substance_IType_Image) + { + ++textureInputIndex; + } + } +} + +void ProceduralMaterial::ApplyOutputs (bool& it_has_changed, bool asHint, std::set<unsigned int>& modifiedOutputsUID, const std::set<unsigned int>& cachedTextureIDs) +{ + // Add the invalid outputs + if (!asHint) + { + for (PingedTextures::iterator it=m_PingedTextures.begin();it!=m_PingedTextures.end();++it) + { + if (*it!=NULL && !(*it)->IsValid()) + { + modifiedOutputsUID.insert((*it)->GetSubstanceBaseTextureUID()); + it_has_changed = true; + } + } + } + + // Push outputs + unsigned int flags = Substance_OutOpt_TextureId | Substance_OutOpt_CopyNeeded | (asHint?Substance_PushOpt_HintOnly:0); + GetSubstanceSystem ().processedTextures.clear(); + std::vector<unsigned int> textureIDs; + for (PingedTextures::iterator i=m_PingedTextures.begin();i!=m_PingedTextures.end();++i) + { + ProceduralTexture* texture = *i; + if (texture!=NULL && modifiedOutputsUID.find(texture->GetSubstanceBaseTextureUID())!=modifiedOutputsUID.end() + && cachedTextureIDs.find(texture->GetSubstanceBaseTextureUID())==cachedTextureIDs.end()) + { + GetSubstanceSystem ().processedTextures[texture->GetSubstanceTextureUID()] = texture; + textureIDs.push_back(texture->GetSubstanceTextureUID()); + } + } + + if (textureIDs.size()==0) + { + modifiedOutputsUID.clear(); + it_has_changed = false; + } + + if (textureIDs.size()>0 && substanceHandlePushOutputs( m_SubstanceData->substanceHandle, flags, &textureIDs[0], textureIDs.size(), 0 )!=0) + { + ErrorStringObject("Failed to apply substance texture outputs", this); + } +} +#endif + +void ProceduralMaterial::RebuildTextures() +{ + if (IsFlagEnabled(Flag_AwakeClone)) + { + RebuildClone(); + } +#if ENABLE_SUBSTANCE + else if (!IsWorldPlaying() || m_LoadingBehavior!=ProceduralLoadingBehavior_BakeAndDiscard) + { + GetSubstanceSystem().QueueSubstance(this); + } +#endif +} + +void ProceduralMaterial::RebuildTexturesImmediately() +{ + ASSERT_RUNNING_ON_MAIN_THREAD; + RebuildTextures(); +#if ENABLE_SUBSTANCE + GetSubstanceSystem().WaitFinished(this); +#endif +} + +void ProceduralMaterial::ReloadAll (bool unload, bool load) +{ +#if ENABLE_SUBSTANCE + std::vector<SInt32> objects; + Object::FindAllDerivedObjects (ClassID (ProceduralMaterial), &objects); + std::sort(objects.begin(), objects.end()); + + if (objects.empty()) + return; + + GetSubstanceSystem().WaitFinished(); + for (int i=0;i<objects.size ();i++) + { + ProceduralMaterial& mat = *PPtr<ProceduralMaterial> (objects[i]); + Textures& textures = mat.GetTextures(); + for (Textures::iterator it=textures.begin() ; it!=textures.end() ; ++it) + { +#if UNITY_EDITOR + (*it)->EnableFlag(ProceduralTexture::Flag_Cached, false); +#endif + (*it)->Invalidate(); + } +#if UNITY_EDITOR + mat.EnableFlag(ProceduralMaterial::Flag_Awake); +#endif + if (mat.m_LoadingBehavior==ProceduralLoadingBehavior_BakeAndDiscard || (unload && !load)) + { + for (Textures::iterator it=textures.begin() ; it!=textures.end() ; ++it) + { + if (unload) + (*it)->UnloadFromGfxDevice(false); + if (load) + (*it)->UploadToGfxDevice(); + } + } + else + { + mat.RebuildTextures (); + } + } + SubstanceSystem::Context context(ProceduralProcessorUsage_All); + GetSubstanceSystem().WaitFinished(); +#endif +} + +template<class _class_> +void AwakeProceduralObject(PPtr<_class_>& pptr, _class_*& object, bool awakeThreaded) +{ + if (awakeThreaded) + { + #if SUPPORT_THREADS + Assert (!Thread::CurrentThreadIsMainThread()); + #endif + + // Awake the object + object = dynamic_pptr_cast<_class_*> (InstanceIDToObjectThreadSafe(pptr.GetInstanceID())); + } + else + { + #if SUPPORT_THREADS + Assert (Thread::CurrentThreadIsMainThread()); + #endif + + // We can safely load it synchroneously + object = pptr; + } + + // Clear the PPtr if the object no more exist (removed/deprecated) + if (object==NULL) + pptr.SetInstanceID(0); +} + +void ProceduralMaterial::AwakeDependencies(bool awakeThreaded) +{ + // Simplest validity check + if (m_Textures.size()==0) + { + EnableFlag(Flag_Broken); + return; + } + +#if ENABLE_SUBSTANCE + // Awake package + AwakeProceduralObject(m_SubstancePackage, m_PingedPackage, awakeThreaded); + if (m_PingedPackage==NULL && m_LoadingBehavior!=ProceduralLoadingBehavior_BakeAndDiscard) + { + EnableFlag(Flag_Broken); + return; + } + + // Awake texture inputs + unsigned int input_count(0); + for (SubstanceInputs::iterator it=m_Inputs.begin();it!=m_Inputs.end();++it) + { + if (it->internalType==Substance_IType_Image) + { + if (input_count>=m_TextureInputs.size()) + { + m_TextureInputs.push_back( TextureInput() ); + m_TextureInputs.back().inputParameters = new SubstanceTextureInput(); + memset(m_TextureInputs.back().inputParameters, 0, sizeof(SubstanceTextureInput)); + m_TextureInputs.back().image = new Image(); + m_TextureInputs.back().buffer = NULL; + } + + AwakeProceduralObject(it->value.texture, m_TextureInputs[input_count].texture, awakeThreaded); + ++input_count; + } + } +#endif + + // Awake textures + if (m_PingedTextures.size()!=m_Textures.size()) + { + int size = m_PingedTextures.size(); + m_PingedTextures.resize(m_Textures.size()); + if (size<m_PingedTextures.size()) + memset(&m_PingedTextures[size], 0, sizeof(ProceduralTexture*)*(m_Textures.size()-size)); + } + + int texture_index(0); + for (ProceduralMaterial::Textures::iterator i=m_Textures.begin();i!=m_Textures.end();++i,++texture_index) + { + AwakeProceduralObject(*i, m_PingedTextures[texture_index], awakeThreaded); + if (m_PingedTextures[texture_index]==NULL) + { + EnableFlag(Flag_Broken); + return; + } + m_PingedTextures[texture_index]->SetOwner(this); + } +} + +#if ENABLE_SUBSTANCE + +bool ProceduralMaterial::ProcessTexturesThreaded(const std::map<ProceduralTexture*, SubstanceTexture>& textures) +{ + if (IsFlagEnabled(Flag_Broken)) + return true; + + GetSubstanceSystem().UpdateMemoryBudget(); + + std::set<unsigned int> cachedTextureIDs; +#if ENABLE_CACHING && !UNITY_EDITOR + if (PreProcess(cachedTextureIDs)) + { + return true; + } +#else + // Force rebuild if it's cached and no input is modified + InvalidateIfCachedOrInvalidTextures(); +#endif + + // Apply substance parameters (this is the exact ordering the engine needs) + std::set<unsigned int> modifiedOutputsUID; + bool it_has_changed(false); + // Apply inputs values + ApplyInputs (it_has_changed, false, modifiedOutputsUID); + + // For readable textures, force the rebuild by pushing all output UIDs, + // even if no input was changed (Bnecessary for a clean workflow for case 538383 / GetPixels32()) + if (IsFlagEnabled(Flag_Readable)) + { + for (PingedTextures::iterator i=m_PingedTextures.begin() ; i!=m_PingedTextures.end() ; ++i) + { + ProceduralTexture* texture = *i; + modifiedOutputsUID.insert(texture->GetSubstanceBaseTextureUID()); + } + } + + // Apply outputs uid + ApplyOutputs(it_has_changed, false, modifiedOutputsUID, cachedTextureIDs); + // Apply input hints + ApplyInputs (it_has_changed, true, modifiedOutputsUID); + // Apply outputs hints + ApplyOutputs(it_has_changed, true, modifiedOutputsUID, cachedTextureIDs); + + if (!IsFlagEnabled(Flag_Readable) && !it_has_changed) + { + // Flush render list + substanceHandleFlush( m_SubstanceData->substanceHandle ); + return false; + } + + if (substanceHandleStart( m_SubstanceData->substanceHandle, Substance_Sync_Synchronous )!=0) + ErrorStringObject("Failed to start substance computation", this); + + // Flush render list + substanceHandleFlush( m_SubstanceData->substanceHandle ); + +#if ENABLE_CACHING && !UNITY_EDITOR + PostProcess(textures, cachedTextureIDs); +#endif + return true; +} + +void ProceduralMaterial::ApplyTextureInput (int substanceInputIndex, const SubstanceTextureInput& requiredTextureInput) +{ + // Find which input needs update + bool found(false); + int textureInputIndex(0); + SubstanceInput* input; + for (SubstanceInputs::iterator i=m_Inputs.begin();i != m_Inputs.end();i++) + { + input = &*i; + if (input->internalIndex==substanceInputIndex) + { + found = true; + break; + } + if (input->internalType==Substance_IType_Image) + { + ++textureInputIndex; + } + } + if (!found || textureInputIndex>=m_TextureInputs.size()) + { + ErrorStringObject("Failed to push Substance texture input", this); + return; + } + + // Check format + TextureFormat format; + bool need16bitsConvert(false); + if (requiredTextureInput.pixelFormat==Substance_PF_RGBA) + format = kTexFormatRGBA32; + else if (requiredTextureInput.pixelFormat==Substance_PF_RGB) + format = kTexFormatRGB24; + else if (requiredTextureInput.pixelFormat==(Substance_PF_16b | Substance_PF_RGBA)) + { + format = kTexFormatRGBA32; + need16bitsConvert = true; + } + else if (requiredTextureInput.pixelFormat==(Substance_PF_16b | Substance_PF_RGB)) + { + format = kTexFormatRGB24; + need16bitsConvert = true; + } + else if (requiredTextureInput.pixelFormat==Substance_PF_DXT1) + format = kTexFormatDXT1; + else if (requiredTextureInput.pixelFormat==Substance_PF_DXT3) + format = kTexFormatDXT3; + else if (requiredTextureInput.pixelFormat==Substance_PF_DXT5) + format = kTexFormatDXT5; + else if (requiredTextureInput.pixelFormat==Substance_PF_L) + format = kTexFormatAlpha8; + else + { + ErrorStringObject("Failed to push Substance texture input : unsupported format", this); + return; + } + + if (m_TextureInputs.size()<=textureInputIndex) + { + ErrorStringObject("Failed to push Substance texture input : unexpected error", this); + return; + } + + // Initialize the texture input if required + TextureInput& textureInput = m_TextureInputs[textureInputIndex]; + if (textureInput.inputParameters->level0Width!=requiredTextureInput.level0Width + || textureInput.inputParameters->level0Height!=requiredTextureInput.level0Height + || textureInput.inputParameters->pixelFormat!=requiredTextureInput.pixelFormat) + { + // Fill format description + memcpy(textureInput.inputParameters, &requiredTextureInput, sizeof(SubstanceTextureInput)); + textureInput.inputParameters->mTexture.level0Width = textureInput.inputParameters->level0Width; + textureInput.inputParameters->mTexture.level0Height = textureInput.inputParameters->level0Height; + textureInput.inputParameters->mTexture.mipmapCount = textureInput.inputParameters->mipmapCount; + textureInput.inputParameters->mTexture.pixelFormat = textureInput.inputParameters->pixelFormat; + textureInput.inputParameters->mTexture.channelsOrder = 0; + textureInput.image->SetImage(textureInput.inputParameters->level0Width, textureInput.inputParameters->level0Height, format, true); + + if (textureInput.buffer!=NULL) + { + UNITY_FREE(kMemSubstance, textureInput.buffer); + textureInput.buffer = NULL; + } + + if (need16bitsConvert) + { + size_t pitch = ((requiredTextureInput.pixelFormat | Substance_PF_RGBA)?4:3)*sizeof(unsigned short); + textureInput.buffer = UNITY_MALLOC_ALIGNED_NULL(kMemSubstance, pitch*textureInput.inputParameters->level0Width*textureInput.inputParameters->level0Height, 16); + if (!textureInput.buffer) + { + ErrorString("Could not allocate memory for textureInput.buffer (ApplyTextureInput)"); + return; + } + textureInput.inputParameters->mTexture.buffer = textureInput.buffer; + } + else + { + textureInput.inputParameters->mTexture.buffer = textureInput.image->GetImageData(); + } + } + + Texture2D* texture = textureInput.texture; + if (texture==NULL) + { + // Default to white texture + textureInput.image->ClearImage(ColorRGBAf(1.0f, 1.0f, 1.0f, 1.0f)); + } + else + { + // Try to retrieve current texture + if (texture->ExtractImage(textureInput.image)) + { + textureInput.image->FlipImageY(); + } + else + { + ErrorStringObject("Incorrect ProceduralMaterial input", this); + if (texture->GetRawImageData()==NULL) + { + ErrorStringObject("ProceduralMaterial: Unexpected error (Texture input is not in RAM), try a reimport", texture); + } + else + { + ErrorStringObject("ProceduralMaterial: Texture input is compressed in undecompressable format, you should switch it to RAW, then reimport the material", texture); + } + + textureInput.image->ClearImage(ColorRGBAf(1.0f, 0.0f, 0.0f, 1.0f)); + } + } + + // Actually we can't force the input texture format using the Substance API, + // so here it requires 8bits -> 16bits conversion. + if (need16bitsConvert) + { + size_t pitch = (requiredTextureInput.pixelFormat | Substance_PF_RGBA)?4:3; + unsigned short * output = (unsigned short*)textureInput.buffer; + unsigned char * input = textureInput.image->GetImageData(); + unsigned char * inputEnd = input + pitch*textureInput.image->GetWidth()*textureInput.image->GetHeight(); + while(input!=inputEnd) + { + *(output++) = (unsigned short)*(input++)*257; + } + } +} +#endif + +void ProceduralMaterial::UpdateAnimation(float time) +{ + if (m_AnimationUpdateRate>0) + { + if (time<m_AnimationTime + || time>m_AnimationTime+m_AnimationUpdateRate/1000.0f) + { + m_AnimationTime = time; + SetSubstanceFloat("$time", time); + RebuildTextures(); + } + } +} + +std::vector<std::string> ProceduralMaterial::GetSubstanceProperties() const +{ + std::vector<std::string> properties; + for (SubstanceInputs::const_iterator i=m_Inputs.begin();i!=m_Inputs.end();++i) + { + properties.push_back(i->name); + } + return properties; +} + +bool ProceduralMaterial::HasSubstanceProperty( const std::string& inputName ) const +{ + Mutex::AutoLock locker(m_InputMutex); + return FindSubstanceInput(inputName)!=NULL; +} + +bool ProceduralMaterial::GetSubstanceBoolean( const std::string& inputName ) const +{ + Mutex::AutoLock locker(m_InputMutex); + const SubstanceInput* input = FindSubstanceInput(inputName); + if (input!=NULL) + { + return input->value.scalar[0]>0.5f; + } + return false; +} + +void ProceduralMaterial::SetSubstanceBoolean( const std::string& inputName, bool value ) +{ + SetSubstanceFloat( inputName, value?1.0f:0.0f ); +} + +float ProceduralMaterial::GetSubstanceFloat( const std::string& inputName ) const +{ + Mutex::AutoLock locker(m_InputMutex); + const SubstanceInput* input = FindSubstanceInput(inputName); + if (input!=NULL) + return input->value.scalar[0]; + return 0.0F; +} + +void ProceduralMaterial::SetSubstanceFloat( const std::string& inputName, float value ) +{ + SetSubstanceVector(inputName, Vector4f(value, 0.0f, 0.0f, 0.0f)); +} + +Vector4f ProceduralMaterial::GetSubstanceVector( const std::string& inputName ) const +{ + Mutex::AutoLock locker(m_InputMutex); + const SubstanceInput* input = FindSubstanceInput(inputName); + if (input!=NULL) + { + Vector4f value; + value.Set(input->value.scalar); + return value; + } + return Vector4f(0.0F, 0.0F, 0.0F, 0.0F); +} + +void ProceduralMaterial::SetSubstanceVector( const std::string& inputName, const Vector4f& value ) +{ + Mutex::AutoLock locker(m_InputMutex); + const SubstanceInput* input = FindSubstanceInput(inputName); + if (input!=NULL) + { + SubstanceValue inputValue; + memcpy(inputValue.scalar, value.GetPtr(), sizeof(float)*4); + SetDirty(); +#if ENABLE_SUBSTANCE + GetSubstanceSystem().QueueInput(this, inputName, inputValue); +#endif + } +} + +ColorRGBAf ProceduralMaterial::GetSubstanceColor( const std::string& inputName ) const +{ + Mutex::AutoLock locker(m_InputMutex); + const SubstanceInput* input = FindSubstanceInput(inputName); + if (input!=NULL) + { + ColorRGBAf value; + memcpy(value.GetPtr(), input->value.scalar, sizeof(float)*4); + return value; + } + + return ColorRGBAf(0.0F, 0.0F, 0.0F, 0.0F); +} + +void ProceduralMaterial::SetSubstanceColor( const std::string& inputName, const ColorRGBAf& value ) +{ + SetSubstanceVector(inputName, Vector4f(value.r, value.g, value.b, value.a)); +} + +int ProceduralMaterial::GetSubstanceEnum( const string& inputName ) +{ + Mutex::AutoLock locker(m_InputMutex); + SubstanceInput* input = FindSubstanceInput(inputName); + if (input!=NULL) + { + int index(0); + for (std::vector<SubstanceEnumItem>::iterator it=input->enumValues.begin();it!=input->enumValues.end();++it) + { + if ((int)input->value.scalar[0]==it->value) + { + return index; + } + + ++index; + } + } + return -1; +} + +void ProceduralMaterial::SetSubstanceEnum( const string& inputName, int value ) +{ + Mutex::AutoLock locker(m_InputMutex); + SubstanceInput* input = FindSubstanceInput(inputName); + if (input!=NULL && value>=0 && value<input->enumValues.size()) + { + SubstanceEnumItem& item = input->enumValues[value]; + SubstanceValue inputValue; + inputValue.scalar[0] = (float)item.value; + SetDirty(); +#if ENABLE_SUBSTANCE + GetSubstanceSystem().QueueInput(this, inputName, inputValue); +#endif + } +} + +Texture2D* ProceduralMaterial::GetSubstanceTexture( const string& inputName ) const +{ + Mutex::AutoLock locker(m_InputMutex); + const SubstanceInput* input = FindSubstanceInput(inputName); + if (input!=NULL && input->internalType == Substance_IType_Image) + return input->value.texture; + return NULL; +} + +void ProceduralMaterial::SetSubstanceTexture( const string& inputName, Texture2D* value ) +{ + Mutex::AutoLock locker(m_InputMutex); + const SubstanceInput* input(NULL); + int texture_index(0); + for (SubstanceInputs::iterator i=m_Inputs.begin();i!=m_Inputs.end();++i) + { + if (i->name==inputName) + { + input = &*i; + break; + } + if (i->type==ProceduralPropertyType_Texture) + ++texture_index; + } + + if (input!=NULL && input->type==ProceduralPropertyType_Texture + && texture_index<m_TextureInputs.size()) + { + m_TextureInputs[texture_index].texture = value; + SubstanceValue inputValue; + inputValue.texture = value; + SetDirty(); +#if ENABLE_SUBSTANCE + GetSubstanceSystem().QueueInput(this, inputName, inputValue); +#endif + } +} + +#if ENABLE_SUBSTANCE +void ProceduralMaterial::Callback_SetSubstanceInput( const string& inputName, SubstanceValue& inputValue) +{ + Mutex::AutoLock locker(m_InputMutex); + SubstanceInput* input = FindSubstanceInput(inputName); + + //AreSubstanceInputValuesEqual(input->internalType, input->value, inputValue)) + // Its up to the user to set value if it hasn't changed, he may want to really set the value + // even if it hasn't changed, to cache it for instance. + if (input==NULL) + return; + + ClampSubstanceInputValues(*input, inputValue); + + if (input->type==ProceduralPropertyType_Texture) + { + input->value.texture = inputValue.texture; + } + else + { + memcpy(input->value.scalar, inputValue.scalar, + sizeof(float)*GetRequiredInputComponentCount(input->internalType)); + } + + input->EnableFlag(SubstanceInput::Flag_Modified); + +#if !UNITY_EDITOR + if (IsFlagEnabled(Flag_ConstSize) && (input->name=="$outputsize" || input->name=="$randomseed")) + { + EnableFlag(Flag_ConstSize, false); + Clean(); + + // Initialize fresh substance data + std::vector<ProceduralMaterial*> materials; + materials.push_back(this); + PackSubstances(materials); + } +#endif +} +#endif + +const SubstanceInput* ProceduralMaterial::FindSubstanceInput( const string& inputName ) const +{ + for (SubstanceInputs::const_iterator i=m_Inputs.begin();i!=m_Inputs.end();++i) + { + if (i->name==inputName) + return &*i; + } + return NULL; +} + +SubstanceInput* ProceduralMaterial::FindSubstanceInput( const string& inputName ) +{ + for (SubstanceInputs::iterator i=m_Inputs.begin();i!=m_Inputs.end();++i) + { + if (i->name==inputName) + return &*i; + } + return NULL; +} + +bool ProceduralMaterial::IsSubstancePropertyCached( const string& inputName ) const +{ + const SubstanceInput* input = FindSubstanceInput(inputName); + if (input!=NULL) + return input->IsFlagEnabled(SubstanceInput::Flag_Cached); + return false; +} + +void ProceduralMaterial::CacheSubstanceProperty( const string& inputName, bool value ) +{ + SubstanceInput* input = FindSubstanceInput(inputName); + if (input!=NULL) + input->EnableFlag(SubstanceInput::Flag_Cached, value); +} + +void ProceduralMaterial::ClearCache() +{ +#if ENABLE_SUBSTANCE + for (SubstanceInputs::iterator it=m_Inputs.begin() ; it!=m_Inputs.end() ; ++it) + it->EnableFlag(SubstanceInput::Flag_Cached, false); + GetSubstanceSystem().QueryClearCache(this); +#endif +} + +void ProceduralMaterial::SetProceduralMemoryBudget(ProceduralCacheSize budget) +{ + SetProceduralMemorySleepBudget(budget); + + switch(budget) + { + case ProceduralCacheSize_Tiny: SetProceduralMemoryWorkBudget(ProceduralCacheSize_Medium); break; + case ProceduralCacheSize_Medium: SetProceduralMemoryWorkBudget(ProceduralCacheSize_Heavy); break; + case ProceduralCacheSize_Heavy: SetProceduralMemoryWorkBudget(ProceduralCacheSize_NoLimit); break; + case ProceduralCacheSize_NoLimit: SetProceduralMemoryWorkBudget(ProceduralCacheSize_NoLimit); break; + default: + case ProceduralCacheSize_None: SetProceduralMemoryWorkBudget(ProceduralCacheSize_Tiny); break; + } +} + +ProceduralCacheSize ProceduralMaterial::GetProceduralMemoryBudget() const +{ + if (m_SubstanceData==NULL) + return ProceduralCacheSize_None; + return m_SubstanceData->memoryWorkBudget; +} + +void ProceduralMaterial::SetProceduralMemoryWorkBudget(ProceduralCacheSize budget) +{ + if (m_SubstanceData!=NULL) + m_SubstanceData->memoryWorkBudget = budget; +} + +ProceduralCacheSize ProceduralMaterial::GetProceduralMemoryWorkBudget() const +{ + if (m_SubstanceData==NULL) + return ProceduralCacheSize_None; + return m_SubstanceData->memoryWorkBudget; +} + +void ProceduralMaterial::SetProceduralMemorySleepBudget(ProceduralCacheSize budget) +{ + if (m_SubstanceData!=NULL) + m_SubstanceData->memorySleepBudget = budget; +} + +ProceduralCacheSize ProceduralMaterial::GetProceduralMemorySleepBudget() const +{ + if (m_SubstanceData==NULL) + return ProceduralCacheSize_None; + return m_SubstanceData->memorySleepBudget; +} + +#if UNITY_EDITOR +// Strips substance data during serialization when building a player +struct TemporarilyStripSubstanceData +{ + ProceduralMaterial* material; + PPtr<SubstanceArchive> substancePackage; + SubstanceInputs inputs; + + TemporarilyStripSubstanceData (ProceduralMaterial& mat, bool isBuildingPlayer) + { + bool shouldDiscardSubstanceData; + shouldDiscardSubstanceData = !IsSubstanceSupportedOnPlatform(GetEditorUserBuildSettings().GetActiveBuildTarget()); + shouldDiscardSubstanceData |= mat.GetLoadingBehavior() == ProceduralLoadingBehavior_BakeAndDiscard; + + // Should we discard the substance data? + if (isBuildingPlayer && shouldDiscardSubstanceData) + { + material = &mat; + + // Clear m_SubstancePackage & m_Inputs and back it up so we can revert them after serialization + swap(substancePackage, mat.m_SubstancePackage); + swap(inputs, mat.m_Inputs); + } + else + { + material = NULL; + } + } + + ~TemporarilyStripSubstanceData () + { + if (material != NULL) + { + swap(material->m_Inputs, inputs); + swap(material->m_SubstancePackage, substancePackage); + } + } +}; +#endif + + + +template<class T> void ProceduralMaterial::Transfer( T& transfer ) +{ + Super::Transfer( transfer ); + + // Serialize maximum sizes + if (transfer.IsVersionSmallerOrEqual (2)) + { + int m_MaximumSize; + TRANSFER( m_MaximumSize ); + m_Width = m_MaximumSize; + m_Height = m_MaximumSize; + } + else + { + TRANSFER( m_Width ); + TRANSFER( m_Height ); + } + + TRANSFER( m_Textures ); + TRANSFER( m_Flags ); + + // Serialize load behavior + if (transfer.IsReading ()) + { + // Handle deprecated GenerateAtLoad flag, replaced by LoadingBehavior + m_LoadingBehavior = IsFlagEnabled(Flag_DeprecatedGenerateAtLoad)?ProceduralLoadingBehavior_Generate:ProceduralLoadingBehavior_None; + EnableFlag(Flag_DeprecatedGenerateAtLoad, false); + } + transfer.Transfer(reinterpret_cast<int&> (m_LoadingBehavior), "m_LoadingBehavior"); + + #if UNITY_EDITOR + // Strip unused data when building/collecting assets + TemporarilyStripSubstanceData stripData (*this, transfer.GetFlags () & kBuildPlayerOnlySerializeBuildProperties); + #endif + + TRANSFER( m_SubstancePackage ); + TRANSFER( m_Inputs ); + + TRANSFER( m_PrototypeName ); + if (m_PrototypeName=="") + { + m_PrototypeName = GetName(); + } + + TRANSFER( m_AnimationUpdateRate ); + + TRANSFER(m_Hash); +} + +bool ProceduralMaterial::IsProcessing() const +{ +#if ENABLE_SUBSTANCE + return GetSubstanceSystem().IsSubstanceProcessing(this); +#else + return false; +#endif +} + +void ProceduralMaterial::SetProceduralProcessorUsage(ProceduralProcessorUsage processorUsage) +{ +#if ENABLE_SUBSTANCE + GetSubstanceSystem().SetProcessorUsage(processorUsage); +#endif +} + +ProceduralProcessorUsage ProceduralMaterial::GetProceduralProcessorUsage() +{ +#if ENABLE_SUBSTANCE + return GetSubstanceSystem().GetProcessorUsage(); +#else + return ProceduralProcessorUsage_Unsupported; +#endif +} + +void ProceduralMaterial::StopProcessing() +{ +#if ENABLE_SUBSTANCE + GetSubstanceSystem().ClearProcessingQueue(); +#endif +} + +#if UNITY_EDITOR + +void ProceduralMaterial::InvalidateIfCachedOrInvalidTextures() +{ + // Check if some textures are cached + bool cachedOrInvalid=false; + for (PingedTextures::iterator i=m_PingedTextures.begin();i!=m_PingedTextures.end();++i) + { + ProceduralTexture* texture = *i; + if (texture!=NULL + && (texture->IsFlagEnabled(ProceduralTexture::Flag_Cached) + || !texture->IsValid())) + { + cachedOrInvalid = true; + break; + } + } + + if (cachedOrInvalid) + { + // Check if an input has been modified + bool modified = false; + for (SubstanceInputs::iterator i=m_Inputs.begin();i != m_Inputs.end();++i) + { + SubstanceInput& input = *i; + if (input.IsFlagEnabled(SubstanceInput::Flag_Modified)) + { + modified = true; + break; + } + } + // Force rebuild since no input is modified + if (!modified) + { + for (SubstanceInputs::iterator i=m_Inputs.begin();i != m_Inputs.end();++i) + { + SubstanceInput& input = *i; + input.EnableFlag(SubstanceInput::Flag_Awake); + } + } + } +} + +bool IsSubstanceSupportedOnPlatform(BuildTargetPlatform platform) +{ +return (platform == kBuildWebPlayerLZMA + || platform == kBuildWebPlayerLZMAStreamed + || platform == kBuildStandaloneOSXIntel + || platform == kBuildStandaloneOSXIntel64 + || platform == kBuildStandaloneOSXUniversal + || platform == kBuildStandaloneWinPlayer + || platform == kBuildStandaloneWin64Player + || platform == kBuildStandaloneLinux + || platform == kBuildStandaloneLinux64 + || platform == kBuildStandaloneLinuxUniversal + || platform == kBuild_Android + || platform == kBuild_iPhone + || platform == kBuildNaCl + ); +} +#endif + +bool IsSubstanceSupported() +{ +#if UNITY_EDITOR + BuildTargetPlatform platform = GetEditorUserBuildSettings().GetActiveBuildTarget(); + return IsSubstanceSupportedOnPlatform(platform); +#endif + +#if ENABLE_SUBSTANCE + return true; +#else + return false; +#endif +} + +TextureFormat GetSubstanceTextureFormat(SubstanceOutputFormat outputFormat, bool requireCompressed) +{ +#if ENABLE_SUBSTANCE +#if UNITY_EDITOR + BuildTargetPlatform platform = GetEditorUserBuildSettings().GetActiveBuildTarget(); + if (!requireCompressed || IsSubstanceSupportedOnPlatform(platform)) +#endif + { + TextureFormat format(kTexFormatRGBA32); + + if (outputFormat==Substance_OFormat_Compressed) + { +#if UNITY_IPHONE + format = kTexFormatPVRTC_RGBA4; +#elif UNITY_ANDROID + if (gGraphicsCaps.supportsTextureFormat[kTexFormatDXT5]) + { + format = kTexFormatDXT5; + } + else if (gGraphicsCaps.supportsTextureFormat[kTexFormatPVRTC_RGBA4]) + { + format = kTexFormatPVRTC_RGBA4; + } + else + { + // Lowest common denominator = ETC + // But this will cancel the alpha, need to think of something else for non-DXT non-PVR platforms + // format = kTexFormatETC_RGB4; + } +#else + format = kTexFormatDXT5; +#endif + } + + return format; + } +#endif + + return kTexFormatRGBA32; +} + +SubstanceEngineIDEnum GetSubstanceEngineID() +{ +#if defined(__ppc__) + return Substance_EngineID_xenos; +#endif + return Substance_EngineID_sse2; +} diff --git a/Runtime/Graphics/ProceduralMaterial.h b/Runtime/Graphics/ProceduralMaterial.h new file mode 100644 index 0000000..e008566 --- /dev/null +++ b/Runtime/Graphics/ProceduralMaterial.h @@ -0,0 +1,289 @@ +#pragma once + +#include "Configuration/UnityConfigure.h" +#include "Runtime/Threads/Mutex.h" +#include "Runtime/Shaders/Material.h" +#include "Runtime/Utilities/Hash128.h" +#include "ProceduralTexture.h" +#include "SubstanceInput.h" +#include "External/Allegorithmic/builds/Engines/include/substance/handle.h" +#include "External/Allegorithmic/builds/Engines/include/substance/linker/linker.h" + +class SubstanceArchive; +class Texture2D; +class Image; + +#if UNITY_EDITOR +bool IsSubstanceSupportedOnPlatform(BuildTargetPlatform platform); +#endif + +bool IsSubstanceSupported(); // return true if Substance is supported on current platform +TextureFormat GetSubstanceTextureFormat(SubstanceOutputFormat outputFormat, bool requireCompressed=false); // return the required Substance texture format +SubstanceEngineIDEnum GetSubstanceEngineID(); // return the required Substance engine ID + +// Global Substance processor usage +enum ProceduralProcessorUsage +{ + ProceduralProcessorUsage_Unsupported = 0, // Substance isn't supported + ProceduralProcessorUsage_One, // uses only one CPU + ProceduralProcessorUsage_Half, // uses half of the CPU count (and not the first one if applicable) + ProceduralProcessorUsage_All // uses all the CPU +}; + +// Memory budget +enum ProceduralCacheSize +{ + ProceduralCacheSize_Tiny = 0, // 128 Mb + ProceduralCacheSize_Medium, // 256 Mb + ProceduralCacheSize_Heavy, // 512 Mb + ProceduralCacheSize_NoLimit, // unlimited + ProceduralCacheSize_None // 1 byte +}; + +size_t GetProceduralMemoryBudget(ProceduralCacheSize budget); + +// Loading behavior +enum ProceduralLoadingBehavior +{ + ProceduralLoadingBehavior_None = 0, // do nothing + ProceduralLoadingBehavior_Generate, // generate textures + ProceduralLoadingBehavior_BakeAndKeep, // use baked textures, the substance may be generated later on + ProceduralLoadingBehavior_BakeAndDiscard, // use baked textures, the substance data is removed from runtime + ProceduralLoadingBehavior_Cache // generate textures and cache it, then use it at runtime to speed-up the loading +}; + +/* A ProceduralMaterial is a dynamic material that use the Substance engine. + */ + +class ProceduralMaterial : public Material +{ +public: // NESTED TYPES + + typedef std::vector<PPtr<ProceduralTexture> > Textures; + typedef std::vector<ProceduralTexture*> PingedTextures; + +public: // METHODS + + REGISTER_DERIVED_CLASS( ProceduralMaterial, Material ) + DECLARE_OBJECT_SERIALIZE( ProceduralMaterial ) + + ProceduralMaterial( MemLabelId label, ObjectCreationMode mode ); + + void Clean(); + + ProceduralMaterial* Clone(); +private: + void RebuildClone(); + +public: +#if UNITY_EDITOR + // Creates the material (one time call only from the importer) + void Init( SubstanceArchive& substancePackage, const UnityStr& prototypeName, const SubstanceInputs& inputs, const Textures& textures ); + SubstanceArchive* GetSubstancePackage() { return m_PingedPackage; } + const char* GetSubstancePackageName(); +#endif + const Textures& GetTextures() const { return m_Textures; } + Textures& GetTextures() { return m_Textures; } + PingedTextures& GetPingedTextures() { return m_PingedTextures; } + SubstanceInputs& GetSubstanceInputs () { return m_Inputs; } + const SubstanceInputs& GetSubstanceInputs () const { return m_Inputs; } + const SubstanceArchive* GetSubstancePackage() const { return m_PingedPackage; } + SubstanceHandle* GetSubstanceHandle(); + + // Generation sizes accessors + void SetSize(int width, int height); + int GetWidth() const { return m_Width; } + int GetHeight() const { return m_Height; } + + // Threaded loading, launch the generation if all data is available + void AwakeFromLoadThreaded(); + + // Used in the editor to generate at reimport time + void AwakeFromLoad( AwakeFromLoadMode awakeMode ); + + // Call the rebuild of all the textures and updates the SBS texture assets + // generationType, the type of requested texture generation + // forceTextureGeneration, a boolean to force textures generation even though they may be up to date from the engine's point of view + void RebuildTextures(); + + // Rebuilds all texture immediately in a synchronous manner + // When that function returns, all textures should have been generated + void RebuildTexturesImmediately(); + + // Call the rebuild of all the textures in all of the Procedural Materials + static void ReloadAll (bool unload = true, bool load = true); + + // Awake dependent objects + void AwakeDependencies(bool awakeThreaded); + + // Process rebuild of textures +#if ENABLE_SUBSTANCE + static ProceduralMaterial* m_PackedSubstance; + static void PackSubstances(std::vector<ProceduralMaterial*>& materials); + bool ProcessTexturesThreaded(const std::map<ProceduralTexture*, SubstanceTexture>& textures); +#if !UNITY_EDITOR + bool PreProcess(std::set<unsigned int>& cachedTextureIDs); + void PostProcess(const std::map<ProceduralTexture*, SubstanceTexture>& textures, const std::set<unsigned int>& cachedTextureIDs); +#endif +#endif + + // Scriptable input accessors + std::vector<std::string> GetSubstanceProperties() const; + bool HasSubstanceProperty( const std::string& inputName ) const; + bool GetSubstanceBoolean( const std::string& inputName ) const; + void SetSubstanceBoolean( const std::string& inputName, bool value ); + float GetSubstanceFloat( const std::string& inputName ) const; + void SetSubstanceFloat( const std::string& inputName, float value ); + Vector4f GetSubstanceVector( const std::string& inputName ) const; + void SetSubstanceVector( const std::string& inputName, const Vector4f& value ); + ColorRGBAf GetSubstanceColor( const std::string& inputName ) const; + void SetSubstanceColor( const std::string& inputName, const ColorRGBAf& value ); + int GetSubstanceEnum( const string& inputName ); + void SetSubstanceEnum( const string& inputName, int value ); + Texture2D* GetSubstanceTexture( const string& inputName ) const; + void SetSubstanceTexture( const string& inputName, Texture2D* value ); + +#if ENABLE_SUBSTANCE + // Called by the SubstanceSystem when he process a "SetInput" command + void Callback_SetSubstanceInput( const string& inputName, SubstanceValue& inputValue ); +#endif + + const SubstanceInput* FindSubstanceInput( const string& inputName ) const; + SubstanceInput* FindSubstanceInput( const string& inputName ); + + // Property caching + bool IsSubstancePropertyCached( const string& inputName ) const; + void CacheSubstanceProperty( const string& inputName, bool value ); + void ClearCache(); + + // Memory budget + void SetProceduralMemoryBudget(ProceduralCacheSize budget); + ProceduralCacheSize GetProceduralMemoryBudget() const; + void SetProceduralMemoryWorkBudget(ProceduralCacheSize budget); + ProceduralCacheSize GetProceduralMemoryWorkBudget() const; + void SetProceduralMemorySleepBudget(ProceduralCacheSize budget); + ProceduralCacheSize GetProceduralMemorySleepBudget() const; + +protected: + +#if ENABLE_SUBSTANCE + void ApplyInputs (bool& it_has_changed, bool asHint, std::set<unsigned int>& modifiedOutputsUID); + void ApplyOutputs (bool& it_has_changed, bool asHint, std::set<unsigned int>& modifiedOutputsUID, const std::set<unsigned int>& cachedTextureIDs); +#endif + +private: + + // Shared substance data + struct SubstanceData + { + UInt8* substanceData; // Platform dependent linked binary content + SubstanceHandle* substanceHandle; // Substance engine handle, shared by instances + ProceduralCacheSize memoryWorkBudget; // 'Work' cache size, shared by instances + ProceduralCacheSize memorySleepBudget; // 'Sleep' cache size, shared by instances + int instanceCount; // Count of substances using the same handle + }; + + PPtr<SubstanceArchive> m_SubstancePackage; // The parent SBS package from which we get generated + SubstanceArchive* m_PingedPackage; // The pinged package + SubstanceData* m_SubstanceData; // Shared substance data + UnityStr m_PrototypeName; // The name of the original graph in the package + int m_Width; // Width + int m_Height; // Height + Textures m_Textures; // The list of persistent output textures for that material + PingedTextures m_PingedTextures; // The list of pinged output textures + SubstanceInputs m_Inputs; // Substance inputs + static Mutex m_InputMutex; // Input accessors mutex + Hash128 m_Hash; // Hash used for cache status checking + +public: + // Texture inputs + struct TextureInput + { + Texture2D* texture; + Image* image; + SubstanceTextureInput* inputParameters; + void* buffer; + }; + std::vector<TextureInput> m_TextureInputs; +#if UNITY_EDITOR + std::vector<TextureInput>& GetTextureInputs() { return m_TextureInputs; } +#endif + + void ApplyTextureInput (int substanceInputIndex, const SubstanceTextureInput& requiredTextureInput); + + // Animated substances +private: + int m_AnimationUpdateRate; + float m_AnimationTime; +public: + void SetAnimationUpdateRate(int rate) { m_AnimationUpdateRate = rate; } + int GetAnimationUpdateRate() const { return m_AnimationUpdateRate; } + void UpdateAnimation(float time); + + // Flags +public: + enum Flag + { + Flag_DeprecatedGenerateAtLoad = 1<<0, // deprecated + Flag_Animated = 1<<2, // the material has animated textures + Flag_AwakeClone = 1<<3, // the material is a clone which require to be awaken + Flag_GenerateAll = 1<<4, // we force the generation of all outputs + Flag_ConstSize = 1<<5, // the size and seed don't change at runtime + Flag_ForceGenerate = 1<<6, // force the generation + Flag_Clone = 1<<7, // the material is a clone + Flag_Import = 1<<8, // the material is being imported + Flag_Awake = 1<<9, // the material is awakening + Flag_Uncompressed = 1<<10, // the import is forced uncompressed + Flag_Broken = 1<<11, // some dependencies are lacking, the substance can't be generated + Flag_Readable = 1<<12 // generated textures are readable, provided it's in RAW format + }; + void EnableFlag(const Flag& flag, bool enabled=true) { if (enabled) m_Flags |= (unsigned int)flag; else m_Flags &= ~(unsigned int)flag; } + bool IsFlagEnabled(const Flag& flag) const { return m_Flags & (unsigned int)flag; } +private: + unsigned int m_Flags; + + // Loading mode +public: + void SetLoadingBehavior(ProceduralLoadingBehavior behavior) { m_LoadingBehavior = behavior; } + ProceduralLoadingBehavior GetLoadingBehavior() const { return m_LoadingBehavior; } +private: + ProceduralLoadingBehavior m_LoadingBehavior; + +// Substance processing +public: + bool IsProcessing() const; + static void SetProceduralProcessorUsage(ProceduralProcessorUsage processorUsage); + static ProceduralProcessorUsage GetProceduralProcessorUsage(); + static void StopProcessing(); + SubstanceData* GetSubstanceData() { return m_SubstanceData; } + +// Presets handling + bool SetPreset(const std::string& presetContent); + std::string GetPreset() const; + +// Textures accessors + ProceduralTexture* GetGeneratedTexture(const std::string& textureName); + +// Integration + unsigned int integrationTimeStamp; + +#if UNITY_EDITOR + UInt8* GetHashPtr() { return m_Hash.hashData.bytes; } + void InvalidateIfCachedOrInvalidTextures(); + + friend struct TemporarilyStripSubstanceData; +#endif + +// Enter/Leave PlayMode +#if UNITY_EDITOR + bool m_isAlreadyLoadedInCurrentScene; +#endif + +// Caching +#if ENABLE_SUBSTANCE && !UNITY_EDITOR + std::string GetCacheFolder() const; + std::string GetCacheFilename(const ProceduralTexture& texture) const; + bool ReadCachedTexture(string& fileName, std::map<ProceduralTexture*, SubstanceTexture>& cachedTextures, const std::string& folder, const ProceduralTexture& texture); + bool WriteCachedTexture(string& fileName, const std::string& folder, const ProceduralTexture& texture, const SubstanceTexture& data); +#endif +}; diff --git a/Runtime/Graphics/ProceduralPreset.cpp b/Runtime/Graphics/ProceduralPreset.cpp new file mode 100644 index 0000000..154cdd3 --- /dev/null +++ b/Runtime/Graphics/ProceduralPreset.cpp @@ -0,0 +1,143 @@ +#include "UnityPrefix.h" +#include "ProceduralMaterial.h" + +int ProceduralPreset_parseValues(std::vector<std::string>& values, std::string name, std::string line) +{ + values.clear(); + int start = line.find(name); + if (start==std::string::npos) + { + return 0; + } + + line = line.substr(start+name.size()); + if (line.size()<3 || line[0]!='=' || line[1]!='\"') + { + return 0; + } + + line = line.substr(2); + int p=0; + string val; + while (p<line.size() && line[p]!='\"') + { + if (line[p]==',') + { + values.push_back(val); val = ""; + } + else + { + val += line[p]; + } + ++p; + } + if (val.size()>0) + { + values.push_back(val); + } + return values.size(); +} + +inline std::string ProceduralPreset_getLine(std::string::const_iterator& pos, const std::string& preset) +{ + std::string::const_iterator i = pos; + std::string::const_iterator it = std::find(i, preset.end(), '\n'); + pos = it; + if (it!=preset.end()) ++pos; + return std::string(i, it); +} + +bool ProceduralMaterial::SetPreset(const std::string& preset) +{ + std::string::const_iterator pos = preset.begin(); + std::string line; + line = ProceduralPreset_getLine(pos, preset); + if (line.find("formatversion=\"1.0\"")==std::string::npos) + { + return false; + } + + while (pos!=preset.end() && line.find("/sbspreset")==std::string::npos) + { + line = ProceduralPreset_getLine(pos, preset); + if (line.find("presetinput")!=std::string::npos) + { + std::vector<std::string> identifier; + if (ProceduralPreset_parseValues(identifier, "identifier", line)!=1) + continue; + + SubstanceInput* input = FindSubstanceInput(identifier[0]); + if (input==NULL || identifier[0]=="$normalformat" || identifier[0]=="$outputsize") + continue; + + std::vector<std::string> type; + if (ProceduralPreset_parseValues(type, "type", line)!=1) + continue; + + unsigned int internalType = atoi(type[0].c_str()); + if (internalType!=input->internalType || internalType==Substance_IType_Image) + continue; + + std::vector<std::string> value; + ProceduralPreset_parseValues(value, "value", line); + float f[4]; + int count = GetRequiredInputComponentCount((SubstanceInputType)internalType); + if (value.size()!=count) + continue; + for (int i=0 ; i<count ; ++i) + { + f[i] = (float)atof(value[i].c_str()); + } + SetSubstanceVector(identifier[0], Vector4f(f)); + } + } + RebuildTextures(); + return true; +} + +std::string ProceduralMaterial::GetPreset() const +{ + std::string preset = "<sbspresets formatversion=\"1.0\" count=\"1\">\n"; + preset += " <sbspreset pkgurl=\"\" description=\"\" label=\"\">\n"; + char tmp[256]; + for (SubstanceInputs::const_iterator i=m_Inputs.begin() ; i!=m_Inputs.end() ; ++i) + { + preset += " <presetinput identifier=\""; + preset += i->name; + preset += "\" uid=\""; + snprintf(tmp, 256, "%d", i->internalIdentifier); + preset += tmp; + preset += "\" type=\""; + snprintf(tmp, 256, "%d", (int)i->internalType); + preset += tmp; + preset += "\" value=\""; + if (i->internalType!=Substance_IType_Image) + { + bool isInteger = IsSubstanceAnyIntType(i->internalType); + int count = GetRequiredInputComponentCount(i->internalType); + for (int j=0 ; j<count ; ++j) + { + if (j>0) preset += ","; + if (isInteger) snprintf(tmp, 256, "%d", (int)i->value.scalar[j]); + else snprintf(tmp, 256, "%f", i->value.scalar[j]); + preset += tmp; + } + } + preset += "\"/>\n"; + } + preset += " </sbspreset>\n"; + preset += "</sbspresets>\n"; + return preset; +} + +ProceduralTexture* ProceduralMaterial::GetGeneratedTexture(const std::string& textureName) +{ + for (Textures::iterator it=m_Textures.begin() ; it!=m_Textures.end() ; ++it) + { + if (it->IsValid() && (*it)->GetName()==textureName) + { + return &**it; + } + } + return NULL; +} diff --git a/Runtime/Graphics/ProceduralTexture.cpp b/Runtime/Graphics/ProceduralTexture.cpp new file mode 100644 index 0000000..c4ea5ea --- /dev/null +++ b/Runtime/Graphics/ProceduralTexture.cpp @@ -0,0 +1,372 @@ +#include "UnityPrefix.h" +#include "ProceduralTexture.h" +#include "SubstanceArchive.h" +#include "ProceduralMaterial.h" +#include "Image.h" +#include "SubstanceSystem.h" +#include "Texture2D.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "Runtime/Math/Color.h" +#include "Runtime/GfxDevice/GfxDevice.h" +#include "Runtime/Serialize/PersistentManager.h" +#include "Runtime/Graphics/S3Decompression.h" +#include "Runtime/File/ApplicationSpecificPersistentDataPath.h" +#include "External/shaderlab/Library/shaderlab.h" +#include "External/shaderlab/Library/properties.h" + +using namespace std; + +ProceduralTexture::TextureParameters::TextureParameters() : + width (0), + height(0), + mipLevels(0), + textureFormat(kTexFormatTotalCount) +{ +} + +ProceduralTexture::TextureParameters::TextureParameters(int inWidth, int inHeight, int inMipLevels, TextureFormat inFormat) : + width (inWidth), + height(inHeight), + mipLevels(inMipLevels), + textureFormat(inFormat) +{ +} + +IMPLEMENT_CLASS( ProceduralTexture ) +IMPLEMENT_OBJECT_SERIALIZE( ProceduralTexture ) + +ProceduralTexture::ProceduralTexture( MemLabelId label, ObjectCreationMode mode ) : + Super( label, mode ), + m_SubstanceTextureUID( 0 ), + m_Type(Substance_OType_Unknown), + m_AlphaSource(Substance_OType_Unknown), + m_Flags(0), + m_UploadState(UploadState_None), + m_Format(Substance_OFormat_Compressed), + m_PingedMaterial(NULL) +{ +} + +ProceduralTexture::~ProceduralTexture() +{ + // Update pointer + if (m_PingedMaterial==NULL) + m_PingedMaterial = dynamic_pptr_cast<ProceduralMaterial*>(Object::IDToPointer(m_SubstanceMaterial.GetInstanceID())); + +#if ENABLE_SUBSTANCE + /////@TODO: This is an incredible hack. Waiting for another process to complete in the destructor is a big no no, especially when it involves a Thread::Sleep ()... + UnlockObjectCreation(); + GetSubstanceSystem ().NotifyTextureDestruction(this); + LockObjectCreation(); +#endif + + RemoveTexture (); +} + +#if ENABLE_SUBSTANCE +ProceduralTexture* ProceduralTexture::Clone(ProceduralMaterial* owner) +{ + ProceduralTexture* clone = CreateObjectFromCode<ProceduralTexture>(); + clone->m_SubstanceMaterial = owner; + clone->m_PingedMaterial = owner; + clone->m_SubstanceTextureUID = m_SubstanceTextureUID; + clone->m_Type = m_Type; + clone->m_AlphaSource = m_AlphaSource; + clone->m_Format = m_Format; + clone->m_SubstanceFormat = m_SubstanceFormat; + clone->m_ClonedID = GetTextureID(); + clone->SetName(GetName()); + clone->EnableFlag(Flag_AwakeClone, true); + clone->SetUsageMode(GetUsageMode()); + return clone; +} + +void ProceduralTexture::AwakeClone() +{ + // Update material texenvs + const ShaderLab::PropertySheet::TexEnvs& textureProperties = GetSubstanceMaterial()->GetProperties().GetTexEnvsMap(); + for (ShaderLab::PropertySheet::TexEnvs::const_iterator i=textureProperties.begin();i!=textureProperties.end();++i) + { + if (i->second.texEnv->GetAssignedTextureID()==m_ClonedID) + { + GetSubstanceMaterial()->SetTexture(i->first, this); + } + } + EnableFlag(Flag_AwakeClone, false); +} +#endif + +#if UNITY_EDITOR +void ProceduralTexture::Init( ProceduralMaterial& _Parent, int substanceTextureUID, ProceduralOutputType type, SubstanceOutputFormat format, ProceduralOutputType alphaSource, bool requireCompressed ) +{ + m_SubstanceMaterial = &_Parent; + m_SubstanceTextureUID = ((UInt64)substanceTextureUID)<<32; + m_Type = type; + m_Format = format; + m_TextureParameters.textureFormat = GetSubstanceTextureFormat(format, requireCompressed); + m_AlphaSource = alphaSource; + m_Flags = 0; + + AwakeFromLoad(kDefaultAwakeFromLoad); +} +#endif + +bool ProceduralTexture::IsBaked() const +{ + return m_BakedParameters.IsValid() && m_BakedData.size()>0; +} + +bool ProceduralTexture::GetPixels32(int x, int y, int width, int height, ColorRGBA32* data) +{ + if (m_Format != Substance_OFormat_Raw) + { + WarningStringMsg("Substance %s should be set to RAW in order to use GetPixels32 on its texture outputs.", m_PingedMaterial->GetName()); + return false; + } + if (! m_PingedMaterial->IsFlagEnabled(ProceduralMaterial::Flag_Readable)) + { + WarningStringMsg("The isReadable property of Substance %s should be set to true in order to use GetPixels32 on its texture outputs.", m_PingedMaterial->GetName()); + return false; + } + const ProceduralTexture::TextureParameters& parameters = GetBakedParameters(); + if (GetBakedData().size()==0 || (parameters.textureFormat!=kTexFormatRGBA32 && parameters.textureFormat!=kTexFormatARGB32)) + return false; + ImageReference rawTexture(parameters.width, parameters.height, GetRowBytesFromWidthAndFormat(parameters.width, parameters.textureFormat), parameters.textureFormat, &GetBakedData()[0]); + ImageReference resultImage(parameters.width, parameters.height, GetRowBytesFromWidthAndFormat(parameters.width, kTexFormatRGBA32), kTexFormatRGBA32, data); + resultImage.BlitImage(rawTexture, ImageReference::BLIT_COPY); + return true; +} + +void ProceduralTexture::Invalidate() +{ + m_TextureParameters.Invalidate(); +} + +void ProceduralTexture::UnloadFromGfxDevice (bool forceUnloadAll) +{ + RemoveTexture(); +} + +void ProceduralTexture::UploadToGfxDevice () +{ + if (!m_BakedParameters.IsValid()) + return; + if (m_BakedData.size()==0) + { + GetPersistentManager().ReloadFromDisk(this); + } + else + { + UploadBakedTexture(); + } +} + +void ProceduralTexture::RemoveTexture () +{ + if (IsFlagEnabled (Flag_Uploaded)) + { + GetGfxDevice().DeleteTexture (GetTextureID()); + EnableFlag(Flag_Uploaded, false); + m_UploadState = UploadState_None; + } +} + +void ProceduralTexture::UploadWaitingTexture () +{ + RemoveTexture (); + // Upload blue texture to show the substance is waiting generation + UInt8 bluePixel[] = { 255, 0, 0, 255 }; + GetGfxDevice().UploadTexture2D( GetTextureID(), GetDimension(), bluePixel, sizeof(bluePixel), + 1, 1, kTexFormatARGB32, 1, true, 0 /* \note We upload only one level so can't skip any */, + GetUsageMode(), kTexColorSpaceLinear); + Texture::s_TextureIDMap.insert (std::make_pair(GetTextureID(),this)); + EnableFlag(Flag_Uploaded, true); + m_UploadState = UploadState_Waiting; + m_TextureSettings.Apply( GetTextureID(), GetDimension(), false, kTexColorSpaceLinear ); +} + +void ProceduralTexture::UploadBakedTexture () +{ + RemoveTexture (); + Assert(m_BakedData.size()>0); + GetGfxDevice().UploadTexture2D( GetTextureID(), GetDimension(), &m_BakedData[0], m_BakedData.size(), + m_BakedParameters.width, m_BakedParameters.height, m_BakedParameters.textureFormat, + m_BakedParameters.mipLevels, true, 0 /* \note We upload only one level so can't skip any */, + GetUsageMode(), GetActiveTextureColorSpace()); + Texture::s_TextureIDMap.insert (std::make_pair(GetTextureID(),this)); + EnableFlag(Flag_Uploaded, true); + m_UploadState = UploadState_Baked; + m_TextureSettings.Apply( GetTextureID(), GetDimension(), m_BakedParameters.mipLevels != 1, GetActiveTextureColorSpace() ); + m_TextureParameters = m_BakedParameters; + +#if !UNITY_EDITOR + m_BakedData.clear(); +#endif +} + +void ProceduralTexture::SetSubstanceShuffledUID(unsigned int textureUID) +{ + m_SubstanceTextureUID &= ((UInt64)0xffffffff) << 32; + m_SubstanceTextureUID |= (UInt64)textureUID; +} + +void ProceduralTexture::AwakeFromLoadThreaded() +{ + Super::AwakeFromLoadThreaded(); + + // Update format before it gets packed & generated + m_SubstanceFormat = GetSubstanceTextureFormat(m_Format); + +#if UNITY_ANDROID || UNITY_IPHONE + // It is more practical to do this check at runtime since for Substances we control + // the way we generate the normal maps. Doing this here instead of in the editor avoids + // the reimports that would be associated with platform switches (non-mobile <-> mobile). + if (m_UsageMode == kTexUsageNormalmapDXT5nm) + { + // Override normal format + m_UsageMode = kTexUsageNormalmapPlain; + } +#endif +} + +void ProceduralTexture::AwakeFromLoad( AwakeFromLoadMode awakeMode ) +{ + Super::AwakeFromLoad( awakeMode ); + + // Force loading of material + if ((awakeMode & kDidLoadThreaded) == 0) + { + ProceduralMaterial* material = m_SubstanceMaterial; + if (material==NULL) + { + // This happens + } + } + + if (IsBaked()) + { + if (m_UploadState<UploadState_Valid) + UploadBakedTexture(); + } + else + { + if (m_UploadState==UploadState_None) + UploadWaitingTexture(); + } + + m_SubstanceFormat = GetSubstanceTextureFormat(m_Format); + +#if UNITY_ANDROID || UNITY_IPHONE + // It is more practical to do this check at runtime since for Substances we control + // the way we generate the normal maps. Doing this here instead of in the editor avoids + // the reimports that would be associated with platform switches (non-mobile <-> mobile). + if (m_UsageMode == kTexUsageNormalmapDXT5nm) + { + // Override normal format + m_UsageMode = kTexUsageNormalmapPlain; + } +#endif +} + +void ProceduralTexture::UploadSubstanceTexture(SubstanceTexture& outputTexture) +{ +#if ENABLE_SUBSTANCE + // Check if it's full pyramid + if (outputTexture.mipmapCount==0) + { + outputTexture.mipmapCount = CalculateMipMapCount3D(outputTexture.level0Width, outputTexture.level0Height, 1); + } + + // Check if we can replace the existing texture data + TextureParameters state (outputTexture.level0Width, outputTexture.level0Height, outputTexture.mipmapCount, m_SubstanceFormat); + + bool reuseTextureMemory = (state==m_TextureParameters); + if (!reuseTextureMemory) + RemoveTexture (); + + int bufferSize = CalculateMipMapOffset(state.width, state.height, state.textureFormat, state.mipLevels+1); + GetGfxDevice().UploadTexture2D( GetTextureID(), GetDimension(), reinterpret_cast<UInt8*> (outputTexture.buffer), bufferSize, + state.width, state.height, state.textureFormat, state.mipLevels, !reuseTextureMemory, min(state.mipLevels-1, Texture::GetMasterTextureLimit()), + GetUsageMode(), GetActiveTextureColorSpace()); + Texture::s_TextureIDMap.insert (std::make_pair(GetTextureID(),this)); + EnableFlag(Flag_Uploaded, true); + m_TextureParameters = state; + + // Handle readable flag + if (GetSubstanceMaterial()!=NULL && GetSubstanceMaterial()->IsFlagEnabled(ProceduralMaterial::Flag_Readable) + && (state.textureFormat==kTexFormatRGBA32 || state.textureFormat==kTexFormatARGB32)) + { + size_t size = state.width*state.height*4; + m_BakedData.resize(size); + memcpy(&m_BakedData[0], outputTexture.buffer, size); + m_BakedParameters = state; + } + + if (IsFlagEnabled(Flag_AwakeClone)) + AwakeClone(); + + m_TextureSettings.Apply( GetTextureID(), GetDimension(), outputTexture.mipmapCount != 1, GetActiveTextureColorSpace() ); + m_UploadState = UploadState_Generated; +#endif +} + +void ProceduralTexture::SetOwner(ProceduralMaterial* material) +{ + Assert (material->IsFlagEnabled(ProceduralMaterial::Flag_Clone) + || m_PingedMaterial==NULL || m_PingedMaterial==material); + if (m_PingedMaterial==NULL) + m_PingedMaterial = material; +} + +template<class TransferFunction> +void ProceduralTexture::Transfer (TransferFunction& transfer) +{ + Super::Transfer (transfer); + + TRANSFER( m_SubstanceMaterial ); + TRANSFER( m_SubstanceTextureUID ); + transfer.Transfer(reinterpret_cast<int&> (m_Type), "Type"); + transfer.Transfer(reinterpret_cast<int&> (m_AlphaSource), "AlphaSource"); + transfer.Transfer(reinterpret_cast<int&> (m_Format), "Format"); + + if (m_Format<0 || m_Format>1) + m_Format = Substance_OFormat_Compressed; + + TRANSFER( m_TextureSettings ); + + if (transfer.IsBuildingTargetPlatform(kBuildXBOX360)) + { + const size_t size = m_BakedData.size(); + std::vector<UInt8> swappedData; + swappedData.resize(size); + if (m_Format == Substance_OFormat_Compressed) + { + // Compressed Substance textures are DXT : 16b byte-swap needed + for (int i=0 ; i<size/2 ; ++i) + { + swappedData[2*i+0] = m_BakedData[2*i+1]; + swappedData[2*i+1] = m_BakedData[2*i+0]; + } + } + else + { + // RAW Substance textures = 4Bpp : 32b byte-swap needed + for (int i=0 ; i<size/4 ; ++i) + { + swappedData[4*i+0] = m_BakedData[4*i+3]; + swappedData[4*i+1] = m_BakedData[4*i+2]; + swappedData[4*i+2] = m_BakedData[4*i+1]; + swappedData[4*i+3] = m_BakedData[4*i+0]; + } + } + TRANSFER( swappedData ); + } + else + { + TRANSFER( m_BakedData ); + } + + TRANSFER( m_BakedParameters ); + transfer.Transfer( m_UsageMode, "m_LightmapFormat"); + transfer.Transfer( m_ColorSpace, "m_ColorSpace"); +} diff --git a/Runtime/Graphics/ProceduralTexture.h b/Runtime/Graphics/ProceduralTexture.h new file mode 100644 index 0000000..94740a7 --- /dev/null +++ b/Runtime/Graphics/ProceduralTexture.h @@ -0,0 +1,190 @@ +#pragma once + +#include "Configuration/UnityConfigure.h" +#include "Runtime/Utilities/dynamic_array.h" +#include "Texture.h" +#include "External/Allegorithmic/builds/Engines/include/substance/handle.h" + +class ColorRGBAf; +class ColorRGBA32; +class ProceduralMaterial; + +enum ProceduralOutputType +{ + Substance_OType_Unknown = 0, + Substance_OType_Diffuse, + Substance_OType_Normal, + Substance_OType_Height, + Substance_OType_Emissive, + Substance_OType_Specular, + Substance_OType_Opacity, + ProceduralOutputType_Count +}; + +enum SubstanceOutputFormat +{ + Substance_OFormat_Compressed = 0, + Substance_OFormat_Raw +}; + +/* A ProceduralTexture is a dynamic texture generated by the Substance engine. + * It's part of a Substance graph hierarchy, compound of ProceduralMaterial, SubstanceArchive. + */ + + +class ProceduralTexture : public Texture +{ +public: + + // Uploaded texture parameters + struct TextureParameters + { + DECLARE_SERIALIZE(TextureParameters) + + int width; + int height; + int mipLevels; + TextureFormat textureFormat; + + bool IsValid () const { return width != 0; } + friend bool operator == (const TextureParameters& lhs, const TextureParameters& rhs) + { + return lhs.width == rhs.width && lhs.height == rhs.height && lhs.mipLevels == rhs.mipLevels && lhs.textureFormat == rhs.textureFormat; + } + + TextureParameters (); + TextureParameters (int inWidth, int inHeight, int inMipLevels, TextureFormat inFormat); + + void Invalidate() { *this = TextureParameters(); } + }; + + // Flags + enum Flag + { + Flag_Binded = 1<<0, // the output is altered by an input + Flag_Uploaded = 1<<1, // the data has been uploaded + Flag_AwakeClone = 1<<2, // the texture need awake from clone + Flag_Cached = 1<<3 // the texture has been cached, editor flag only + }; + + // Upload State + enum UploadState + { + UploadState_None, // no data has been uploaded + UploadState_Waiting, // blue waiting texture + UploadState_Valid, // lower states indicate the current uploaded data isn't valid + UploadState_Baked, // baked texture + UploadState_Generated // generated texture + }; + +protected: // FIELDS + + PPtr<ProceduralMaterial> m_SubstanceMaterial; // The parent procedural material from which we get generated + ProceduralMaterial* m_PingedMaterial; // The pinged procedural material + UInt64 m_SubstanceTextureUID; // The index of this texture (i.e. Substance output) WORD[shuffledID,baseID] + unsigned int m_SubstancePreShuffleUID; // This ID is required to use substanceLinkerHandleSelectOutputs + ProceduralOutputType m_Type; + ProceduralOutputType m_AlphaSource; + unsigned int m_Flags; + UploadState m_UploadState; + SubstanceOutputFormat m_Format; + TextureFormat m_SubstanceFormat; // Platform dependant format required for substance generation + TextureParameters m_TextureParameters; + std::vector<UInt8> m_BakedData; // Baked texture if substance isn't supported + TextureParameters m_BakedParameters; + TextureID m_ClonedID; // Link to the source ID when cloned and not generated + +public: // METHODS + + REGISTER_DERIVED_CLASS( ProceduralTexture, Texture ) + DECLARE_OBJECT_SERIALIZE( ProceduralTexture ) + + ProceduralTexture( MemLabelId label, ObjectCreationMode mode ); + +#if ENABLE_SUBSTANCE + ProceduralTexture* Clone(ProceduralMaterial* owner); + void AwakeClone(); +#endif + + virtual int GetDataWidth() const { return m_TextureParameters.width; } + virtual int GetDataHeight() const { return m_TextureParameters.height; } + TextureFormat GetDataFormat() const { return m_TextureParameters.textureFormat; } + int GetDataMipLevels() const { return m_TextureParameters.mipLevels; } + virtual TextureDimension GetDimension () const { return kTexDim2D; } + virtual bool HasMipMap () const { return m_TextureParameters.mipLevels != 1; } + virtual int CountMipmaps() const { return m_TextureParameters.mipLevels; } + + const ProceduralOutputType& GetType() const { return m_Type; } + const ProceduralOutputType& GetAlphaSource() const { return m_AlphaSource; } + bool IsFlagEnabled(const Flag& flag) const { return m_Flags & (unsigned int)flag; } + void EnableFlag(const Flag& flag, bool enabled=true) { if (enabled) m_Flags |= (unsigned int)flag; else m_Flags &= ~(unsigned int)flag; } + +#if ENABLE_PROFILER + virtual int GetStorageMemorySize() const { return m_TextureParameters.width * m_TextureParameters.height; } +#endif + virtual bool ExtractImage (ImageReference* image, int imageIndex = 0) const { return false; } + + virtual void UnloadFromGfxDevice(bool forceUnloadAll); + virtual void UploadToGfxDevice(); + void RemoveTexture (); + void UploadWaitingTexture (); + void UploadBakedTexture (); + + // Gets the output texture UID (i.e. output UID for the Substance engine) + unsigned int GetSubstanceTextureUID() const { return m_SubstanceTextureUID & 0xffffffff; } + unsigned int GetSubstanceBaseTextureUID() const { return (m_SubstanceTextureUID >> 32); } + + void SetSubstancePreShuffleUID(unsigned int id) { m_SubstancePreShuffleUID = id; } + unsigned int GetSubstancePreShuffleUID() const { return m_SubstancePreShuffleUID; } + + // Set the UIDs + // (Don't call externally though !) + void SetSubstanceShuffledUID(unsigned int textureUID); + void SetSubstanceFormat(TextureFormat format) { m_SubstanceFormat = format; } + TextureFormat GetSubstanceFormat() const { return m_SubstanceFormat; } + + virtual void AwakeFromLoadThreaded(); + virtual void AwakeFromLoad( AwakeFromLoadMode awakeMode ); + + virtual bool ShouldIgnoreInGarbageDependencyTracking () { return false; } + + // (Don't call externally though !) + void UploadSubstanceTexture(SubstanceTexture& outputTexture); + + bool HasBeenGenerated() const { return m_UploadState==UploadState_Generated; } + bool HasBeenUploaded() const { return (m_UploadState==UploadState_Generated) || (m_UploadState==UploadState_Baked); } + +#if UNITY_EDITOR + // Creates the texture (one time call only by importer) + void Init( ProceduralMaterial& _Parent, int _TextureIndex, ProceduralOutputType type, SubstanceOutputFormat format, ProceduralOutputType alphaSource, bool requireCompressed ); + const TextureParameters& GetTextureParameters() const { return m_TextureParameters; } + TextureParameters& GetTextureParameters() { return m_TextureParameters; } + + virtual TextureFormat GetEditorUITextureFormat () const { return m_TextureParameters.textureFormat; } +#endif + + bool IsBaked() const; + std::vector<UInt8>& GetBakedData() { return m_BakedData; } + const TextureParameters& GetBakedParameters() const { return m_BakedParameters; } + TextureParameters& GetBakedParameters() { return m_BakedParameters; } + + bool GetPixels32(int x, int y, int width, int height, ColorRGBA32* data); + + bool IsValid() { return m_TextureParameters.IsValid(); } + void Invalidate(); + + void SetOwner(ProceduralMaterial* material); + ProceduralMaterial* GetSubstanceMaterial() { return m_PingedMaterial; } +#if UNITY_EDITOR + PPtr<ProceduralMaterial>& GetSubstanceMaterialPtr() { return m_SubstanceMaterial; } +#endif +}; + +template<class T> +void ProceduralTexture::TextureParameters::Transfer(T& transfer) +{ + TRANSFER(width); + TRANSFER(height); + TRANSFER(mipLevels); + transfer.Transfer(reinterpret_cast<int&> (textureFormat), "textureFormat"); +} diff --git a/Runtime/Graphics/RenderBufferManager.cpp b/Runtime/Graphics/RenderBufferManager.cpp new file mode 100644 index 0000000..2b0f25a --- /dev/null +++ b/Runtime/Graphics/RenderBufferManager.cpp @@ -0,0 +1,251 @@ +#include "UnityPrefix.h" +#include "RenderBufferManager.h" +#include "Runtime/Camera/Camera.h" +#include "Runtime/Camera/CameraUtil.h" +#include "Runtime/Camera/RenderManager.h" +#include "Runtime/GfxDevice/GfxDevice.h" +#include "RenderTexture.h" +#include "Runtime/Utilities/BitUtility.h" +#include "Runtime/Profiler/Profiler.h" +#if UNITY_XENON + #include "PlatformDependent/Xbox360/Source/GfxDevice/TexturesXenon.h" +#elif UNITY_WII + #include "Runtime/Graphics/ScreenManager.h" +#endif + +#ifndef DEBUG_RB_MANAGER +#define DEBUG_RB_MANAGER 0 +#endif + +using namespace std; + +static RenderBufferManager* gRenderBufferManager = NULL; + +void RenderBufferManager::InitRenderBufferManager () +{ + Assert(gRenderBufferManager == NULL); + gRenderBufferManager = new RenderBufferManager(); +} + +void RenderBufferManager::CleanupRenderBufferManager () +{ + Assert(gRenderBufferManager != NULL); + delete gRenderBufferManager; + gRenderBufferManager = NULL; +} + +RenderBufferManager& GetRenderBufferManager () +{ + Assert(gRenderBufferManager != NULL); + return *gRenderBufferManager; +} + +RenderBufferManager* GetRenderBufferManagerPtr () +{ + return gRenderBufferManager; +} + + +static int CalcSize( int size, int parentSize ) +{ + switch (size) { + case RenderBufferManager::kFullSize: + return parentSize; + default: + #if DEBUGMODE + if( size <= 0 ) { + AssertString ("Invalid Temp Buffer size"); + return 128; + } + #endif + return size; + } +} + + +RenderTexture *RenderBufferManager::GetTempBuffer (int width, int height, DepthBufferFormat depthFormat, RenderTextureFormat colorFormat, UInt32 flags, RenderTextureReadWrite colorSpace, int antiAliasing) +{ + if( colorFormat == kRTFormatDefault ) + colorFormat = GetGfxDevice().GetDefaultRTFormat(); + + if( colorFormat == kRTFormatDefaultHDR ) + colorFormat = GetGfxDevice().GetDefaultHDRRTFormat(); + + bool sRGB = colorSpace == kRTReadWriteSRGB; + const bool createdFromScript = flags & kRBCreatedFromScript; + const bool sampleOnlyDepth = flags & kRBSampleOnlyDepth; + const TextureDimension dim = (flags & kRBCubemap) ? kTexDimCUBE : kTexDim2D; + + if (colorSpace == kRTReadWriteDefault) + sRGB = GetActiveColorSpace() == kLinearColorSpace; + + // only SRGB where it makes sense + sRGB = sRGB && (colorFormat != GetGfxDevice().GetDefaultHDRRTFormat()); + + if( width <= 0 || height <= 0 ) + { + if (dim != kTexDim2D) { + AssertString( "Trying to get a relatively sized RenderBuffer cubemap" ); + return NULL; + } + Camera *cam = GetCurrentCameraPtr(); + if (cam == NULL) { + AssertString ("Trying to get a relatively sized RenderBuffer without an active camera."); + return NULL; + } + Rectf r = cam->GetScreenViewportRect(); +#if UNITY_WII + GetScreenManager().ScaleViewportToFrameBuffer(r); +#endif + // Figure out pixel size. Get screen extents as ints so we do the rounding correctly. + int viewport[4]; + RectfToViewport(r, viewport); + width = viewport[2]; + height = viewport[3]; + } + + if (dim == kTexDimCUBE && (!IsPowerOfTwo(width) || width != height)) + { + AssertString( "Trying to get a non square or non power of two RenderBuffer cubemap" ); + return NULL; + } + + if (antiAliasing < 1 || antiAliasing > 8 || !IsPowerOfTwo(antiAliasing)) + { + AssertString( "Trying to get RenderBuffer with invalid antiAliasing (must be 1, 2, 4 or 8)" ); + return NULL; + } + + // TODO: actually set & check depth + + // Go over free textures & find the one that matches in parameters. + // The main point is: If we used a texture of same dims last frame we'll get that. + FreeTextures::iterator found = m_FreeTextures.end(); + for( FreeTextures::iterator i = m_FreeTextures.begin(); i != m_FreeTextures.end(); ++i ) + { + RenderTexture* rt = i->second; + if( !rt + || rt->GetDepthFormat() != depthFormat + || rt->GetColorFormat() != colorFormat + || rt->GetDimension() != dim + //@TODO: Only matters on OSX as DX can just set sampler state... + || rt->GetSRGBReadWrite() != sRGB + || rt->GetAntiAliasing() != antiAliasing + || rt->GetSampleOnlyDepth() != sampleOnlyDepth ) + continue; + int tw = rt->GetWidth(); + int th = rt->GetHeight(); + + if (tw == width && th == height) { // If the texture is same size + found = i; + break; + } + } + + // We didn't find any. + if (found == m_FreeTextures.end() || !found->second) + { + m_TempBuffers++; + RenderTexture *tex = NEW_OBJECT (RenderTexture); + tex->Reset(); + + tex->SetHideFlags(Object::kDontSave); + tex->SetName (Format ("TempBuffer %d", m_TempBuffers).c_str()); + tex->SetWidth(width); + tex->SetHeight(height); + tex->SetColorFormat( colorFormat ); + tex->SetDepthFormat( depthFormat ); + tex->SetDimension (dim); + tex->SetSRGBReadWrite (sRGB); + tex->SetAntiAliasing (antiAliasing); + tex->SetSampleOnlyDepth (sampleOnlyDepth); + tex->AwakeFromLoad(kInstantiateOrCreateFromCodeAwakeFromLoad); + m_TakenTextures.insert (tex); + #if DEBUG_RB_MANAGER + printf_console ("RBM: new texture %ix%i fmt=%i\n", width, height, colorFormat); + #endif + return tex; + } + + // We found one. Move it from free to taken + RenderTexture *tex = found->second; + Assert (tex->GetWidth() == width && tex->GetHeight() == height); + m_TakenTextures.insert (tex); + m_FreeTextures.erase (found); + + // Set it's parameters (filtering etc.) as if it was newly created + tex->GetSettings().Reset(); + tex->GetSettings().m_WrapMode = kTexWrapClamp; + tex->ApplySettings(); + + tex->SetCreatedFromScript (createdFromScript); + + // Automatically DiscardContents when createdFromScript - user can't expect any valid content. + if (createdFromScript) + tex->DiscardContents(); + + return tex; +} + +PROFILER_INFORMATION(gRenderBufferCollect, "RenderTexture.GarbageCollectTemporary", kProfilerRender) + +void RenderBufferManager::GarbageCollect (int framesDelay) +{ + ++m_CurrentRBMFrame; + + for (FreeTextures::iterator i = m_FreeTextures.begin(); i != m_FreeTextures.end();) + { + // Should only ever compare the difference since frame wraps around + int frameDiff = m_CurrentRBMFrame - i->first; + if( frameDiff > framesDelay || frameDiff < 0 ) + { + PROFILER_AUTO(gRenderBufferCollect, NULL); + #if DEBUG_RB_MANAGER + printf_console ("RBM: kill unused texture (currframe=%i usedframe=%i)\n", m_CurrentRBMFrame, i->first); + #endif + + FreeTextures::iterator j = i; + i++; + DestroySingleObject(j->second); + m_FreeTextures.erase (j); + } + else + { + i++; + } + } +} + + +void RenderBufferManager::Cleanup () +{ + for (TakenTextures::iterator i=m_TakenTextures.begin();i != m_TakenTextures.end();i++) + { + DestroySingleObject(*i); + } + m_TakenTextures.clear(); + + for (FreeTextures::iterator i=m_FreeTextures.begin();i != m_FreeTextures.end();i++) + { + DestroySingleObject(i->second); + } + m_FreeTextures.clear(); + #if DEBUG_RB_MANAGER + printf_console( "RBM: destroy all textures\n" ); + #endif +} + +void RenderBufferManager::ReleaseTempBuffer (RenderTexture *rTex) +{ + if (!rTex) + return; + + if (!m_TakenTextures.count (PPtr<RenderTexture> (rTex))) + { + ErrorStringObject ("Attempting to release RenderTexture that were not gotten as a temp buffer", rTex); + return; + } + + m_TakenTextures.erase (PPtr<RenderTexture> (rTex)); + m_FreeTextures.push_back (make_pair (m_CurrentRBMFrame, PPtr<RenderTexture> (rTex))); +} diff --git a/Runtime/Graphics/RenderBufferManager.h b/Runtime/Graphics/RenderBufferManager.h new file mode 100644 index 0000000..5496acf --- /dev/null +++ b/Runtime/Graphics/RenderBufferManager.h @@ -0,0 +1,58 @@ +#ifndef RENDERBUFFERMANAGER_H +#define RENDERBUFFERMANAGER_H + +#include "Runtime/GfxDevice/GfxDeviceTypes.h" +#include "Runtime/Utilities/MemoryPool.h" +#include "Runtime/BaseClasses/BaseObject.h" + +class RenderTexture; + + +/* Manager for getting temporary render buffers. + * Use this instead of creating RenderTextures if you need a quick buffer. + * This is a low-overhead class that recycles render textures. + */ +class RenderBufferManager +{ + RenderBufferManager () { m_CurrentRBMFrame = m_TempBuffers = 0; } + +public: + + enum { kFullSize = -1 }; + enum { + kRBCubemap = (1<<0), + kRBCreatedFromScript = (1<<1), + kRBSampleOnlyDepth = (1<<2), + }; + enum { kKillFrames = 15 }; + + static void InitRenderBufferManager (); + static void CleanupRenderBufferManager (); + + // Get a RenderTexture with the specific sizes + // If the width & height parameters uses autosizing, viewport size is taken from the current camera. + RenderTexture *GetTempBuffer (int width, int height, DepthBufferFormat depthFormat, RenderTextureFormat colorFormat, UInt32 flags, RenderTextureReadWrite colorSpace, int antiAliasing = 1); + + // Release the temporary buffer. + void ReleaseTempBuffer (RenderTexture *rTex); + + void GarbageCollect (int framesDelay = kKillFrames); + + void Cleanup (); + +private: + + typedef std::set<PPtr<RenderTexture>, std::less< PPtr<RenderTexture> > , memory_pool<PPtr<RenderTexture> > > TakenTextures; + typedef std::pair <int, PPtr<RenderTexture> > IntPPtrPair; + typedef std::list<IntPPtrPair, memory_pool<IntPPtrPair > > FreeTextures; + + FreeTextures m_FreeTextures; + TakenTextures m_TakenTextures; + int m_TempBuffers; + int m_CurrentRBMFrame; +}; + +RenderBufferManager& GetRenderBufferManager (); +RenderBufferManager* GetRenderBufferManagerPtr (); + +#endif diff --git a/Runtime/Graphics/RenderSurface.h b/Runtime/Graphics/RenderSurface.h new file mode 100644 index 0000000..c856a39 --- /dev/null +++ b/Runtime/Graphics/RenderSurface.h @@ -0,0 +1,40 @@ +#pragma once + +#include "Runtime/GfxDevice/GfxDeviceTypes.h" + +struct +RenderSurfaceBase +{ + TextureID textureID; + int width; + int height; + int samples; + UInt32 flags; + bool colorSurface; + bool backBuffer; + bool shouldDiscard; + bool shouldClear; +}; + +// we dont want to enforce ctor, so lets do it as simple function +inline void RenderSurfaceBase_Init(RenderSurfaceBase& rs) +{ + rs.textureID.m_ID = 0; + rs.width = rs.height = 0; + rs.samples = 1; + rs.flags = 0; + rs.shouldDiscard = rs.shouldClear = false; + rs.backBuffer = false; +} + +inline void RenderSurfaceBase_InitColor(RenderSurfaceBase& rs) +{ + RenderSurfaceBase_Init(rs); + rs.colorSurface = true; +} + +inline void RenderSurfaceBase_InitDepth(RenderSurfaceBase& rs) +{ + RenderSurfaceBase_Init(rs); + rs.colorSurface = false; +} diff --git a/Runtime/Graphics/RenderTexture.cpp b/Runtime/Graphics/RenderTexture.cpp new file mode 100644 index 0000000..b47cf0f --- /dev/null +++ b/Runtime/Graphics/RenderTexture.cpp @@ -0,0 +1,889 @@ +#include "UnityPrefix.h" +#include "RenderTexture.h" +#if UNITY_XENON +#include "PlatformDependent/Xbox360/Source/GfxDevice/TexturesXenon.h" +#endif +#include "Runtime/Camera/RenderManager.h" +#include "Runtime/Shaders/GraphicsCaps.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "External/shaderlab/Library/properties.h" +#include "External/shaderlab/Library/texenv.h" +#include "Runtime/Misc/BuildSettings.h" +#include "Runtime/Utilities/BitUtility.h" +#include "Runtime/GfxDevice/GfxDevice.h" +#include "Runtime/Graphics/RenderSurface.h" +#include "Runtime/Graphics/Image.h" +#include "Runtime/Camera/CameraUtil.h" +#include "Runtime/Utilities/MemoryPool.h" +#include "Runtime/Misc/PlayerSettings.h" +#include "Runtime/Math/ColorSpaceConversion.h" +#include "Runtime/Profiler/Profiler.h" + +PROFILER_INFORMATION(gGrabPixels, "RenderTexture.GrabPixels", kProfilerRender); +PROFILER_INFORMATION(gSetRenderTargets, "RenderTexture.SetActive", kProfilerRender); + +static const int kRenderTextureFormatBPP[kRTFormatCount] = { + 4, // ARGB32 + 4, // Depth various amounts on various HW, but we'll assume 4 + 8, // ARGBHalf + 4, // Shadowmap various amounts on various HW, but we'll assume 4 + 2, // RGB565 + 2, // ARGB4444 + 2, // ARGB1555 + 4, // Default + 4, // A2R10G10B10 + 8, // DefaultHDR + 8, // ARGB64 + 16,// ARGBFloat + 8, // RGFloat + 4, // RGHalf + 4, // RFloat + 2, // RHalf + 1, // R8 + 16,// ARGBInt + 8, // RGInt + 4, // RInt + 4, // BGRA32 +}; + +static int kDepthFormatBPP[kDepthFormatCount] = { + 0, // None + 2, // 16 + 4, // 24 +}; + + +typedef List< ListNode<RenderTexture> > RenderTextureList; +RenderTextureList gRenderTextures; + +static bool gIsRenderTexEnabled = true; +int gTemporarilyAllowIndieRenderTextures = 0; + +void RenderTexture::SetTemporarilyAllowIndieRenderTexture (bool allow) +{ + if (allow) + gTemporarilyAllowIndieRenderTextures++; + else + gTemporarilyAllowIndieRenderTextures--; +} + +void RenderTexture::FindAndSetSRGBWrite( RenderTexture* newActive ) +{ + bool wantLinear = GetActiveColorSpace() == kLinearColorSpace; + GetGfxDevice().SetSRGBWrite(newActive ? newActive->GetSRGBReadWrite() && wantLinear : wantLinear); +} + +void RenderTexture::SetActive (RenderTexture* newActive, int mipLevel, CubemapFace face, UInt32 flags) +{ + newActive = EnsureRenderTextureIsCreated(newActive); + + RenderSurfaceHandle newColorSurface = newActive ? newActive->m_ColorHandle : GetGfxDevice().GetBackBufferColorSurface(); + RenderSurfaceHandle newDepthSurface = newActive ? newActive->m_DepthHandle : GetGfxDevice().GetBackBufferDepthSurface(); + int mips = newActive && newActive->HasMipMap() ? mipLevel : 0; + + SetActive (1, &newColorSurface, newDepthSurface, newActive, mips, face, flags); +} + +bool RenderTexture::SetActive (int count, RenderSurfaceHandle* newColorSurfaces, RenderSurfaceHandle newDepthSurface, RenderTexture* rt, int mipLevel, CubemapFace face, UInt32 flags) +{ + DebugAssert(count > 0); + if( !IsEnabled() ) + { + for (int i = 0; i < count; ++i) + newColorSurfaces[i].Reset(); + + // ??? should we keep them invalid just in case? + newColorSurfaces[0] = GetGfxDevice().GetBackBufferColorSurface(); + newDepthSurface = GetGfxDevice().GetBackBufferDepthSurface(); + } + + int width = 1, height = 1; + if(newColorSurfaces[0].IsValid() && !newColorSurfaces[0].object->backBuffer) + { + width = newColorSurfaces[0].object->width; + height= newColorSurfaces[0].object->height; + } + + // Clamp mip level to valid range + const int mipCount = CalculateMipMapCount3D(width, height, 1); + mipLevel = clamp(mipLevel, 0, mipCount-1); + + GfxDevice& device = GetGfxDevice(); + RenderSurfaceHandle oldColorSurface = device.GetActiveRenderColorSurface(0); + RenderSurfaceHandle oldDepthSurface = device.GetActiveRenderDepthSurface(); + + Assert(newColorSurfaces[0].IsValid() == newDepthSurface.IsValid()); + bool renderTarget = newColorSurfaces[0].IsValid() && !newColorSurfaces[0].object->backBuffer; + + if( oldColorSurface != newColorSurfaces[0] || (flags & kFlagForceResolve) ) + { + // MSAA surface has to be resolved + RenderTexture* oldRt = GetActive(); + if( oldRt && oldRt->IsAntiAliased() ) + oldRt->ResolveAntiAliasedSurface(); + } + + UInt32 rtFlags = 0; + rtFlags |= (flags & kFlagDontRestoreColor) ? GfxDevice::kFlagDontRestoreColor : 0; + rtFlags |= (flags & kFlagDontRestoreDepth) ? GfxDevice::kFlagDontRestoreDepth : 0; + rtFlags |= (flags & kFlagForceResolve) ? GfxDevice::kFlagForceResolve : 0; + + PROFILER_AUTO(gSetRenderTargets, rt); + device.SetRenderTargets (count, newColorSurfaces, newDepthSurface, mipLevel, face, rtFlags); + GPU_TIMESTAMP(); + + // we can call RenderTexture::SetActive before device init (d3d11) + if( oldColorSurface == newColorSurfaces[0] && oldDepthSurface == newDepthSurface + && newColorSurfaces[0].IsValid() && !newColorSurfaces[0].object->backBuffer + ) + { + return false; + } + + + // NOTE: important to set ActiveRenderTexture to null/non-null value before setting up viewport (as that might involve + // flipping vertically based on whether there is a RT or not). + GetGfxDevice().SetActiveRenderTexture(rt); + + // Setup the viewport for the render texture + if( !(flags & kFlagDontSetViewport) ) + { + if (renderTarget) + { + width >>= mipLevel; + height >>= mipLevel; + device.SetViewport (0, 0, width, height); + } + else + { + // When switching to main window, restore the viewport to the one of the current + // camera. + const int* cameraViewPort = GetRenderManager().GetCurrentViewPort(); + int viewcoord[4]; + viewcoord[0] = cameraViewPort[0]; + viewcoord[1] = cameraViewPort[1]; + viewcoord[2] = cameraViewPort[2]; + viewcoord[3] = cameraViewPort[3]; + + // Flip viewport just before applying to device, but don't store it flipped in the render manager + FlipScreenRectIfNeeded( device, viewcoord ); + device.SetViewport( viewcoord[0], viewcoord[1], viewcoord[2], viewcoord[3] ); + } + } + + if (renderTarget) + { + // If texture coordinates go from top to bottom, then we need to + // invert projection matrix when rendering into a texture. + #if UNITY_WII + device.SetInvertProjectionMatrix( true ); + #else + device.SetInvertProjectionMatrix( !device.UsesOpenGLTextureCoords() ); + #endif + } + else + { + device.SetInvertProjectionMatrix( false ); + } + + return true; +} + + +bool RenderTexture::GetSupportedMipMapFlag(bool mipMap) const +{ + if (!gGraphicsCaps.hasAutoMipMapGeneration) + mipMap = false; + if (m_Dimension == kTexDimCUBE && gGraphicsCaps.buggyMipmappedCubemaps) + mipMap = false; + if (m_Dimension == kTexDim3D && gGraphicsCaps.buggyMipmapped3DTextures) + mipMap = false; + return mipMap; +} + + +bool RenderTexture::Create () +{ + if( !IsEnabled() ) + return false; + + DestroySurfaces(); + GfxDevice& device = GetGfxDevice(); + + if (m_Width <= 0 || m_Height <= 0) + { + ErrorString("RenderTexture.Create failed: width & height must be larger than 0"); + return false; + } + + if (m_Dimension == kTexDimCUBE && (!GetIsPowerOfTwo() || m_Width != m_Height)) + { + ErrorString("RenderTexture.Create failed: cube maps must be power of two and width must match height"); + return false; + } + + if( !device.IsRenderTargetConfigValid(m_Width, m_Height, static_cast<RenderTextureFormat>(m_ColorFormat), static_cast<DepthBufferFormat>(m_DepthFormat)) ) + { + if( GetIsPowerOfTwo() ) + { + if (gGraphicsCaps.maxRenderTextureSize < 4) + { + ErrorString("RenderTexture.Create failed: maxRenderTextureSize is too small"); + return false; + } + + // Decrease too large render textures while maintaining aspect ratio + do + { + m_Width = std::max( m_Width / 2, 4 ); + m_Height = std::max( m_Height / 2, 4 ); + } + while ( !device.IsRenderTargetConfigValid(m_Width, m_Height, static_cast<RenderTextureFormat>(m_ColorFormat), static_cast<DepthBufferFormat>(m_DepthFormat)) ); + } + else + { + ErrorString("RenderTexture.Create failed: requested size is too large."); + return false; + } + } + + if( !gGraphicsCaps.supportsRenderTextureFormat[m_ColorFormat] ) + { + ErrorString("RenderTexture.Create failed: format unsupported."); + return false; + } + + if (!GetIsPowerOfTwo() && (gGraphicsCaps.npotRT == kNPOTNone)) + { + ErrorString("RenderTexture.Create failed: non-power-of-two sizes not supported."); + return false; + } + + if (m_Dimension == kTexDimCUBE && (!gGraphicsCaps.hasRenderToCubemap || IsDepthRTFormat((RenderTextureFormat)m_ColorFormat))) + { + ErrorString("RenderTexture.Create failed: cubemap not supported."); + return false; + } + + if (m_Dimension == kTexDim3D && (!gGraphicsCaps.has3DTexture || !gGraphicsCaps.hasRenderTo3D)) + { + ErrorString("RenderTexture.Create failed: volume texture not supported."); + return false; + } + + + bool mipMap = GetSupportedMipMapFlag(m_MipMap); + if (!GetIsPowerOfTwo()) + mipMap = false; + + int samples = m_AntiAliasing; + samples = clamp<int>(samples, 1, 8); + if (!gGraphicsCaps.hasMultiSample) + samples = 1; + if (m_Dimension != kTexDim2D) + samples = 1; + + if (samples > 1) + mipMap = false; + + // If we have native depth textures, we should use our regular textureID for the depth + // surface. + TextureID colorTID, resolvedColorTID, depthTID; + if ((m_ColorFormat == kRTFormatDepth && gGraphicsCaps.hasNativeDepthTexture) || + (m_ColorFormat == kRTFormatShadowMap && gGraphicsCaps.hasNativeShadowMap)) + { + depthTID = m_TexID; + m_SecondaryTexIDUsed = false; + } + else + { + // Use resolved surface as texture if MSAA enabled + if (samples > 1 && !gGraphicsCaps.hasMultiSampleAutoResolve) + resolvedColorTID = m_TexID; + else + colorTID = m_TexID; + // For regular non-MSAA render textures, also provide capability to treat depth buffer as a texture. + // Only do this if HW supports native depth textures AND texture is 2D AND + // we actually have a depth buffer AND driver is not broken for this case. + bool useDepthAsTexture = false; + if (m_Dimension == kTexDim2D && m_DepthFormat != kDepthFormatNone && samples <= 1) + useDepthAsTexture = gGraphicsCaps.hasStencilInDepthTexture && !gGraphicsCaps.buggyTextureBothColorAndDepth; + if (useDepthAsTexture) + { + depthTID = m_SecondaryTexID; + m_SecondaryTexIDUsed = true; + } + else + m_SecondaryTexIDUsed = false; + } + + UInt32 colorFlags = 0; + if (mipMap) colorFlags |= kSurfaceCreateMipmap; + if (m_GenerateMips) colorFlags |= kSurfaceCreateAutoGenMips; + if (m_SRGB) colorFlags |= kSurfaceCreateSRGB; + if (m_EnableRandomWrite) colorFlags |= kSurfaceCreateRandomWrite; + if (colorTID == TextureID() && samples <= 1) colorFlags |= kSurfaceCreateNeverUsed; // never sampled or resolved + m_ColorHandle = device.CreateRenderColorSurface (colorTID, + m_Width, m_Height, samples, m_VolumeDepth, m_Dimension, static_cast<RenderTextureFormat>(m_ColorFormat), colorFlags); + + if (samples > 1 && !gGraphicsCaps.hasMultiSampleAutoResolve) + { + m_ResolvedColorHandle = device.CreateRenderColorSurface (resolvedColorTID, + m_Width, m_Height, 1, m_VolumeDepth, m_Dimension, static_cast<RenderTextureFormat>(m_ColorFormat), colorFlags); + } + + UInt32 depthFlags = 0; + if (m_ColorFormat==kRTFormatShadowMap) depthFlags |= kSurfaceCreateShadowmap; + if (m_SampleOnlyDepth) depthFlags |= kSurfaceCreateSampleOnly; + m_DepthHandle = device.CreateRenderDepthSurface (depthTID, + m_Width, m_Height, samples, m_Dimension, static_cast<DepthBufferFormat>(m_DepthFormat), depthFlags); + + if (!(m_ColorHandle.IsValid() || m_DepthHandle.IsValid())) + { + ErrorString("RenderTexture.Create failed"); + return false; + } + + if (IsCreated()) + { + Assert(m_RegisteredSizeForStats == 0); + m_RegisteredSizeForStats = GetRuntimeMemorySize(); + device.GetFrameStats().ChangeRenderTextureBytes (m_RegisteredSizeForStats); + } + + if (m_CreatedFromScript) + { + // We can't currently read the minds of users, so let's always restore their render targets. + // TODO: extend RenderTexture API + device.SetSurfaceFlags(m_ColorHandle, GfxDevice::kSurfaceAlwaysRestore, ~GfxDevice::kSurfaceRestoreMask); + device.SetSurfaceFlags(m_DepthHandle, GfxDevice::kSurfaceAlwaysRestore, ~GfxDevice::kSurfaceRestoreMask); + } + + SetStoredColorSpaceNoDirtyNoApply(m_SRGB ? kTexColorSpaceSRGB : kTexColorSpaceLinear); + + // Set filtering, etc. mode + ApplySettings(); + + UpdateTexelSize(); + + return true; +} + +void RenderTexture::UpdateTexelSize() +{ + SetUVScale( 1.0f, 1.0f ); + if (m_Width != 0 && m_Height != 0) + { + int width = m_Width; + int height = m_Height; + #if GFX_EMULATES_NPOT_RENDERTEXTURES + width = NextPowerOfTwo (width); + height = NextPowerOfTwo (height); + #endif + SetTexelSize( 1.0f / width, 1.0f / height ); + } +} + + +void RenderTexture::ApplySettings() +{ + TextureDimension dim = GetDimension(); + bool mip = HasMipMap(); + // Don't ever use Aniso on depth textures or textures where we + // sample depth buffer. + bool depth = IsDepthRTFormat((RenderTextureFormat)m_ColorFormat); + if (depth || m_SecondaryTexIDUsed) + m_TextureSettings.m_Aniso = 0; + + m_TextureSettings.Apply (GetTextureID(), dim, mip, GetActiveTextureColorSpace()); + if (m_SecondaryTexIDUsed) + m_TextureSettings.Apply (m_SecondaryTexID, dim, mip, GetActiveTextureColorSpace()); + NotifyMipBiasChanged(); +} + + +void RenderTexture::Release() +{ + if (GetGfxDevice().GetActiveRenderTexture() == this) + { + ErrorString("Releasing render texture that is set to be RenderTexture.active!"); + GetGfxDevice().SetActiveRenderTexture(NULL); + } + DestroySurfaces(); +} + +void RenderTexture::DestroySurfaces() +{ + if(!IsCreated()) + return; + + GfxDevice& device = GetGfxDevice(); + // Different graphics caps can change the calculated runtime size (case 555046) + // so we store the size we added and make sure to subtract the same amount. + // Graphics caps can change during an editor session due to emulation. + device.GetFrameStats().ChangeRenderTextureBytes (-m_RegisteredSizeForStats); + m_RegisteredSizeForStats = 0; + if( m_ColorHandle.IsValid() ) + device.DestroyRenderSurface (m_ColorHandle); + if( m_ResolvedColorHandle.IsValid() ) + device.DestroyRenderSurface (m_ResolvedColorHandle); + if( m_DepthHandle.IsValid() ) + device.DestroyRenderSurface (m_DepthHandle); +} + +void RenderTexture::DiscardContents() +{ + if(IsCreated()) + RenderTextureDiscardContents(this, true, true); + +} +void RenderTexture::DiscardContents(bool discardColor, bool discardDepth) +{ + if(IsCreated()) + RenderTextureDiscardContents(this, discardColor, discardDepth); +} + + +void RenderTexture::MarkRestoreExpected() +{ + GfxDevice& device = GetGfxDevice(); + device.IgnoreNextUnresolveOnRS (m_ColorHandle); + device.IgnoreNextUnresolveOnRS (m_DepthHandle); + device.IgnoreNextUnresolveOnRS (m_ResolvedColorHandle); +} + + +void RenderTexture::GrabPixels (int left, int bottom, int width, int height) +{ + if (!IsCreated()) + Create(); + + const RenderSurfaceHandle& handle = IsAntiAliased() ? m_ResolvedColorHandle : m_ColorHandle; + + if (!handle.IsValid()) + return; + + if (left < 0) { + width += left; + left = 0; + } + if (bottom < 0) { + height += bottom; + bottom = 0; + } + if (width > m_Width) + width = m_Width; + if (height > m_Height) + height = m_Height; + + PROFILER_AUTO(gGrabPixels, NULL); + GfxDevice& device = GetGfxDevice(); + device.GrabIntoRenderTexture (handle, m_DepthHandle, left, bottom, width, height); + GPU_TIMESTAMP(); + device.GetFrameStats().AddRenderTextureChange(); // treat resolve as RT change +} +void RenderTexture::CorrectVerticalTexelSize(bool shouldBePositive) +{ + if (!GetGfxDevice().UsesOpenGLTextureCoords()) + { + if (m_TexelSizeY < 0.0f && shouldBePositive) m_TexelSizeY = -m_TexelSizeY; + else if (m_TexelSizeY > 0.0f && !shouldBePositive) m_TexelSizeY = -m_TexelSizeY; + } +} +RenderTexture::RenderTexture (MemLabelId label, ObjectCreationMode mode) +: Super(label, mode) +, m_DepthFormat(kDepthFormat24) +, m_ColorFormat(kRTFormatARGB32) +, m_Dimension(kTexDim2D) +, m_MipMap(false) +, m_GenerateMips(true) +, m_SRGB(false) +, m_EnableRandomWrite(false) +, m_CreatedFromScript(false) +, m_SampleOnlyDepth(false) +, m_RegisteredSizeForStats(0) +, m_RenderTexturesNode(this) +{ + m_Width = 256; + m_Height = 256; + m_VolumeDepth = 1; + m_AntiAliasing = 1; + + GetSettings().m_WrapMode = kTexWrapClamp; + + // We use unchecked version since we may not be on the main thread + // This means CreateTextureID() implementation must be thread safe! + m_SecondaryTexID = GetUncheckedGfxDevice().CreateTextureID(); + m_SecondaryTexIDUsed = false; +} + +RenderTexture::~RenderTexture () +{ + Release(); + m_RenderTexturesNode.RemoveFromList(); + Texture::s_TextureIDMap.erase (m_SecondaryTexID); + // FreeTextureID() implementation must be thread safe! + GetUncheckedGfxDevice().FreeTextureID(m_SecondaryTexID); +} + +void RenderTexture::SetDimension (TextureDimension dim) +{ + if (m_Dimension == dim) + return; + + if (!IsCreated ()) + m_Dimension = dim; + else + ErrorString ("Setting dimension of already created render texture is not supported!"); +} + +void RenderTexture::SetVolumeDepth (int v) +{ + if (m_VolumeDepth == v) + return; + + if (!IsCreated ()) { + m_VolumeDepth = v; + } else + ErrorString ("Setting volume depth of already created render texture is not supported!"); +} + +void RenderTexture::SetAntiAliasing (int aa) +{ + if (m_AntiAliasing == aa) + return; + + if (aa < 1 || aa > 8 || !IsPowerOfTwo(aa)) + { + ErrorString( "Invalid antiAliasing value (must be 1, 2, 4 or 8)" ); + return; + } + + if (!IsCreated ()) { + m_AntiAliasing = aa; + } else + ErrorString ("Setting anti-aliasing of already created render texture is not supported!"); +} + + +void RenderTexture::SetMipMap( bool mipMap ) +{ + mipMap = GetSupportedMipMapFlag(mipMap); + + if (mipMap == m_MipMap) + return; + + if (!IsCreated ()) { + m_MipMap = mipMap; + } else { + ErrorString ("Setting mipmap mode of render texture that is loaded not supported!"); + } +} + +void RenderTexture::SetGenerateMips (bool v) +{ + if (v == m_GenerateMips) + return; + + if (!IsCreated ()) + { + if (m_MipMap && m_DepthFormat != kDepthFormatNone && !v) + { + WarningStringObject ("Mipmapped RenderTextures with manual mip generation can't have depth buffer", this); + v = true; + } + m_GenerateMips = v; + } + else + ErrorString ("Can't change mipmap generation of already created RenderTexture"); +} + +void RenderTexture::SetSRGBReadWrite ( bool sRGB ) +{ + if (!IsCreated ()) { + bool setSRGB = sRGB ? (GetActiveColorSpace() == kLinearColorSpace) : false; + setSRGB = setSRGB && (m_ColorFormat != GetGfxDevice().GetDefaultHDRRTFormat()); + m_SRGB = setSRGB; + } else + ErrorString ("Changing srgb mode of render texture that is loaded not supported!"); +} + +void RenderTexture::SetEnableRandomWrite (bool v) +{ + if (v == m_EnableRandomWrite) + return; + + if (!IsCreated ()) { + m_EnableRandomWrite = v; + } else + ErrorString ("Can't change random write mode of already created render texture!"); +} + +void RenderTexture::AwakeFromLoad(AwakeFromLoadMode awakeMode) +{ + m_Width = std::max (1, m_Width); + m_Height = std::max (1, m_Height); + m_VolumeDepth = std::max (1, m_VolumeDepth); + m_AntiAliasing = clamp<int> (m_AntiAliasing, 1, 8); + + if( IsDepthRTFormat( (RenderTextureFormat)m_ColorFormat ) ) + m_MipMap = false; + if (m_Dimension==kTexDimCUBE) + m_Height = m_Width; + + if( !GetIsPowerOfTwo() && GetSettings().m_WrapMode == kTexWrapRepeat ) + GetSettings().m_WrapMode = kTexWrapClamp; + + if( IsDepthRTFormat((RenderTextureFormat)m_ColorFormat) ) // always clamp depth textures + GetSettings().m_WrapMode = kTexWrapClamp; + +#if GFX_OPENGLESxx_ONLY + // currently i have no idea about possibility of linear filter for fp rts (seems like gl do support it) + if( IsHalfRTFormat((RenderTextureFormat)m_ColorFormat) && !gGraphicsCaps.gles20.hasHalfLinearFilter ) + GetSettings().m_FilterMode = kTexFilterNearest; +#endif + + gRenderTextures.push_back(m_RenderTexturesNode); + + UpdateTexelSize(); + + Super::AwakeFromLoad (awakeMode); +} + +void RenderTexture::SetWidth (int width) +{ + if (!IsCreated ()) { + m_Width = width; + UpdateTexelSize(); + SetDirty(); + } else + ErrorString ("Resizing of render texture that is loaded not supported!"); +} + +void RenderTexture::SetHeight (int height) +{ + if (!IsCreated ()) { + m_Height = height; + UpdateTexelSize(); + SetDirty(); + } else + ErrorString ("Resizing of render texture that is loaded not supported!"); +} + +void RenderTexture::SetDepthFormat( DepthBufferFormat depth ) +{ + if (!IsCreated ()) + m_DepthFormat = depth; + else + ErrorString ("Setting depth of render texture that is loaded not supported!"); +} + +void RenderTexture::SetColorFormat( RenderTextureFormat format ) +{ + if (!IsCreated ()) + { + if( format == kRTFormatDefault ) + format = GetGfxDevice().GetDefaultRTFormat(); + + m_ColorFormat = format; + if (IsDepthRTFormat(format)) m_TextureSettings.m_Aniso = 0; // never use Anisotropic on depth textures + } else + ErrorString ("Changing format of render texture that is loaded not supported!"); +} + + +RenderTexture* RenderTexture::GetActive () +{ + return GetGfxDevice().GetActiveRenderTexture(); +} + +ShaderLab::TexEnv *RenderTexture::SetGlobalProperty (const ShaderLab::FastPropertyName& name) +{ + ShaderLab::TexEnv *te = ShaderLab::g_GlobalProperties->SetTexture (name, this); + te->ClearMatrix(); + return te; +} + +bool RenderTexture::IsEnabled () +{ + if (gGraphicsCaps.hasRenderToTexture) + return gIsRenderTexEnabled && (GetBuildSettings().hasRenderTexture || gTemporarilyAllowIndieRenderTextures); + else + return false; +} + +void RenderTexture::SetEnabled (bool enable) +{ + if (!enable) + ReleaseAll(); + + gIsRenderTexEnabled = enable; +} + +void RenderTexture::ReleaseAll () +{ + SetActive (NULL); + + for (RenderTextureList::iterator i=gRenderTextures.begin ();i != gRenderTextures.end ();i++) + (**i).Release (); +} + + +#if ENABLE_PROFILER || UNITY_EDITOR + +int RenderTexture::GetCreatedRenderTextureCount () +{ + int count = 0; + for (RenderTextureList::iterator i=gRenderTextures.begin ();i != gRenderTextures.end ();i++) + { + if ((**i).IsCreated ()) + count++; + } + + return count; +} + +int RenderTexture::GetCreatedRenderTextureBytes () +{ + int count = 0; + for (RenderTextureList::iterator i=gRenderTextures.begin ();i != gRenderTextures.end ();i++) + { + if ((**i).IsCreated ()) + count += (**i).GetRuntimeMemorySize(); + } + + return count; +} + +#endif + + + +int EstimateRenderTextureSize (int width, int height, int depth, RenderTextureFormat format, DepthBufferFormat depthFormat, TextureDimension dim, bool mipMap) +{ + // color buffer + int colorBPP; + if (format == kRTFormatDepth && gGraphicsCaps.hasNativeDepthTexture) + colorBPP = 0; + else if (format == kRTFormatShadowMap && gGraphicsCaps.hasNativeShadowMap) + colorBPP = 0; + else + colorBPP = kRenderTextureFormatBPP[format]; + int size = width * height * colorBPP; + + if (dim == kTexDim3D) + size *= depth; + else if (dim == kTexDimCUBE) + size *= 6; + if (mipMap && gGraphicsCaps.hasAutoMipMapGeneration) + size += size / 3; + + // depth buffer + size += width * height * kDepthFormatBPP[depthFormat]; + return size; +} + + + +int RenderTexture::GetRuntimeMemorySize() const +{ + int bytes = EstimateRenderTextureSize (m_Width, m_Height, m_VolumeDepth, (RenderTextureFormat)m_ColorFormat, (DepthBufferFormat)m_DepthFormat, m_Dimension, m_MipMap); + bytes *= m_AntiAliasing; + return bytes;// + Super::GetRuntimeMemorySize(); Since the name is assigned after this value is used for gfxstats accumulate, the string will add to the mem usage later - Causes Assert +} + +void RenderTexture::ResolveAntiAliasedSurface() +{ + if (!m_ResolvedColorHandle.IsValid()) + return; + + GfxDevice& device = GetGfxDevice(); + device.ResolveColorSurface(m_ColorHandle, m_ResolvedColorHandle); +} + +RenderTexture* EnsureRenderTextureIsCreated(RenderTexture* tex) +{ + RenderTexture* ret = tex; + + if(!RenderTexture::IsEnabled()) + ret = 0; + + if(ret && !ret->IsCreated()) + ret->Create(); + + // check if we could create + if(ret && !ret->IsCreated()) + ret = 0; + + return ret; +} + +template<class TransferFunction> +void RenderTexture::Transfer (TransferFunction& transfer) +{ + Super::Transfer (transfer); + TRANSFER (m_Width); + TRANSFER (m_Height); + TRANSFER (m_AntiAliasing); + TRANSFER (m_DepthFormat); + TRANSFER (m_ColorFormat); + TRANSFER (m_MipMap); + TRANSFER (m_GenerateMips); + TRANSFER (m_SRGB); + transfer.Align(); + TRANSFER (m_TextureSettings); +} + +IMPLEMENT_CLASS (RenderTexture) +IMPLEMENT_OBJECT_SERIALIZE (RenderTexture) + +bool RenderTextureSupportsStencil (RenderTexture* rt) +{ + if(!(gGraphicsCaps.hasStencil && GetBuildSettings ().hasAdvancedVersion)) + return false; + + if (rt) + return rt->GetDepthFormat() >= kDepthFormat24; + + return GetGfxDevice().GetFramebufferDepthFormat() >= kDepthFormat24; +} + +void RenderTextureDiscardContents(RenderTexture* rt, bool discardColor, bool discardDepth) +{ + GfxDevice& device = GetGfxDevice(); + + RenderSurfaceHandle color = rt ? rt->GetColorSurfaceHandle() : device.GetBackBufferColorSurface(); + RenderSurfaceHandle rcolor = rt ? rt->GetResolvedColorSurfaceHandle() : RenderSurfaceHandle(); + RenderSurfaceHandle depth = rt ? rt->GetDepthSurfaceHandle() : device.GetBackBufferDepthSurface(); + + if(discardColor && color.IsValid()) + device.DiscardContents(color); + if(discardColor && rcolor.IsValid()) + device.DiscardContents(rcolor); + if(discardDepth && depth.IsValid()) + device.DiscardContents(depth); +} + + +// -------------------------------------------------------------------------- + + +#if ENABLE_UNIT_TESTS +#include "External/UnitTest++/src/UnitTest++.h" +SUITE (RenderTextureTests) +{ +TEST(RenderTextureTests_BPPTableCorrect) +{ + // checks that you did not forget to update BPP counts when you added a new format :) + for (int i = 0; i < kRTFormatCount; ++i) + { + CHECK(kRenderTextureFormatBPP[i] != 0); + } + for (int i = 1; i < kDepthFormatCount; ++i) // skip DepthFormatNone, since it is expected to have zero + { + CHECK(kDepthFormatBPP[i] != 0); + } +} +} + +#endif diff --git a/Runtime/Graphics/RenderTexture.h b/Runtime/Graphics/RenderTexture.h new file mode 100644 index 0000000..44e7281 --- /dev/null +++ b/Runtime/Graphics/RenderTexture.h @@ -0,0 +1,210 @@ +#pragma once + +#include "Texture.h" +#include "Runtime/GfxDevice/GfxDeviceTypes.h" +#include "Runtime/GfxDevice/GfxDeviceObjects.h" +#include "Runtime/Utilities/BitUtility.h" +#include "Runtime/Utilities/LinkedList.h" +#include "Runtime/Modules/ExportModules.h" + +namespace ShaderLab { + struct FastPropertyName; + class TexEnv; +} + +enum { + kScreenOffscreen = -1, +}; + + +class EXPORT_COREMODULE RenderTexture : public Texture +{ +public: + + DECLARE_OBJECT_SERIALIZE (RenderTexture) + REGISTER_DERIVED_CLASS (RenderTexture, Texture) + + RenderTexture (MemLabelId label, ObjectCreationMode mode); + // virtual ~RenderTexture (); declared-by-macro + + void SetWidth (int width); + int GetWidth () const { return m_Width; } + + void SetHeight (int height); + int GetHeight () const { return m_Height; } + + void SetDepthFormat( DepthBufferFormat depth ); + DepthBufferFormat GetDepthFormat() const { return static_cast<DepthBufferFormat>(m_DepthFormat); } + + bool GetIsPowerOfTwo () const { return IsPowerOfTwo(m_Width) && IsPowerOfTwo(m_Height); } + + void SetDimension (TextureDimension dim); + virtual TextureDimension GetDimension() const { return m_Dimension; } + + void SetVolumeDepth(int v); + int GetVolumeDepth() const { return m_VolumeDepth; } + + void SetAntiAliasing(int aa); + int GetAntiAliasing() const { return m_AntiAliasing; } + bool IsAntiAliased() const { return m_AntiAliasing > 1; } + + void SetMipMap( bool mipmap ); + bool GetMipMap() const { return m_MipMap; } + + void SetGenerateMips (bool v); + bool GetGenerateMips() const { return m_GenerateMips; } + + void SetSRGBReadWrite(bool sRGB); + bool GetSRGBReadWrite() const { return m_SRGB; } + + void SetEnableRandomWrite(bool v); + bool GetEnableRandomWrite() const { return m_EnableRandomWrite; } + + void SetCreatedFromScript( bool fromScript ) { m_CreatedFromScript = fromScript; } + bool GetCreatedFromScript() const { return m_CreatedFromScript; } + + void SetSampleOnlyDepth( bool sampleOnly ) { m_SampleOnlyDepth = sampleOnly; } + bool GetSampleOnlyDepth() const { return m_SampleOnlyDepth; } + + RenderTextureFormat GetColorFormat() const { return static_cast<RenderTextureFormat>(m_ColorFormat); } + void SetColorFormat( RenderTextureFormat format ); + + virtual bool HasMipMap () const { return m_MipMap; } + virtual int CountMipmaps() const { return 1; } //@TODO ? + + enum { + kFlagDontSetViewport = (1<<0), + kFlagForceResolve = (1<<1), // Force resolve to texture: Used by MSAA targets and Xbox 360 + kFlagDontRestoreColor = (1<<2), // Xbox 360 specific: do not restore old contents to EDRAM + kFlagDontRestoreDepth = (1<<3), // Xbox 360 specific: do not restore old contents to EDRAM + kFlagDontRestore = kFlagDontRestoreColor | kFlagDontRestoreDepth, + }; + + static void FindAndSetSRGBWrite( RenderTexture* newActive ); + + // Makes the render texture the current render target. If texture is NULL the back buffer is activated. + static void SetActive( RenderTexture* texture, int mipLevel = 0, CubemapFace face = kCubeFaceUnknown, UInt32 flags = 0 ); + static bool SetActive(int count, RenderSurfaceHandle* colors, RenderSurfaceHandle depth, RenderTexture* rt, int mipLevel = 0, CubemapFace face=kCubeFaceUnknown, UInt32 flags=0); + + // Returns the active render texture. + // NULL means the main window is active. + static RenderTexture* GetActive(); + + // Does card support RTs, and built with Unity Pro, and not manually disabled? + static bool IsEnabled (); + // this should not be used... it's there only to support RenderTexture.enabled in scripts, in case anyone uses it. + static void SetEnabled (bool enable); + + // Destroys all render textures created + static void ReleaseAll (); + + // Creates the render texture. + // Create is automatically called inside Activate the first time. + // Create can fail if RenderTexture::IsEnabled is false + bool Create (); + + // Destroys the render texture + void Release (); + + // Is the render texture created? + bool IsCreated() const { return m_ColorHandle.IsValid() || m_DepthHandle.IsValid(); } + + // Discards the contents of this render texture + // Xbox 360: will not restore contents to EDRAM the next time this RenderTexture is set. + // GLES: will use combo of glDiscardFramebufferEXT and glClear to avoid both resolve and restore + // Other platforms: currently no effect. + // NB: we have both because no-arg one had been used directly from scripts, so we keep it for b/c + void DiscardContents(); + void DiscardContents(bool discardColor, bool discardDepth); + void MarkRestoreExpected(); + + virtual bool ExtractImage (ImageReference* /*image*/, int /*imageIndex*/ = 0) const { return false; } + virtual void ApplySettings(); + + virtual int GetRuntimeMemorySize() const; + + + #if UNITY_EDITOR + TextureFormat GetEditorUITextureFormat () const { return -1; } + #endif + + #if ENABLE_PROFILER || UNITY_EDITOR + virtual int GetStorageMemorySize() const { return 0; } + static int GetCreatedRenderTextureCount (); + static int GetCreatedRenderTextureBytes (); + #endif + + + RenderSurfaceHandle GetColorSurfaceHandle() { return m_ColorHandle; } + RenderSurfaceHandle GetResolvedColorSurfaceHandle() { return m_ResolvedColorHandle; } + RenderSurfaceHandle GetDepthSurfaceHandle() { return m_DepthHandle; } + + + virtual int GetDataWidth() const { return m_Width; } + virtual int GetDataHeight() const { return m_Height; } + + void AwakeFromLoad(AwakeFromLoadMode mode); + + virtual void UnloadFromGfxDevice(bool /*forceUnloadAll*/) { } + virtual void UploadToGfxDevice() { } + + ShaderLab::TexEnv *SetGlobalProperty (const ShaderLab::FastPropertyName& name); + + void GrabPixels (int left, int bottom, int width, int height); + + // Flips vertical texel size to positive or negative if we're not using OpenGL coords + void CorrectVerticalTexelSize(bool shouldBePositive); + + const TextureID& GetSecondaryTextureID() { return m_SecondaryTexID; } + + static void SetTemporarilyAllowIndieRenderTexture (bool allow); + +private: + void DestroySurfaces(); + void ResolveAntiAliasedSurface(); + void UpdateTexelSize(); + bool GetSupportedMipMapFlag(bool mipMap) const; + +private: + int m_Width; ///< range {1, 20000} + int m_Height;///< range {1, 20000} + int m_AntiAliasing;///< enum { None = 1, 2xMSAA = 2, 4xMSAA = 4, 8xMSAA = 8 } Anti-aliasing + int m_VolumeDepth;///< range {1, 20000} + int m_ColorFormat; ///< enum { RGBA32 = 0, Depth texture = 1 } Color buffer format + int m_DepthFormat; ///< enum { No depth buffer = 0, 16 bit depth = 1, 24 bit depth = 2 } Depth buffer format + TextureDimension m_Dimension; + bool m_MipMap; + bool m_GenerateMips; + bool m_SRGB; + bool m_EnableRandomWrite; + bool m_CreatedFromScript; + bool m_SampleOnlyDepth; + + TextureID m_SecondaryTexID; + + RenderSurfaceHandle m_ColorHandle; + RenderSurfaceHandle m_ResolvedColorHandle; + RenderSurfaceHandle m_DepthHandle; + + int m_RegisteredSizeForStats; + + ListNode<RenderTexture> m_RenderTexturesNode; + + bool m_SecondaryTexIDUsed; +}; + +int EstimateRenderTextureSize (int width, int height, int depth, RenderTextureFormat format, DepthBufferFormat depthFormat, TextureDimension dim, bool mipMap); + +// return or passed tex (but created if needed) or NULL if RT cannot be created by any reason +RenderTexture* EnsureRenderTextureIsCreated(RenderTexture* tex); + +bool RenderTextureSupportsStencil(RenderTexture* rt); + +void RenderTextureDiscardContents(RenderTexture* rt, bool discardColor, bool discardDepth); + +#include "Runtime/Scripting/Backend/ScriptingTypes.h" +struct ScriptingRenderBuffer +{ + int m_RenderTextureInstanceID; + RenderSurfaceBase* m_BufferPtr; +}; diff --git a/Runtime/Graphics/S3Decompression.cpp b/Runtime/Graphics/S3Decompression.cpp new file mode 100644 index 0000000..b2f518d --- /dev/null +++ b/Runtime/Graphics/S3Decompression.cpp @@ -0,0 +1,1882 @@ +#include "UnityPrefix.h" +#include "S3Decompression.h" +#include "Runtime/Utilities/LogAssert.h" +#include "Runtime/Utilities/BitUtility.h" +#include "Runtime/Graphics/Image.h" +#include "Runtime/Graphics/FlashATFDecompression.h" +#include "Runtime/Graphics/ETC2Decompression.h" + +#define PRINT_DECOMPRESSION_TIMES 0 +#if PRINT_DECOMPRESSION_TIMES +#include "Runtime/Input/TimeManager.h" +#endif + + +// \todo [2010-02-09 petri] Define in some config file? +#define HAS_ETC_DECOMPRESSOR (UNITY_ANDROID && !i386 || UNITY_EDITOR || UNITY_BB10 || UNITY_TIZEN) +#define HAS_ATC_DECOMPRESSOR (UNITY_ANDROID && !i386 || UNITY_EDITOR) + +#define HAS_ASTC_DECOMPRESSOR (UNITY_ANDROID || UNITY_EDITOR) + +#if HAS_ATC_DECOMPRESSOR + #include "External/Qualcomm_TextureConverter/TextureConverter.h" + #if UNITY_WIN + #include <process.h> + // On Windows, Qonvert is compiled with MSVCRT, but we link against LIBCMT; filling the gaps here + extern "C" int _imp___getpid(void){ return _getpid(); } + #elif UNITY_OSX + // On OSX, Qonvert is compiled with -fstack-protector, but we don't use it for the MacEditor + extern "C" + { + void* __stack_chk_guard = (void*)0xdeadc0de; + void __stack_chk_fail() { ErrorString("stack_chk_fail failed"); } + } + #elif defined(ARM_ARCH_VFP) + // On Android, Qonvert is compiled with armv5 (no vfp), but we don't want to include the softfp emulation + extern "C" unsigned __aeabi_f2uiz(float f){return (int)f;} + #endif +#endif + +// ------------------------------------------------------------------------ +// DXT + + +#if HAS_DXT_DECOMPRESSOR + +struct DXTColBlock +{ + UInt16 col0; + UInt16 col1; + UInt8 row[4]; +}; + +struct DXTAlphaBlockExplicit +{ + UInt16 row[4]; +}; + +struct DXTAlphaBlock3BitLinear +{ + UInt8 alpha0; + UInt8 alpha1; + UInt8 stuff[6]; +}; + +#if UNITY_BIG_ENDIAN + +struct Color8888 +{ + UInt8 b; + UInt8 g; + UInt8 r; + UInt8 a; +}; + +static inline UInt16 GetByteSwap16( UInt16 i ) +{ + return static_cast<UInt16>((i << 8) | (i >> 8)); +} + +static inline UInt32 GetByteSwap32( UInt32 i ) +{ + return static_cast<UInt32>((i >> 24) | (i >> 8) & 0x0000ff00 | (i << 8) & 0x00ff0000 | (i << 24)); +} + +struct Color565 +{ + unsigned b : 5; + unsigned g : 6; + unsigned r : 5; +}; + +#else + +static inline UInt16 GetByteSwap16( UInt16 i ) { return i; } +static inline UInt32 GetByteSwap32( UInt32 i ) { return i; } + +struct Color8888 // EH? This seems to be wrong! +{ + UInt8 r; + UInt8 g; + UInt8 b; + UInt8 a; +}; + +struct Color565 +{ + unsigned b : 5; + unsigned g : 6; + unsigned r : 5; +}; + +#endif + + +inline void GetColorBlockColors( const DXTColBlock* block, Color8888 colors[4] ) +{ + union + { + Color565 color565; + UInt16 color16; + } col0, col1; + col0.color16 = GetByteSwap16( block->col0 ); + col1.color16 = GetByteSwap16( block->col1 ); + + const Color565* col; + + // It's not enough just to shift bits to full 8 bit precision - the lower bits + // must also be filled to match the way hardware does the rounding. + col = &col0.color565; + colors[0].r = ( col->r << 3 ) | ( col->r >> 2 ); + colors[0].g = ( col->g << 2 ) | ( col->g >> 4 ); + colors[0].b = ( col->b << 3 ) | ( col->b >> 2 ); + colors[0].a = 0xff; + + col = &col1.color565; + colors[1].r = ( col->r << 3 ) | ( col->r >> 2 ); + colors[1].g = ( col->g << 2 ) | ( col->g >> 4 ); + colors[1].b = ( col->b << 3 ) | ( col->b >> 2 ); + colors[1].a = 0xff; + + if( col0.color16 > col1.color16 ) + { + // Four-color block: derive the other two colors. + // 00 = color_0, 01 = color_1, 10 = color_2, 11 = color_3 + // These two bit codes correspond to the 2-bit fields + // stored in the 64-bit block. + + colors[2].r = (UInt8)(((UInt16)colors[0].r * 2 + (UInt16)colors[1].r )/3); + colors[2].g = (UInt8)(((UInt16)colors[0].g * 2 + (UInt16)colors[1].g )/3); + colors[2].b = (UInt8)(((UInt16)colors[0].b * 2 + (UInt16)colors[1].b )/3); + colors[2].a = 0xff; + + colors[3].r = (UInt8)(((UInt16)colors[0].r + (UInt16)colors[1].r *2 )/3); + colors[3].g = (UInt8)(((UInt16)colors[0].g + (UInt16)colors[1].g *2 )/3); + colors[3].b = (UInt8)(((UInt16)colors[0].b + (UInt16)colors[1].b *2 )/3); + colors[3].a = 0xff; + } + else + { + // Three-color block: derive the other color. + // 00 = color_0, 01 = color_1, 10 = color_2, 11 = transparent. + // These two bit codes correspond to the 2-bit fields + // stored in the 64-bit block. + + colors[2].r = (UInt8)(((UInt16)colors[0].r + (UInt16)colors[1].r )/2); + colors[2].g = (UInt8)(((UInt16)colors[0].g + (UInt16)colors[1].g )/2); + colors[2].b = (UInt8)(((UInt16)colors[0].b + (UInt16)colors[1].b )/2); + colors[2].a = 0xff; + + // set transparent to black to match DXT specs + colors[3].r = 0x00; + colors[3].g = 0x00; + colors[3].b = 0x00; + colors[3].a = 0x00; + } +} + +// width is width of destination image in pixels +inline void DecodeColorBlock( UInt32* dest, const DXTColBlock& colorBlock, int width, const UInt32 colors[4] ) +{ + // r steps through lines in y + for( int r=0; r < 4; r++, dest += width-4 ) + { + // width * 4 bytes per pixel per line + // each j block row is 4 lines of pixels + + // Do four pixels, step in twos because n is only used for the shift + // n steps through pixels + for( int n = 0; n < 8; n += 2 ) + { + UInt32 bits = (colorBlock.row[r] >> n) & 3; + DebugAssert (bits <= 3); + *dest = colors[bits]; + ++dest; + } + } +} + +inline void DecodeAlphaExplicit( UInt32* dest, const DXTAlphaBlockExplicit& alphaBlock, int width, UInt32 alphazero ) +{ + // alphazero is a bit mask that when ANDed with the image color + // will zero the alpha bits, so if the image DWORDs are + // ARGB then alphazero will be 0x00FFFFFF or if + // RGBA then alphazero will be 0xFFFFFF00 + // alphazero constructed automatically from field order of Color8888 structure + + union + { + Color8888 col; + UInt32 col32; + } u; + u.col.r = u.col.g = u.col.b = 0; + + for( int row=0; row < 4; row++, dest += width-4 ) + { + UInt16 wrd = GetByteSwap16( alphaBlock.row[ row ] ); + + for( int pix = 0; pix < 4; ++pix ) + { + // zero the alpha bits of image pixel + *dest &= alphazero; + + u.col.a = wrd & 0x000f; // get lowest 4 bits + u.col.a = u.col.a | (u.col.a << 4); + + *dest |= u.col32; // OR into the previously nulled alpha + + wrd >>= 4; + ++dest; + } + } +} + +inline void DecodeAlpha3BitLinear( UInt32* dest, const DXTAlphaBlock3BitLinear& alphaBlock, int width, UInt32 alphazero ) +{ + union + { + Color8888 alphaCol[4][4]; + UInt32 alphaCol32[4][4]; + } u; + UInt32 alphamask = ~alphazero; + UInt16 alphas[8]; + + alphas[0] = alphaBlock.alpha0; + alphas[1] = alphaBlock.alpha1; + + // 8-alpha or 6-alpha block? + if( alphas[0] > alphas[1] ) + { + // 8-alpha block: derive the other 6 alphas. + // 000 = alpha[0], 001 = alpha[1], others are interpolated + + alphas[2] = ( 6 * alphas[0] + alphas[1] + 3) / 7; // bit code 010 + alphas[3] = ( 5 * alphas[0] + 2 * alphas[1] + 3) / 7; // Bit code 011 + alphas[4] = ( 4 * alphas[0] + 3 * alphas[1] + 3) / 7; // Bit code 100 + alphas[5] = ( 3 * alphas[0] + 4 * alphas[1] + 3) / 7; // Bit code 101 + alphas[6] = ( 2 * alphas[0] + 5 * alphas[1] + 3) / 7; // Bit code 110 + alphas[7] = ( alphas[0] + 6 * alphas[1] + 3) / 7; // Bit code 111 + } + else + { + // 6-alpha block: derive the other alphas. + // 000 = alpha[0], 001 = alpha[1], others are interpolated + + alphas[2] = (4 * alphas[0] + alphas[1] + 2) / 5; // Bit code 010 + alphas[3] = (3 * alphas[0] + 2 * alphas[1] + 2) / 5; // Bit code 011 + alphas[4] = (2 * alphas[0] + 3 * alphas[1] + 2) / 5; // Bit code 100 + alphas[5] = ( alphas[0] + 4 * alphas[1] + 2) / 5; // Bit code 101 + alphas[6] = 0; // Bit code 110 + alphas[7] = 255; // Bit code 111 + } + + // Decode 3-bit fields into array of 16 bytes with same value + UInt8 blockBits[4][4]; + + // first two rows of 4 pixels each: + const UInt32 mask = 7; // three bits + + // TBD: Ouch! Unaligned reads there! Poor old PPC! + + UInt32 bits = GetByteSwap32( *(UInt32*)&alphaBlock.stuff[0] ); // first 3 bytes + + blockBits[0][0] = (UInt8)( bits & mask ); + bits >>= 3; + blockBits[0][1] = (UInt8)( bits & mask ); + bits >>= 3; + blockBits[0][2] = (UInt8)( bits & mask ); + bits >>= 3; + blockBits[0][3] = (UInt8)( bits & mask ); + bits >>= 3; + blockBits[1][0] = (UInt8)( bits & mask ); + bits >>= 3; + blockBits[1][1] = (UInt8)( bits & mask ); + bits >>= 3; + blockBits[1][2] = (UInt8)( bits & mask ); + bits >>= 3; + blockBits[1][3] = (UInt8)( bits & mask ); + + // now last two rows + // it's ok to fetch one byte too much because there will always be color block after alpha + bits = GetByteSwap32( *(UInt32*)&alphaBlock.stuff[3] ); // last 3 bytes + + blockBits[2][0] = (UInt8)( bits & mask ); + bits >>= 3; + blockBits[2][1] = (UInt8)( bits & mask ); + bits >>= 3; + blockBits[2][2] = (UInt8)( bits & mask ); + bits >>= 3; + blockBits[2][3] = (UInt8)( bits & mask ); + bits >>= 3; + blockBits[3][0] = (UInt8)( bits & mask ); + bits >>= 3; + blockBits[3][1] = (UInt8)( bits & mask ); + bits >>= 3; + blockBits[3][2] = (UInt8)( bits & mask ); + bits >>= 3; + blockBits[3][3] = (UInt8)( bits & mask ); + + // decode the codes into alpha values + int row, pix; + + for( row = 0; row < 4; row++ ) + { + for( pix=0; pix < 4; pix++ ) + { + u.alphaCol[row][pix].a = (UInt8) alphas[ blockBits[row][pix] ]; + } + } + + // Write out alpha values to the image bits + for( row=0; row < 4; row++, dest += width-4 ) + { + for( pix = 0; pix < 4; pix++ ) + { + *dest &= alphazero; // zero the alpha bits of image pixel + *dest |= u.alphaCol32[row][pix] & alphamask; // or the bits into the prev. nulled alpha + dest++; + } + } +} + + +void DecompressDXT1( int xblocks, int yblocks, int destWidth, const UInt32* m_pCompBytes, UInt32* decompBytes ) +{ + union + { + Color8888 colors8888[4]; + UInt32 colors32[4]; + } u; + + for( int j = 0; j < yblocks; ++j ) + { + const DXTColBlock* block = (const DXTColBlock*)( m_pCompBytes + j * xblocks * 2 ); // 8 bytes per block + + for( int i = 0; i < xblocks; ++i, ++block ) + { + GetColorBlockColors( block, u.colors8888 ); + UInt32* dest = (UInt32*)((UInt8*)decompBytes + i*16 + (j*4) * destWidth * 4 ); + DecodeColorBlock( dest, *block, destWidth, u.colors32 ); + } + } +} + + +void DecompressDXT3( int xblocks, int yblocks, int destWidth, const UInt32* m_pCompBytes, UInt32* decompBytes ) +{ + union + { + Color8888 colors[4]; + UInt32 colors32[4]; + } u; + + // fill alphazero with appropriate value to zero out alpha when + // alphazero is ANDed with the image color + u.colors[0].a = 0; + u.colors[0].r = u.colors[0].g = u.colors[0].b = 0xff; + UInt32 alphazero = u.colors32[0]; + + for( int j = 0; j < yblocks; ++j ) + { + const DXTColBlock* block = (const DXTColBlock*)( m_pCompBytes + j * xblocks * 4 ); // 16 bytes for alpha+color block + + for( int i = 0; i < xblocks; ++i, ++block ) + { + // Get alpha block + const DXTAlphaBlockExplicit* alphaBlock = (const DXTAlphaBlockExplicit*)block; + ++block; + + // Get color block & colors + GetColorBlockColors( block, u.colors ); + + // Decode the color block into the bitmap bits + UInt32* dest = (UInt32*)((UInt8*)decompBytes + i*16 + (j*4) * destWidth * 4 ); + DecodeColorBlock( dest, *block, destWidth, u.colors32 ); + + // Overwrite the previous alpha bits with the alpha block info + DecodeAlphaExplicit( dest, *alphaBlock, destWidth, alphazero ); + } + } +} + + +void DecompressDXT5( int xblocks, int yblocks, int destWidth, const UInt32* m_pCompBytes, UInt32* decompBytes ) +{ + union + { + Color8888 colors[4]; + UInt32 colors32[4]; + } u; + + // fill alphazero with appropriate value to zero out alpha when + // alphazero is ANDed with the image color 32 bit + u.colors[0].a = 0; + u.colors[0].r = u.colors[0].g = u.colors[0].b = 0xff; + UInt32 alphazero = u.colors32[0]; + + for( int j = 0; j < yblocks; ++j ) + { + const DXTColBlock* block = (const DXTColBlock*) ( m_pCompBytes + j * xblocks * 4 ); // 16 bytes for alpha+color block + + for( int i = 0; i < xblocks; ++i, ++block ) + { + // Get alpha block + const DXTAlphaBlock3BitLinear* alphaBlock = (const DXTAlphaBlock3BitLinear*)block; + block++; + + // Get color block & colors + GetColorBlockColors( block, u.colors ); + + // Decode the color block into the bitmap bits + UInt32* dest = (UInt32*)((UInt8*)decompBytes + i*16 + (j*4) * destWidth * 4 ); + DecodeColorBlock( dest, *block, destWidth, u.colors32 ); + + // Overwrite the previous alpha bits with the alpha block info + DecodeAlpha3BitLinear( dest, *alphaBlock, destWidth, alphazero ); + } + } +} + +#endif + + + +// ------------------------------------------------------------------------ +// PVRTC + + + +#if HAS_PVRTC_DECOMPRESSOR + +const int kPVRSizeX2 = 8; // block width 8 pixels in 2BPP case +const int kPVRSizeX4 = 4; // block width 4 pixels in 4BPP case +const int kPVRBlockSizeY = 4; // block height always 4 pixels + +const int kPVRPunchThroughIndex = 2; + +#define PVR_WRAP_POT_COORD(Val, Size) ((Val) & ((Size)-1)) + +#define CLAMP(X, lower, upper) (std::min(std::max((X),(lower)), (upper))) + +#define PVR_LIMIT_COORD(Val, Size, AssumeImageTiles) \ + ((AssumeImageTiles) ? PVR_WRAP_POT_COORD((Val), (Size)) : CLAMP((Val), 0, (Size)-1)) + +// 64 bits per PVRTC block +struct PVRTCBlock +{ + UInt32 packedData[2]; +}; + + + +// Given a block, extract the color information and convert to 5554 formats +static void Unpack5554Colour (const PVRTCBlock *pBlock, int ABColours[2][4]) +{ + UInt32 RawBits[2]; + + int i; + + /* + // Extract A and B + */ + RawBits[0] = pBlock->packedData[1] & (0xFFFE); /*15 bits (shifted up by one)*/ + RawBits[1] = pBlock->packedData[1] >> 16; /*16 bits*/ + + /* + //step through both colours + */ + for(i = 0; i < 2; i++) + { + /* + // if completely opaque + */ + if(RawBits[i] & (1<<15)) + { + /* + // Extract R and G (both 5 bit) + */ + ABColours[i][0] = (RawBits[i] >> 10) & 0x1F; + ABColours[i][1] = (RawBits[i] >> 5) & 0x1F; + + /* + // The precision of Blue depends on A or B. If A then we need to + // replicate the top bit to get 5 bits in total + */ + ABColours[i][2] = RawBits[i] & 0x1F; + if(i==0) + { + ABColours[0][2] |= ABColours[0][2] >> 4; + } + + /* + // set 4bit alpha fully on... + */ + ABColours[i][3] = 0xF; + } + /* + // Else if colour has variable translucency + */ + else + { + /* + // Extract R and G (both 4 bit). + // (Leave a space on the end for the replication of bits + */ + ABColours[i][0] = (RawBits[i] >> (8-1)) & 0x1E; + ABColours[i][1] = (RawBits[i] >> (4-1)) & 0x1E; + + /* + // replicate bits to truly expand to 5 bits + */ + ABColours[i][0] |= ABColours[i][0] >> 4; + ABColours[i][1] |= ABColours[i][1] >> 4; + + /* + // grab the 3(+padding) or 4 bits of blue and add an extra padding bit + */ + ABColours[i][2] = (RawBits[i] & 0xF) << 1; + + /* + // expand from 3 to 5 bits if this is from colour A, or 4 to 5 bits if from + // colour B + */ + if(i==0) + { + ABColours[0][2] |= ABColours[0][2] >> 3; + } + else + { + ABColours[0][2] |= ABColours[0][2] >> 4; + } + + /* + // Set the alpha bits to be 3 + a zero on the end + */ + ABColours[i][3] = (RawBits[i] >> 11) & 0xE; + }/*end if variable alpha*/ + }/*end for i*/ + +} + + +// Given the block and the texture type and it's relative position in the +// 2x2 group of blocks, extract the bit patterns for the fully defined pixels. +template<bool Do2bitMode> +static void UnpackModulations(const PVRTCBlock *pBlock, + int ModulationVals[8][16], + int ModulationModes[8][16], + int StartX, + int StartY) +{ + int BlockModMode= pBlock->packedData[1] & 1; + UInt32 ModulationBits = pBlock->packedData[0]; + + // if it's in an interpolated mode + if(Do2bitMode && BlockModMode) + { + // run through all the pixels in the block. Note we can now treat all the + // "stored" values as if they have 2bits (even when they didn't!) + for(int y = 0; y < kPVRBlockSizeY; y++) + { + for(int x = 0; x < kPVRSizeX2; x++) + { + ModulationModes[y+StartY][x+StartX] = BlockModMode; + + // if this is a stored value... + if(((x^y)&1) == 0) + { + ModulationVals[y+StartY][x+StartX] = ModulationBits & 3; + ModulationBits >>= 2; + } + } + } + } + // else if direct encoded 2bit mode - i.e. 1 mode bit per pixel + else if(Do2bitMode) + { + for(int y = 0; y < kPVRBlockSizeY; y++) + { + for(int x = 0; x < kPVRSizeX2; x++) + { + ModulationModes[y+StartY][x+StartX] = BlockModMode; + + // double the bits so 0=> 00, and 1=>11 + if(ModulationBits & 1) + { + ModulationVals[y+StartY][x+StartX] = 0x3; + } + else + { + ModulationVals[y+StartY][x+StartX] = 0x0; + } + ModulationBits >>= 1; + } + } + } + // else its the 4bpp mode so each value has 2 bits + else + { + for(int y = 0; y < kPVRBlockSizeY; y++) + { + for(int x = 0; x < kPVRSizeX4; x++) + { + ModulationModes[y+StartY][x+StartX] = BlockModMode; + ModulationVals[y+StartY][x+StartX] = ModulationBits & 3; + ModulationBits >>= 2; + } + } + } + + // make sure nothing is left over + DebugAssert(ModulationBits==0); +} + + +template<bool do2bitMode> +static int GetUCoordPVR (int x) +{ + if (do2bitMode) + { + int u = (x & 0x7) | ((~x & 0x4) << 1); + return u - kPVRSizeX2/2; + } + else + { + int u = (x & 0x3) | ((~x & 0x2) << 1); + return u - kPVRSizeX4/2; + } +} + +static int GetVCoordPVR (int y) +{ + int v = (y & 0x3) | ((~y & 0x2) << 1); + return v - kPVRBlockSizeY/2; +} + + +// Performs a HW bit accurate interpolation of either the +// A or B colors for a particular pixel +// +// NOTE: It is assumed that the source colors are in ARGB 5554 format - +// This means that some "preparation" of the values will be necessary. +// NOTE: QP is Q-P, SR is S-R +template<bool Do2bitMode> +static void InterpolateColoursPVRTC(const int* __restrict P, + const int* __restrict QP, + const int* __restrict R, + const int* __restrict SR, + const int u, + const int v, + int* __restrict Result) +{ + int k; + int tmp1, tmp2; + + int uscale = Do2bitMode ? 8 : 4; + + for(k = 0; k < 4; k++) + { + tmp1 = P[k] * uscale + u * QP[k]; + tmp2 = R[k] * uscale + u * SR[k]; + + tmp1 = tmp1 * 4 + v * (tmp2 - tmp1); + + Result[k] = tmp1; + } + + /* + // Lop off the appropriate number of bits to get us to 8 bit precision + */ + if(Do2bitMode) + { + /* + // do RGB + */ + for(k = 0; k < 3; k++) + { + Result[k] >>= 2; + } + + Result[3] >>= 1; + } + else + { + /* + // do RGB (A is ok) + */ + for(k = 0; k < 3; k++) + { + Result[k] >>= 1; + } + } + + /* + // sanity check + */ + for(k = 0; k < 4; k++) + { + DebugAssert(Result[k] < 256); + } + + + /* + // Convert from 5554 to 8888 + // + // do RGB 5.3 => 8 + */ + for(k = 0; k < 3; k++) + { + Result[k] += Result[k] >> 5; + } + Result[3] += Result[3] >> 4; + + /* + // 2nd sanity check + */ + for(k = 0; k < 4; k++) + { + DebugAssert(Result[k] < 256); + } + +} + + +// Get the modulation value as a numerator of a fraction of 8ths +template<bool Do2bitMode> +static void GetModulationValue(int x, + int y, + const int ModulationVals[8][16], + const int ModulationModes[8][16], + int *Mod, + int *DoPT) +{ + static const int RepVals0[4] = {0, 3, 5, 8}; + static const int RepVals1[4] = {0, 4, 4, 8}; + + int ModVal; + + /* + // Map X and Y into the local 2x2 block + */ + y = (y & 0x3) | ((~y & 0x2) << 1); + if(Do2bitMode) + { + x = (x & 0x7) | ((~x & 0x4) << 1); + + } + else + { + x = (x & 0x3) | ((~x & 0x2) << 1); + } + + /* + // assume no PT for now + */ + *DoPT = 0; + + /* + // extract the modulation value. If a simple encoding + */ + if(ModulationModes[y][x]==0) + { + ModVal = RepVals0[ModulationVals[y][x]]; + } + else if(Do2bitMode) + { + /* + // if this is a stored value + */ + if(((x^y)&1)==0) + { + ModVal = RepVals0[ModulationVals[y][x]]; + } + /* + // else average from the neighbours + // + // if H&V interpolation... + */ + else if(ModulationModes[y][x] == 1) + { + ModVal = (RepVals0[ModulationVals[y-1][x]] + + RepVals0[ModulationVals[y+1][x]] + + RepVals0[ModulationVals[y][x-1]] + + RepVals0[ModulationVals[y][x+1]] + 2) / 4; + } + /* + // else if H-Only + */ + else if(ModulationModes[y][x] == 2) + { + ModVal = (RepVals0[ModulationVals[y][x-1]] + + RepVals0[ModulationVals[y][x+1]] + 1) / 2; + } + /* + // else it's V-Only + */ + else + { + ModVal = (RepVals0[ModulationVals[y-1][x]] + + RepVals0[ModulationVals[y+1][x]] + 1) / 2; + + }/*end if/then/else*/ + } + /* + // else it's 4BPP and PT encoding + */ + else + { + ModVal = RepVals1[ModulationVals[y][x]]; + + *DoPT = ModulationVals[y][x] == kPVRPunchThroughIndex; + } + + *Mod =ModVal; +} + + + +// PVRTC UV twiddling interleaves bits of Y & X, like: XYXYXYXYXY. +// If size of one coordinate is larger, those bits are just copied into higher bits; +// e.g. XXXX,YYYYYYYY = YYYYXYXYXYXY. + +static UInt32 TwiddleY_PVRTC(UInt32 YSize, UInt32 XSize, UInt32 YPos) +{ + UInt32 Twiddled; + + UInt32 MinDimension; + + UInt32 SrcBitPos; + UInt32 DstBitPos; + + int ShiftCount; + + DebugAssert(YPos < YSize); + DebugAssert(IsPowerOfTwo(YSize)); + DebugAssert(IsPowerOfTwo(XSize)); + + if (YSize < XSize) + MinDimension = YSize; + else + MinDimension = XSize; + + // Step through all the bits in the "minimum" dimension + SrcBitPos = 1; + DstBitPos = 1; + Twiddled = 0; + ShiftCount = 0; + + while (SrcBitPos < MinDimension) + { + if(YPos & SrcBitPos) + Twiddled |= DstBitPos; + + SrcBitPos <<= 1; + DstBitPos <<= 2; + ShiftCount += 1; + } + + // prepend any unused bits, if they were from Y + if (YSize >= XSize) + { + YPos >>= ShiftCount; + Twiddled |= (YPos << (2*ShiftCount)); + } + + return Twiddled; +} + + +static UInt32 TwiddleX_PVRTC(UInt32 YSize, UInt32 XSize, UInt32 XPos) +{ + UInt32 Twiddled; + + UInt32 MinDimension; + + UInt32 SrcBitPos; + UInt32 DstBitPos; + + int ShiftCount; + + DebugAssert(XPos < XSize); + DebugAssert(IsPowerOfTwo(YSize)); + DebugAssert(IsPowerOfTwo(XSize)); + + if (YSize < XSize) + MinDimension = YSize; + else + MinDimension = XSize; + + // Step through all the bits in the "minimum" dimension + SrcBitPos = 1; + DstBitPos = 2; + Twiddled = 0; + ShiftCount = 0; + + while (SrcBitPos < MinDimension) + { + if (XPos & SrcBitPos) + { + Twiddled |= DstBitPos; + } + + SrcBitPos <<= 1; + DstBitPos <<= 2; + ShiftCount += 1; + } + + // prepend any unused bits, if they were from X + if (YSize < XSize) + { + XPos >>= ShiftCount; + Twiddled |= (XPos << (2*ShiftCount)); + } + + return Twiddled; +} + + + +static UInt32 TwiddleUVPVRTC(UInt32 YSize, UInt32 XSize, UInt32 YPos, UInt32 XPos) +{ + return TwiddleY_PVRTC (YSize, XSize, YPos) + TwiddleX_PVRTC (YSize, XSize, XPos); +} + + + +template<bool Do2bitMode, bool AssumeImageTiles> +static void DecompressPVRTC(const PVRTCBlock *pCompressedData, + const int XDim, + const int YDim, + unsigned char* pResultImage) +{ + int BlkX, BlkY; + int BlkXp1, BlkYp1; + int BlkXDim, BlkYDim; + + int ModulationVals[8][16]; + int ModulationModes[8][16]; + + int Mod, DoPT; + + // local neighbourhood of blocks + const PVRTCBlock *pBlocks[2][2]; + + const PVRTCBlock *pPrevious[2][2] = {{NULL, NULL}, {NULL, NULL}}; + + // Low precision colors extracted from the blocks. + // Rightmost colors have leftmost values subtracted from them. + struct PVRBlockColors { + int Reps[2][4]; + }; + PVRBlockColors Colours5554[2][2]; + + // Interpolated A and B colours for the pixel + int ASig[4], BSig[4]; + + const int XBlockSize = Do2bitMode ? kPVRSizeX2 : kPVRSizeX4; + + + // For MBX don't allow the sizes to get too small + BlkXDim = std::max(2, XDim / XBlockSize); + BlkYDim = std::max(2, YDim / kPVRBlockSizeY); + + // Step through the pixels of the image decompressing each one in turn + // + // Note that this is a hideously inefficient way to do this! + for (int y = 0; y < YDim; y++) + { + BlkY = (y - kPVRBlockSizeY/2); + BlkY = PVR_LIMIT_COORD(BlkY, YDim, AssumeImageTiles); + BlkY /= kPVRBlockSizeY; + //BlkY = PVR_LIMIT_COORD(BlkY, BlkYDim, AssumeImageTiles); + BlkYp1 = PVR_LIMIT_COORD(BlkY+1, BlkYDim, AssumeImageTiles); + + // Since Y & X coordinates twiddle into separate bits, + // we can compute Y block twiddle mask here for the whole row. + const PVRTCBlock* blockPointerY = pCompressedData + TwiddleY_PVRTC (BlkYDim, BlkXDim, BlkY); + const PVRTCBlock* blockPointerY1 = pCompressedData + TwiddleY_PVRTC (BlkYDim, BlkXDim, BlkYp1); + + int coordV = GetVCoordPVR(y); + + for (int x = 0; x < XDim; x++) + { + // map this pixel to the top left neighbourhood of blocks + BlkX = (x - XBlockSize/2); + BlkX = PVR_LIMIT_COORD(BlkX, XDim, AssumeImageTiles); + BlkX /= XBlockSize; + //BlkX = PVR_LIMIT_COORD(BlkX, BlkXDim, AssumeImageTiles); + + + // compute the positions of the other 3 blocks + BlkXp1 = PVR_LIMIT_COORD(BlkX+1, BlkXDim, AssumeImageTiles); + + // Map to block memory locations + + // block offsets, X bits + UInt32 blockOffsetX = TwiddleX_PVRTC (BlkYDim, BlkXDim, BlkX); + UInt32 blockOffsetX1 = TwiddleX_PVRTC (BlkYDim, BlkXDim, BlkXp1); + + pBlocks[0][0] = blockPointerY + blockOffsetX; + pBlocks[0][1] = blockPointerY + blockOffsetX1; + pBlocks[1][0] = blockPointerY1 + blockOffsetX; + pBlocks[1][1] = blockPointerY1 + blockOffsetX1; + + + // extract the colours and the modulation information IF the previous values + // have changed. + if (pPrevious[0][0]!=pBlocks[0][0]||pPrevious[0][1]!=pBlocks[0][1]|| + pPrevious[1][0]!=pBlocks[1][0]||pPrevious[1][1]!=pBlocks[1][1]) + { + int StartY = 0; + for (int i = 0; i < 2; i++) + { + int StartX = 0; + for (int j = 0; j < 2; j++) + { + Unpack5554Colour(pBlocks[i][j], Colours5554[i][j].Reps); + + UnpackModulations<Do2bitMode>(pBlocks[i][j], + ModulationVals, + ModulationModes, + StartX, StartY); + + StartX += XBlockSize; + } // end for j + + // for rightmost color block, subtract leftmost colors now + for (int ab = 0; ab < 2; ++ab) + { + for (int c = 0; c < 4; ++c) + Colours5554[i][1].Reps[ab][c] -= Colours5554[i][0].Reps[ab][c]; + } + + StartY += kPVRBlockSizeY; + } // end for i + + // make a copy of the new pointers + pPrevious[0][0] = pBlocks[0][0]; + pPrevious[0][1] = pBlocks[0][1]; + pPrevious[1][0] = pBlocks[1][0]; + pPrevious[1][1] = pBlocks[1][1]; + } // end if the blocks have changed + + + // decompress the pixel. First compute the interpolated A and B signals + int coordU = GetUCoordPVR<Do2bitMode>(x); + InterpolateColoursPVRTC<Do2bitMode>(Colours5554[0][0].Reps[0], + Colours5554[0][1].Reps[0], + Colours5554[1][0].Reps[0], + Colours5554[1][1].Reps[0], + coordU, coordV, + ASig); + + InterpolateColoursPVRTC<Do2bitMode>(Colours5554[0][0].Reps[1], + Colours5554[0][1].Reps[1], + Colours5554[1][0].Reps[1], + Colours5554[1][1].Reps[1], + coordU, coordV, + BSig); + + GetModulationValue<Do2bitMode>(x,y, ModulationVals, ModulationModes, + &Mod, &DoPT); + + + // compute the modulated color and store in output image + for (int i = 0; i < 4; i++) + { + int res = (ASig[i] * 8 + Mod * (BSig[i] - ASig[i])) >> 3; + pResultImage[i] = (UInt8)res; + } + if (DoPT) + { + pResultImage[3] = 0; + } + pResultImage += 4; + + } // end for x + } // end for y +} +#endif + + + + +// ------------------------------------------------------------------------ +// ETC + + + +#if HAS_ETC_DECOMPRESSOR + +#define GETBITS(source, size, startpos) (( (source) >> ((startpos)-(size)+1) ) & ((1<<(size)) -1)) +#define GETBITSHIGH(source, size, startpos) (( (source) >> (((startpos)-32)-(size)+1) ) & ((1<<(size)) -1)) +inline int clampUByte(int v) { return (v < 0) ? 0 : ((v > 255) ? 255 : v); } + +inline void STORE_RGB(UInt32* img, int width, int x, int y, int r, int g, int b) +{ + Assert(r >= 0 && r <= 255); + Assert(g >= 0 && g <= 255); + Assert(b >= 0 && b <= 255); + Assert(x >= 0 && x < width); + img[y*width + x] = (0xFF<<24) | (r<<0) | (g<<8) | (b<<16); +} + +void decompressBlockDiffFlip(unsigned int block_part1, unsigned int block_part2, UInt32* dstImg, int width, int startx, int starty) +{ + static const int unscramble[4] = {2, 3, 1, 0}; + + int compressParams[16][4]; + + compressParams[0][0] = -8; compressParams[0][1] = -2; compressParams[0][2] = 2; compressParams[0][3] = 8; + compressParams[1][0] = -8; compressParams[1][1] = -2; compressParams[1][2] = 2; compressParams[1][3] = 8; + compressParams[2][0] = -17; compressParams[2][1] = -5; compressParams[2][2] = 5; compressParams[2][3] = 17; + compressParams[3][0] = -17; compressParams[3][1] = -5; compressParams[3][2] = 5; compressParams[3][3] = 17; + compressParams[4][0] = -29; compressParams[4][1] = -9; compressParams[4][2] = 9; compressParams[4][3] = 29; + compressParams[5][0] = -29; compressParams[5][1] = -9; compressParams[5][2] = 9; compressParams[5][3] = 29; + compressParams[6][0] = -42; compressParams[6][1] = -13; compressParams[6][2] = 13; compressParams[6][3] = 42; + compressParams[7][0] = -42; compressParams[7][1] = -13; compressParams[7][2] = 13; compressParams[7][3] = 42; + compressParams[8][0] = -60; compressParams[8][1] = -18; compressParams[8][2] = 18; compressParams[8][3] = 60; + compressParams[9][0] = -60; compressParams[9][1] = -18; compressParams[9][2] = 18; compressParams[9][3] = 60; + compressParams[10][0] = -80; compressParams[10][1] = -24; compressParams[10][2] = 24; compressParams[10][3] = 80; + compressParams[11][0] = -80; compressParams[11][1] = -24; compressParams[11][2] = 24; compressParams[11][3] = 80; + compressParams[12][0] =-106; compressParams[12][1] = -33; compressParams[12][2] = 33; compressParams[12][3] = 106; + compressParams[13][0] =-106; compressParams[13][1] = -33; compressParams[13][2] = 33; compressParams[13][3] = 106; + compressParams[14][0] =-183; compressParams[14][1] = -47; compressParams[14][2] = 47; compressParams[14][3] = 183; + compressParams[15][0] =-183; compressParams[15][1] = -47; compressParams[15][2] = 47; compressParams[15][3] = 183; + + UInt8 avg_color[3], enc_color1[3], enc_color2[3]; + signed char diff[3]; + int table; + int index,shift; + int r,g,b; + int diffbit; + int flipbit; + + diffbit = (GETBITSHIGH(block_part1, 1, 33)); + flipbit = (GETBITSHIGH(block_part1, 1, 32)); + + if( !diffbit ) + { + + // We have diffbit = 0. + + // First decode left part of block. + avg_color[0]= GETBITSHIGH(block_part1, 4, 63); + avg_color[1]= GETBITSHIGH(block_part1, 4, 55); + avg_color[2]= GETBITSHIGH(block_part1, 4, 47); + + // Here, we should really multiply by 17 instead of 16. This can + // be done by just copying the four lower bits to the upper ones + // while keeping the lower bits. + avg_color[0] |= (avg_color[0] <<4); + avg_color[1] |= (avg_color[1] <<4); + avg_color[2] |= (avg_color[2] <<4); + + table = GETBITSHIGH(block_part1, 3, 39) << 1; + + + unsigned int pixel_indices_MSB, pixel_indices_LSB; + + pixel_indices_MSB = GETBITS(block_part2, 16, 31); + pixel_indices_LSB = GETBITS(block_part2, 16, 15); + + if( (flipbit) == 0 ) + { + // We should not flip + shift = 0; + for(int x=startx; x<startx+2; x++) + { + for(int y=starty; y<starty+4; y++) + { + index = ((pixel_indices_MSB >> shift) & 1) << 1; + index |= ((pixel_indices_LSB >> shift) & 1); + shift++; + index=unscramble[index]; + + r=clampUByte(avg_color[0]+compressParams[table][index]); + g=clampUByte(avg_color[1]+compressParams[table][index]); + b=clampUByte(avg_color[2]+compressParams[table][index]); + STORE_RGB(dstImg, width, x, y, r, g, b); + } + } + } + else + { + // We should flip + shift = 0; + for(int x=startx; x<startx+4; x++) + { + for(int y=starty; y<starty+2; y++) + { + index = ((pixel_indices_MSB >> shift) & 1) << 1; + index |= ((pixel_indices_LSB >> shift) & 1); + shift++; + index=unscramble[index]; + + r=clampUByte(avg_color[0]+compressParams[table][index]); + g=clampUByte(avg_color[1]+compressParams[table][index]); + b=clampUByte(avg_color[2]+compressParams[table][index]); + STORE_RGB(dstImg, width, x, y, r, g, b); + } + shift+=2; + } + } + + // Now decode other part of block. + avg_color[0]= GETBITSHIGH(block_part1, 4, 59); + avg_color[1]= GETBITSHIGH(block_part1, 4, 51); + avg_color[2]= GETBITSHIGH(block_part1, 4, 43); + + // Here, we should really multiply by 17 instead of 16. This can + // be done by just copying the four lower bits to the upper ones + // while keeping the lower bits. + avg_color[0] |= (avg_color[0] <<4); + avg_color[1] |= (avg_color[1] <<4); + avg_color[2] |= (avg_color[2] <<4); + + table = GETBITSHIGH(block_part1, 3, 36) << 1; + pixel_indices_MSB = GETBITS(block_part2, 16, 31); + pixel_indices_LSB = GETBITS(block_part2, 16, 15); + + if( (flipbit) == 0 ) + { + // We should not flip + shift=8; + for(int x=startx+2; x<startx+4; x++) + { + for(int y=starty; y<starty+4; y++) + { + index = ((pixel_indices_MSB >> shift) & 1) << 1; + index |= ((pixel_indices_LSB >> shift) & 1); + shift++; + index=unscramble[index]; + + r=clampUByte(avg_color[0]+compressParams[table][index]); + g=clampUByte(avg_color[1]+compressParams[table][index]); + b=clampUByte(avg_color[2]+compressParams[table][index]); + STORE_RGB(dstImg, width, x, y, r, g, b); + } + } + } + else + { + // We should flip + shift=2; + for(int x=startx; x<startx+4; x++) + { + for(int y=starty+2; y<starty+4; y++) + { + index = ((pixel_indices_MSB >> shift) & 1) << 1; + index |= ((pixel_indices_LSB >> shift) & 1); + shift++; + index=unscramble[index]; + + r=clampUByte(avg_color[0]+compressParams[table][index]); + g=clampUByte(avg_color[1]+compressParams[table][index]); + b=clampUByte(avg_color[2]+compressParams[table][index]); + STORE_RGB(dstImg, width, x, y, r, g, b); + } + shift += 2; + } + } + + } + else + { + // We have diffbit = 1. + + // 63 62 61 60 59 58 57 56 55 54 53 52 51 50 49 48 47 46 45 44 43 42 41 40 39 38 37 36 35 34 33 32 + // --------------------------------------------------------------------------------------------------- + // | base col1 | dcol 2 | base col1 | dcol 2 | base col 1 | dcol 2 | table | table |diff|flip| + // | R1' (5 bits) | dR2 | G1' (5 bits) | dG2 | B1' (5 bits) | dB2 | cw 1 | cw 2 |bit |bit | + // --------------------------------------------------------------------------------------------------- + // + // + // c) bit layout in bits 31 through 0 (in both cases) + // + // 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 + // -------------------------------------------------------------------------------------------------- + // | most significant pixel index bits | least significant pixel index bits | + // | p| o| n| m| l| k| j| i| h| g| f| e| d| c| b| a| p| o| n| m| l| k| j| i| h| g| f| e| d| c | b | a | + // -------------------------------------------------------------------------------------------------- + + + // First decode left part of block. + enc_color1[0]= GETBITSHIGH(block_part1, 5, 63); + enc_color1[1]= GETBITSHIGH(block_part1, 5, 55); + enc_color1[2]= GETBITSHIGH(block_part1, 5, 47); + + + // Expand from 5 to 8 bits + avg_color[0] = (enc_color1[0] <<3) | (enc_color1[0] >> 2); + avg_color[1] = (enc_color1[1] <<3) | (enc_color1[1] >> 2); + avg_color[2] = (enc_color1[2] <<3) | (enc_color1[2] >> 2); + + + table = GETBITSHIGH(block_part1, 3, 39) << 1; + + unsigned int pixel_indices_MSB, pixel_indices_LSB; + + pixel_indices_MSB = GETBITS(block_part2, 16, 31); + pixel_indices_LSB = GETBITS(block_part2, 16, 15); + + if( (flipbit) == 0 ) + { + // We should not flip + shift = 0; + for(int x=startx; x<startx+2; x++) + { + for(int y=starty; y<starty+4; y++) + { + index = ((pixel_indices_MSB >> shift) & 1) << 1; + index |= ((pixel_indices_LSB >> shift) & 1); + shift++; + index=unscramble[index]; + + r=clampUByte(avg_color[0]+compressParams[table][index]); + g=clampUByte(avg_color[1]+compressParams[table][index]); + b=clampUByte(avg_color[2]+compressParams[table][index]); + STORE_RGB(dstImg, width, x, y, r, g, b); + } + } + } + else + { + // We should flip + shift = 0; + for(int x=startx; x<startx+4; x++) + { + for(int y=starty; y<starty+2; y++) + { + index = ((pixel_indices_MSB >> shift) & 1) << 1; + index |= ((pixel_indices_LSB >> shift) & 1); + shift++; + index=unscramble[index]; + + r=clampUByte(avg_color[0]+compressParams[table][index]); + g=clampUByte(avg_color[1]+compressParams[table][index]); + b=clampUByte(avg_color[2]+compressParams[table][index]); + STORE_RGB(dstImg, width, x, y, r, g, b); + } + shift+=2; + } + } + + + // Now decode right part of block. + + + diff[0]= GETBITSHIGH(block_part1, 3, 58); + diff[1]= GETBITSHIGH(block_part1, 3, 50); + diff[2]= GETBITSHIGH(block_part1, 3, 42); + + enc_color2[0]= enc_color1[0] + diff[0]; + enc_color2[1]= enc_color1[1] + diff[1]; + enc_color2[2]= enc_color1[2] + diff[2]; + + // Extend sign bit to entire byte. + diff[0] = (diff[0] << 5); + diff[1] = (diff[1] << 5); + diff[2] = (diff[2] << 5); + diff[0] = diff[0] >> 5; + diff[1] = diff[1] >> 5; + diff[2] = diff[2] >> 5; + + // Calculale second color + enc_color2[0]= enc_color1[0] + diff[0]; + enc_color2[1]= enc_color1[1] + diff[1]; + enc_color2[2]= enc_color1[2] + diff[2]; + + // Expand from 5 to 8 bits + avg_color[0] = (enc_color2[0] <<3) | (enc_color2[0] >> 2); + avg_color[1] = (enc_color2[1] <<3) | (enc_color2[1] >> 2); + avg_color[2] = (enc_color2[2] <<3) | (enc_color2[2] >> 2); + + + table = GETBITSHIGH(block_part1, 3, 36) << 1; + pixel_indices_MSB = GETBITS(block_part2, 16, 31); + pixel_indices_LSB = GETBITS(block_part2, 16, 15); + + if( (flipbit) == 0 ) + { + // We should not flip + shift=8; + for(int x=startx+2; x<startx+4; x++) + { + for(int y=starty; y<starty+4; y++) + { + index = ((pixel_indices_MSB >> shift) & 1) << 1; + index |= ((pixel_indices_LSB >> shift) & 1); + shift++; + index=unscramble[index]; + + r=clampUByte(avg_color[0]+compressParams[table][index]); + g=clampUByte(avg_color[1]+compressParams[table][index]); + b=clampUByte(avg_color[2]+compressParams[table][index]); + STORE_RGB(dstImg, width, x, y, r, g, b); + } + } + } + else + { + // We should flip + shift=2; + for(int x=startx; x<startx+4; x++) + { + for(int y=starty+2; y<starty+4; y++) + { + index = ((pixel_indices_MSB >> shift) & 1) << 1; + index |= ((pixel_indices_LSB >> shift) & 1); + shift++; + index=unscramble[index]; + + r=clampUByte(avg_color[0]+compressParams[table][index]); + g=clampUByte(avg_color[1]+compressParams[table][index]); + b=clampUByte(avg_color[2]+compressParams[table][index]); + STORE_RGB(dstImg, width, x, y, r, g, b); + } + shift += 2; + } + } + } +} + + +static void DecompressETC_RGB4 ( int xblocks, int yblocks, int destWidth, const UInt32* m_pCompBytes, UInt32* m_pDecompBytes ) +{ + const UInt8* srcPtr = (const UInt8*)m_pCompBytes; + UInt32* dstPtr = m_pDecompBytes; + + for (int y = 0; y < yblocks; y++) + { + for (int x = 0; x < xblocks; x++) + { + UInt32 part1 = (srcPtr[0]<<24) | (srcPtr[1]<<16) | (srcPtr[2]<<8) | (srcPtr[3]<<0); + UInt32 part2 = (srcPtr[4]<<24) | (srcPtr[5]<<16) | (srcPtr[6]<<8) | (srcPtr[7]<<0); + decompressBlockDiffFlip(part1, part2, dstPtr, destWidth, x*4, y*4); + srcPtr += 8; + } + } +} + +#endif // HAS_ETC_DECOMPRESSOR + + +#if HAS_ATC_DECOMPRESSOR + +template<int textureFormat> +static void DecompressATC ( int xblocks, int yblocks, int destWidth, const UInt32* m_pCompBytes, UInt32* m_pDecompBytes ) +{ + bool doAlpha = (textureFormat == kTexFormatATC_RGBA8); + + TQonvertImage src, dst; + src.nWidth = xblocks * 4; + src.nHeight = yblocks * 4; + src.nFormat = doAlpha ? Q_FORMAT_ATC_RGBA_INTERPOLATED_ALPHA : Q_FORMAT_ATC_RGB; + src.pFormatFlags = NULL; + src.nDataSize = CalculateImageSize (src.nWidth, src.nHeight, textureFormat); + src.pData = (unsigned char*)m_pCompBytes; + + dst.nWidth = destWidth; + dst.nHeight = yblocks * 4; + dst.nFormat = Q_FORMAT_RGBA_8888; + dst.pFormatFlags = NULL; + dst.nDataSize = CalculateImageSize (dst.nWidth, dst.nHeight, kTexFormatRGBA32); + dst.pData = (unsigned char*)m_pDecompBytes; + + int nRet = Qonvert(&src, &dst); + if (nRet != Q_SUCCESS) + { + ErrorStringMsg("DecompressETC2 failed with nRet = %i", nRet); + } +} + +#endif // HAS_ATC_DECOMPRESSOR + +#if HAS_ASTC_DECOMPRESSOR + +// ASTC decoding + +#include "External/astcenc/Source/astc_codec_internals.h" + +// Needed by astcenc +void astc_codec_internal_error(const char *filename, int linenum) +{ + printf_console("ASTCenc: Internal error: File=%s Line=%d\n", filename, linenum); +} +// Define an unlink() function in terms of the Unity DeleteFile function. +int astc_codec_unlink(const char *filename) +{ +/* bool res = DeleteFile(filename); + return (res ? 0 : -1);*/ + + // Not used in runtime + return -1; +} +int rgb_force_use_of_hdr = 0; +int alpha_force_use_of_hdr = 0; +int perform_srgb_transform = 0; +int print_tile_errors = 0; + +// TODO: Make threadsafe when we have multithreaded renderer. Doesn't really hurt to do multiple inits though. +static int astcenc_initialized = 0; + +static void DecompressASTC(const UInt32 *srcData, int destWidth, int destHeight, UInt32 *destData, int blockWidth, int blockHeight) +{ + swizzlepattern swz_decode = { 0, 1, 2, 3 }; + + if(!astcenc_initialized) + { + // initialization routines + prepare_angular_tables(); + build_quantization_mode_table(); + astcenc_initialized = 1; + } + + + astc_codec_image *img = allocate_image(8, destWidth, destHeight, 1, 0); + initialize_image(img); + + const int zblocks = 1; + const int yblocks = CeilfToInt((float)destHeight / (float)blockHeight); + const int xblocks = CeilfToInt((float)destWidth / (float)blockWidth); + + const int xdim = blockWidth; + const int ydim = blockHeight; + const int zdim = 1; + + int x, y, z; + + imageblock pb; + for (z = 0; z < zblocks; z++) + for (y = 0; y < yblocks; y++) + for (x = 0; x < xblocks; x++) + { + int offset = (((z * yblocks + y) * xblocks) + x) * 16; + uint8_t *bp = ((uint8_t *)srcData) + offset; + physical_compressed_block pcb = *(physical_compressed_block *) bp; + symbolic_compressed_block scb; + physical_to_symbolic(xdim, ydim, zdim, pcb, &scb); + decompress_symbolic_block(DECODE_LDR, xdim, ydim, zdim, x * xdim, y * ydim, z * zdim, &scb, &pb); + write_imageblock(img, &pb, xdim, ydim, zdim, x * xdim, y * ydim, z * zdim, swz_decode); + } + + for(y = 0; y < destHeight; y++) + { + memcpy(&destData[y*destWidth], img->imagedata8[0][y], destWidth * 4); + } + destroy_image(img); + +} + +#endif // HAS_ASTC_DECOMPRESSOR + +// ------------------------------------------------------------------------ +// Common + + +bool DecompressNativeTextureFormatWithMipLevel( TextureFormat srcFormat, int srcWidth, int srcHeight, int mipLevel, const UInt32* sourceData, + int destWidth, int destHeight, UInt32* destData ) +{ + +#if HAS_FLASH_ATF_DECOMPRESSOR + // Flash requires that we pass the mipLevel explicitly + if (IsCompressedFlashATFTextureFormat(srcFormat)) + { + DecompressFlashATFTexture((const UInt8*)sourceData, destWidth, destHeight, mipLevel, (UInt8*)destData); + return true; + } +#endif + + return DecompressNativeTextureFormat (srcFormat, srcWidth, srcHeight, (UInt32*)sourceData, destWidth, destHeight, destData); +} + + + +bool DecompressNativeTextureFormat( TextureFormat srcFormat, int srcWidth, int srcHeight, const UInt32* sourceData, + int destWidth, int destHeight, UInt32* destData ) +{ + Assert( IsAnyCompressedTextureFormat(srcFormat) ); + Assert( destWidth >= srcWidth && destHeight >= srcHeight ); + Assert( destWidth % 4 == 0 && destHeight % 4 == 0 ); + +#if UNITY_XENON || HAS_DXT_DECOMPRESSOR || HAS_ETC_DECOMPRESSOR || HAS_ATC_DECOMPRESSOR + int xblocks = (srcWidth + 3) / 4; + int yblocks = (srcHeight + 3) / 4; +#endif + + const UInt32* srcData = sourceData; + + #if UNITY_XENON + UInt32 dataSize = (xblocks * yblocks * ((kTexFormatDXT1 == srcFormat) ? 8 : 16)) >> 1; + UInt16 *s = (UInt16*)srcData; + UInt16 *d = new UInt16[dataSize]; + for(int i=0;i<dataSize;i++) + d[i]=GetByteSwap16(s[i]); + srcData = (UInt32*)d; + #endif + + #if PRINT_DECOMPRESSION_TIMES + double t0 = GetTimeSinceStartup(); + #endif + + + switch( srcFormat ) + { + #if HAS_DXT_DECOMPRESSOR + case kTexFormatDXT1: DecompressDXT1( xblocks, yblocks, destWidth, srcData, destData ); break; + case kTexFormatDXT3: DecompressDXT3( xblocks, yblocks, destWidth, srcData, destData ); break; + case kTexFormatDXT5: DecompressDXT5( xblocks, yblocks, destWidth, srcData, destData ); break; + #endif + + #if HAS_ETC_DECOMPRESSOR + case kTexFormatETC_RGB4: DecompressETC_RGB4(xblocks, yblocks, destWidth, srcData, destData ); break; + #endif + + #if HAS_ATC_DECOMPRESSOR + case kTexFormatATC_RGB4: DecompressATC<kTexFormatATC_RGB4>(xblocks, yblocks, destWidth, srcData, destData); break; + case kTexFormatATC_RGBA8: DecompressATC<kTexFormatATC_RGBA8>(xblocks, yblocks, destWidth, srcData, destData); break; + #endif + + case kTexFormatETC2_RGB: DecompressETC2_RGB8 ((UInt8*)destData, (const UInt8*)srcData, destWidth, destHeight); break; + case kTexFormatETC2_RGBA1: DecompressETC2_RGB8_A1 ((UInt8*)destData, (const UInt8*)srcData, destWidth, destHeight); break; + case kTexFormatETC2_RGBA8: DecompressETC2_RGBA8 ((UInt8*)destData, (const UInt8*)srcData, destWidth, destHeight); break; + + #if HAS_PVRTC_DECOMPRESSOR + case kTexFormatPVRTC_RGB4: + case kTexFormatPVRTC_RGBA4: DecompressPVRTC<false,true>( reinterpret_cast<const PVRTCBlock*>(srcData), srcWidth, srcHeight, reinterpret_cast<unsigned char*> (destData) ); break; + + case kTexFormatPVRTC_RGB2: + case kTexFormatPVRTC_RGBA2: DecompressPVRTC<true,true>( reinterpret_cast<const PVRTCBlock*>(srcData), srcWidth, srcHeight, reinterpret_cast<unsigned char*> (destData) ); break; + #endif + + #if HAS_FLASH_ATF_DECOMPRESSOR + case kTexFormatFlashATF_RGB_DXT1: + case kTexFormatFlashATF_RGB_JPG: + case kTexFormatFlashATF_RGBA_JPG: + DecompressFlashATFTexture ((const UInt8*)srcData, destWidth, destHeight, 0, (UInt8*)destData); + break; + #endif + +#if HAS_ASTC_DECOMPRESSOR +#define DO_ASTC(bx,by) case kTexFormatASTC_RGB_##bx##x##by : case kTexFormatASTC_RGBA_##bx##x##by : DecompressASTC(srcData, destWidth, destHeight, destData, bx, by); break + + DO_ASTC(4, 4); + DO_ASTC(5, 5); + DO_ASTC(6, 6); + DO_ASTC(8, 8); + DO_ASTC(10, 10); + DO_ASTC(12, 12); + +#undef DO_ASTC +#endif + + default: + AssertString( "Unknown compressed texture format!" ); + #if UNITY_XENON + delete[] srcData; + #endif + return false; + } + +#if UNITY_XENON + delete[] srcData; +#endif + + #if PRINT_DECOMPRESSION_TIMES + double t1 = GetTimeSinceStartup(); + if (srcWidth >= 512 && srcHeight >= 512) + { + printf_console("Decompress %x size %ix%i fmt %i time %.3fs\n", srcData, srcWidth, srcHeight, (int)srcFormat, t1-t0); + } + #endif + + return true; +} + + + +// ------------------------------------------------------------------------ +// Unit Tests + + + +#if ENABLE_UNIT_TESTS + +#include "External/UnitTest++/src/UnitTest++.h" + +SUITE (ImageDecompressionTests) +{ + #if UNITY_LITTLE_ENDIAN + TEST (DecodeDXT5AlphaPalette8a) + { + UInt32 res[16]; + memset (res, 0xCC, sizeof(res)); + UInt8 b[] = { 17, 13, 177, 109, 155, 54, 105, 82, 255 }; + DecodeAlpha3BitLinear (res, *(DXTAlphaBlock3BitLinear*)b, 4, 0x00FFFFFF); + UInt8 r[] = { 13,14,14,14, 14,14,14,15, 14,14,15,15, 14,15,15,16 }; + for (int i = 0; i < 16; ++i) { + CHECK_EQUAL ((int)r[i], (res[i]&0xFF000000)>>24); + } + } + TEST (DecodeDXT5AlphaPalette8b) + { + UInt32 res[16]; + memset (res, 0xCD, sizeof(res)); + UInt8 b[] = { 251, 5, 179, 109, 113, 54, 107, 84, 255 }; + DecodeAlpha3BitLinear (res, *(DXTAlphaBlock3BitLinear*)b, 4, 0x00FFFFFF); + UInt8 r[] = { 181,75,75,75, 75,216,146,181, 75,75,146,110, 75,251,110,216 }; + for (int i = 0; i < 16; ++i) { + CHECK_EQUAL ((int)r[i], (res[i]&0xFF000000)>>24); + } + } + TEST (DecodeDXT5AlphaPalette6) + { + UInt32 res[16]; + memset (res, 0xCC, sizeof(res)); + UInt8 b[] = { 15, 18, 0, 4, 72, 144, 8, 137, 255 }; + DecodeAlpha3BitLinear (res, *(DXTAlphaBlock3BitLinear*)b, 4, 0x00FFFFFF); + UInt8 r[] = { 15,15,15,16, 15,15,16,16, 15,16,16,17, 15,16,16,17 }; + for (int i = 0; i < 16; ++i) { + CHECK_EQUAL ((int)r[i], (res[i]&0xFF000000)>>24); + } + } + #if HAS_PVRTC_DECOMPRESSOR + TEST (DecodePVRTC_2_16x16) + { + const int kSize = 16; + UInt32 src[kSize*kSize*2/32] = { + 0xeeeeeeee, 0x83eed400, 0xeeeeeeee, 0x8befb006, 0xfefefefe, 0xeed9801e, 0x00fefefe, 0xcf18801e, + 0x0eeeeeee, 0x9ff4fc00, 0x00fefefe, 0xaff58000, 0x00ffffff, 0x83add404, 0x00ffffff, 0xb77d8000, + }; + UInt32 expected[kSize*kSize] = { + 0xff3f002b,0xffadeb59,0xffa3f24b,0xff99f83b,0xff000056,0xff99f83b,0xffa3f24b,0xffadeb59,0xff3f002b,0xffc0dd77,0xffcbd787,0xffd4d095,0xffdecaa5,0xffd4d095,0xffcbd787,0xffc0dd77, + 0xff5f0040,0xffa0e756,0xff96ef40,0xff8cf72b,0xff000081,0xff8cf72b,0xff96ef40,0xffa0e756,0xff5f0040,0xffb6d780,0xffc0cf96,0xffcbc7ac,0xffd6bfc1,0xffcbc7ac,0xffc0cf96,0xffb6d780, + 0xff7f0056,0xff95e353,0xff8aed37,0xff7ef61b,0xff0000ad,0xff7ef61b,0xff8aed37,0xff95e353,0xff7f0056,0xffacd18b,0xffb7c8a7,0xffc2bec2,0xffceb5de,0xffc2bec2,0xffb7c8a7,0xffacd18b, + 0xff86004d,0xff95e44f,0xff8bee36,0xff7ff61c,0xff0c009a,0xff7ff61c,0xff8bee36,0xff95e44f,0xff86004d,0xffabd381,0xffb6cb9b,0xffc0c1b4,0xffccb9ce,0xffc0c1b4,0xffb6cb9b,0xffabd381, + 0xff8c0044,0xff96e74c,0xff8cef35,0xff81f71e,0xff180088,0xff81f71e,0xff8cef35,0xff96e74c,0xff8c0044,0xffabd679,0xffb5ce90,0xffbfc6a7,0xffcabdbd,0xffbfc6a7,0xffb5ce90,0xffabd679, + 0xff92003a,0xff96e848,0xff8df034,0xff82f71f,0xff250075,0xff82f71f,0xff8df034,0xff96e848,0xff92003a,0xffaad870,0xffb4d185,0xffbdc998,0xffc8c1ad,0xffbdc998,0xffb4d185,0xffaad870, + 0xff980031,0xff97ea45,0xff8ef133,0xff85f822,0xff310063,0xff85f822,0xff8ef133,0xff97ea45,0xff980031,0xffaadb68,0xffb3d479,0xffbccd8b,0xffc6c69c,0xffbccd8b,0xffb3d479,0xffaadb68, + 0xff76005a,0xff95ed3c,0xff90f331,0xff8bf926,0xff25008a,0xff8bf926,0xff90f331,0xff95ed3c,0xff76005a,0xff8a004e,0xff9e0043,0xffb30036,0xffc8002b,0xffb30036,0xff9e0043,0xff8a004e, + 0xff540084,0xff93f134,0xff92f62f,0xff91fa2a,0xff1800b1,0xff91fa2a,0xff92f62f,0xff93f134,0xff94ed39,0xff95e83e,0xff96e344,0xff97de49,0xff98da4e,0xff97de49,0xff96e344,0xff95e83e, + 0xff3200ad,0xff91f52c,0xff94f82d,0xff97fb2e,0xff0c00d8,0xff97fb2e,0xff94f82d,0xff91f52c,0xff8ef22b,0xff8bee2a,0xff88eb29,0xff85e828,0xff81e427,0xff85e828,0xff88eb29,0xff8bee2a, + 0xff1000d6,0xff8ff924,0xff96fb2b,0xff9dfd32,0xff0000ff,0xff9dfd32,0xff96fb2b,0xff8ff924,0xff88f71c,0xff80f515,0xff79f30e,0xff72f107,0xff6bef00,0xff72f107,0xff79f30e,0xff80f515, + 0xff0c00a0,0xff9cf732,0xff9ffa37,0xffa2fc3c,0xff0000bf,0xff0300b7,0xff0600b0,0xff0900a8,0xff0c00a0,0xff0f0098,0xff120091,0xff150089,0xff180081,0xff150089,0xff120091,0xff0f0098, + 0xff08006b,0xffaaf642,0xffaaf945,0xffa9fc47,0xffa9ff4a,0xffa9fc47,0xffaaf945,0xffaaf642,0xffabf33f,0xffabf03c,0xffaced3a,0xffacea37,0xffade735,0xffacea37,0xffaced3a,0xffabf03c, + 0xff040035,0xffb7f451,0xffb3f851,0xffaffb51,0xffabff52,0xffaffb51,0xffb3f851,0xffb7f451,0xffbcf151,0xffc0ed50,0xffc4ea50,0xffc9e550,0xffcee250,0xffc9e550,0xffc4ea50,0xffc0ed50, + 0xff000000,0xffc6f360,0xffbdf75e,0xffb5fb5c,0xffadff5a,0xffb5fb5c,0xffbdf75e,0xffc6f360,0xffceef63,0xffd6eb65,0xffdee767,0xffe7e269,0xffefde6b,0xffe7e269,0xffdee767,0xffd6eb65, + 0xff1f0015,0xff17001a,0xff0f001f,0xff070025,0xff00002b,0xff070025,0xff0f001f,0xff17001a,0xff1f0015,0xff27000f,0xff2f000a,0xff370005,0xff3f0000,0xff370005,0xff2f000a,0xff27000f, + }; + UInt32 res[kSize*kSize]; + DecompressPVRTC<true,true> (reinterpret_cast<const PVRTCBlock*>(src), kSize, kSize, reinterpret_cast<UInt8*>(res)); + CHECK_ARRAY_EQUAL (expected, res, kSize*kSize); + } + TEST (DecodePVRTC_4_16x16) + { + const int kSize = 16; + UInt32 src[kSize*kSize*4/32] = { + 0x32323230,0x7faa30e7,0x32323232,0x7fbc40f9,0x03030303,0x050230f6,0x03030303,0x060330f4, + 0x32323232,0x7faa40f7,0xa802f232,0xffff30e7,0xff030303,0x0f0040e6,0xaa00ff00,0xff9f40e9, + 0x0303035b,0x300f6aca,0xff030303,0x300f68ca,0x409094aa,0x68af5bba,0xff000040,0x200f58ca, + 0xff000000,0x2c0140e6,0xaa00ff00,0xffff41db,0xff000000,0x1c0140e8,0xaa00ff00,0xffff40bb, + }; + UInt32 expected[kSize*kSize] = { + 0x7f92d62f,0x727ee217,0xf6d2d6ff,0x6a70f100,0xbbb1a5d4,0x727af500,0x777ff700,0x838eef17,0x99c67994,0xa2cca070,0xadd1b56a,0xaed1b971,0xbbe2b688,0xc1e2c193,0xc8e2cc9f,0xc6d4d19f, + 0x00b6cb8d,0x747ee223,0xf2bbc1ff,0x686df200,0x998a7dbe,0x6c72f800,0x6e75fb00,0x8187f121,0x66a93c5e,0xa7aadc64,0xbbbbd286,0xb4bbcd88,0xaebbc88a,0xb2d2b58c,0xb5d2be98,0xc2d0c49e, + 0x00b1c192,0x777fe22f,0xeea5adff,0x666bf300,0x776356a9,0x666bfb00,0x666bff00,0x7f7ff32b,0x338c0029,0xb2a9da81,0xccbdcead,0xc3bdcab1,0xbbbdc6b5,0xb2bdc1b9,0xb6d2b7aa,0xbdccb79e, + 0x00b6b986,0x7d89e72c,0xeeadb1ff,0x6c72f600,0x776958ab,0x6868fc00,0x6663ff00,0x7f79f329,0x338e002b,0xb2a7da7b,0xccbdcea5,0xc3bdcba8,0xbbbdc8ab,0xb2bdc4ae,0xaabdc1b1,0xa7c1b588, + 0x00bbb27a,0x8392eb29,0xeeb5b5ff,0x7279f900,0x776f5aad,0x6a65fd00,0x665aff00,0x7f73f327,0x3390002d,0xb2a5da75,0xccbdce9c,0xc3bdcc9e,0xbbbdcaa0,0xb2bdc8a2,0xaabdc6a5,0x9ec4ac78, + 0x00c0aa6e,0x8a9bef26,0xeebdb9ff,0x7980fc00,0x77755caf,0x6c61fe00,0x6652ff00,0x7f6df325,0x3392002f,0xb2a2da6f,0xccbdce94,0xc3bdcd95,0xbbbdcc96,0xb2bdcb97,0xaabdca98,0x9fb2d672, + 0x00c5a262,0x90a5f323,0xeec6bdff,0x7f88ff00,0x777b5eb1,0x6e5eff00,0x664aff00,0x7f67f323,0x33940031,0xb2a0da69,0xccbdce8c,0xc3bdce8c,0xbbbdce8c,0xb2bdce8c,0xaabdce8c,0xa1b5da69, + 0x00afa366,0x8e99f519,0xeebdb9ff,0x8180fe00,0x77715cc4,0x7461fc00,0x6e52fb00,0x8168f219,0x2e71005e,0x46970049,0x5dbd0033,0x55bd0033,0x4cbd0033,0x44bd0033,0x3bbd0033,0x68bd2e66, + 0x009aa46b,0x8c8ef711,0xeeb5b5ff,0x8379fd00,0x77675ad8,0x7b65f900,0x775af700,0x8369f111,0x9077eb23,0x9d86e434,0xaa94de46,0xa59ade46,0xa1a0de46,0x9da7de46,0x99adde46,0x94a2e734, + 0x0084a56f,0x8a82f908,0xeeadb1ff,0x8572fc00,0x775c58ec,0x8168f600,0x7f63f300,0x856af008,0x8c71ed11,0x9278ea19,0x997fe723,0x9689e723,0x9492e723,0x929be723,0x90a5e723,0x8e99ed19, + 0x006fa673,0x8877fb00,0xeea5adff,0x886bfb00,0x775256ff,0x886bf300,0x886bef00,0x886bef00,0x886bef00,0x886bef00,0x886bef00,0x8877ef00,0x8884ef00,0x8890ef00,0x889cef00,0x8890f300, + 0x0085b776,0x8178f500,0xf2bbc1ff,0x816df800,0x997d7dff,0x6c5e5bff,0x3f3f39ff,0x4c433af6,0x59463ced,0x66493de3,0x724c3fda,0x6c4c3fda,0x664c3fda,0x5f4c3fda,0x594c3fda,0x7f685fe3, + 0x009bc979,0x7b7aef00,0xf6d2d6ff,0x7b70f500,0x7f75f300,0x837af100,0x887fef00,0x8884ed02,0x8888eb04,0x888ce906,0x8890e708,0x8896e206,0x889cde04,0x88a2da02,0x88a9d600,0x8399de00, + 0x00b1d97c,0x747be900,0xfae9ebff,0xebdedbff,0xddd4ccff,0xcecabcff,0xbfbfadff,0xc3c0b1fc,0xc7c1b6f9,0xccc2baf6,0xd0c3bff3,0xcec3bff3,0xccc3bff3,0xc9c3bff3,0xc7c3bff3,0xd4cdcaf6, + 0x00c7ea7f,0x6e7de200,0x666bef00,0x6e75ef00,0x777fef00,0x7f8aef00,0x8894ef00,0x889ceb04,0x88a5e708,0x88ade20c,0x88b5de10,0x88b5d60c,0x88b5ce08,0x88b5c604,0x88b5bd00,0x7fa2ca00, + 0x7b91d617,0x00b5e681,0x00aaed7f,0x00a9e57a,0x00a9de75,0x00a8d76f,0x00a9d06a,0x00b5cf6e,0x00c1ce72,0x00cecd76,0x00dacc7b,0x00dace7e,0x00dad082,0x00dad185,0x00dad488,0x00cdda86, + }; + UInt32 res[kSize*kSize]; + DecompressPVRTC<false,true> (reinterpret_cast<const PVRTCBlock*>(src), kSize, kSize, reinterpret_cast<UInt8*>(res)); + CHECK_ARRAY_EQUAL (expected, res, kSize*kSize); + } + TEST (DecodePVRTC_4_8x8) + { + const int kSize = 8; + UInt32 src[kSize*kSize*4/32] = { + 0x4c4c4c4c,0x63fb3494,0x00fc4c4c,0x68fc2352,0xa9fefefe,0x5bac1078,0x00ff5555,0x68dc2072, + }; + UInt32 expected[kSize*kSize] = { + 0x444a751c,0xc7bff170,0x55357739,0x7a6fa444,0x9394b95b,0xbfc3d493,0xbbc6c6a5,0xbfc3d493,0x445e7e1e,0xc5bcee61,0x5d3f8a3d,0x7b77ab41,0x909ab858,0xb8c2cb96,0xb2c6b9b1,0xb8c2cb96, + 0x44738821,0xc3b9eb54,0x664a9c42,0x7e80b33e,0x8ea1b856,0xb2c1c19a,0xaac6adbd,0xb2c1c19a,0x445e7e1e,0xc5bcee61,0x5d3f8a3d,0x7b77ab41,0x7282a141,0x87a2ab63,0x7faa9e6e,0x87a2ab63, + 0x444a751c,0xc7bff170,0x55357739,0x7a6fa444,0x73769d41,0x6c7d983f,0x6685923d,0x6c7d983f,0x44356c1a,0xc9c2f47d,0x4c2b6535,0x78669c47,0x756a9a42,0x716d983d,0x6e719639,0x716d983d, + 0x44216318,0xccc6f78c,0xccc6ff8c,0xccc6f78c,0xccc6ef8c,0xccc6e78c,0xccc6de8c,0xccc6e78c,0x44356c1a,0x48306828,0x4c2b6535,0x48306828,0x44356c1a,0x3f3a6f0d,0x3b3f7300,0x3f3a6f0d, + }; + UInt32 res[kSize*kSize]; + DecompressPVRTC<false,true> (reinterpret_cast<const PVRTCBlock*>(src), kSize, kSize, reinterpret_cast<UInt8*>(res)); + CHECK_ARRAY_EQUAL (expected, res, kSize*kSize); + } + TEST (DecodePVRTC_4_8x16) + { + const int kSizeX = 8; + const int kSizeY = 16; + UInt32 src[kSizeX*kSizeY*4/32] = { + 0x4c4c4c4c,0x63fb3494,0x00fc4c4c,0x68fc2352,0xa9fefefe,0x5bac1078,0x00ff5555,0x68dc2072, + 0x32323232,0x7faa40f7,0xa802f232,0xffff30e7,0xff030303,0x0f0040e6,0xaa00ff00,0xff9f40e9, + }; + UInt32 expected[kSizeX*kSizeY] = { + 0x5d79bb10,0xe1dcf2aa,0x665ac621,0x9194d24e,0xadb8d47a,0xd8e0d7cd,0xd4e2cade,0xd8e0d7cd,0x5076a118,0xd2cbee7e,0x6652b131,0x878ac246,0x9dadc668,0xc5d1ccb3,0xbfd4bbce,0xc5d1ccb3, + 0x44738821,0xc3b9eb54,0x664a9c42,0x7e80b33e,0x8ea1b856,0xb2c1c19a,0xaac6adbd,0xb2c1c19a,0x445e7e1e,0xc5bcee61,0x5d3f8a3d,0x7b77ab41,0x7282a141,0x87a2ab63,0x7faa9e6e,0x87a2ab63, + 0x444a751c,0xc7bff170,0x55357739,0x7a6fa444,0x73769d41,0x6c7d983f,0x6685923d,0x6c7d983f,0x44356c1a,0xc9c2f47d,0x4c2b6535,0x78669c47,0x756a9a42,0x716d983d,0x6e719639,0x716d983d, + 0x44216318,0xccc6f78c,0xccc6ff8c,0xccc6f78c,0xccc6ef8c,0xccc6e78c,0xccc6de8c,0xccc6e78c,0x55338812,0x5533821b,0x55337d25,0x5533821b,0x55338812,0x55338d09,0x55339200,0x55338d09, + 0x0069a769,0x6646ab12,0xddb5d6c6,0x6646ab12,0xa18ca2c6,0x6646af06,0x6646b100,0x6646af06,0x0063a774,0x7758d309,0xe5adc1e2,0x7758d309,0x8c6f7ce2,0x7758d103,0x7758d000,0x7758d103, + 0x005ea67f,0x886bfb00,0xeea5adff,0x886bfb00,0x775256ff,0x886bf300,0x886bef00,0x886bf300,0x0076b97f,0x816df800,0xf2bbc1ff,0x816df800,0x997d7dff,0x6c5e5bff,0x3f3f39ff,0x6c5e5bff, + 0x008fcc7f,0x7b70f500,0xf6d2d6ff,0x7b70f500,0x7f75f300,0x837af100,0x887fef00,0x837af100,0x00a7de7f,0x7472f200,0xfae9ebff,0xebdedbff,0xddd4ccff,0xcecabcff,0xbfbfadff,0xcecabcff, + 0x00bff17f,0x6e75ef00,0x666bef00,0x6e75ef00,0x777fef00,0x7f8aef00,0x8894ef00,0x7f8aef00,0x6a7cd508,0x00aee670,0x00a8ec6e,0x00aee670,0x00b5e072,0x00bcd974,0x00c3d477,0x00bcd974, + }; + UInt32 res[kSizeX*kSizeY]; + DecompressPVRTC<false,true> (reinterpret_cast<const PVRTCBlock*>(src), kSizeX, kSizeY, reinterpret_cast<UInt8*>(res)); + CHECK_ARRAY_EQUAL (expected, res, kSizeX*kSizeY); + } + TEST (DecodePVRTC_4_16x8) + { + const int kSizeX = 16; + const int kSizeY = 8; + UInt32 src[kSizeX*kSizeY*4/32] = { + 0x4c4c4c4c,0x63fb3494,0x00fc4c4c,0x68fc2352,0xa9fefefe,0x5bac1078,0x00ff5555,0x68dc2072, + 0x32323232,0x7faa40f7,0xa802f232,0xffff30e7,0xff030303,0x0f0040e6,0xaa00ff00,0xff9f40e9, + }; + UInt32 expected[kSizeX*kSizeY] = { + 0x6e5ab31c,0xb8aedc87,0x55357739,0x7a6fa444,0x9394b95b,0xbfc3d493,0xbbc6c6a5,0xc9c9cabb,0x0098c169,0x6668d600,0xf6d2d6ff,0x7b70f500,0xbba9a5ff,0x837af100,0x887fef00,0x7b6dd10e, + 0x725abc1e,0xa89ace75,0x5d3f8a3d,0x7b77ab41,0x909ab858,0xb8c2cb96,0xb2c6b9b1,0xc2c2bbc4,0x009aba6c,0x6a6fd900,0xf2bbc1ff,0x816df800,0x997d7dff,0x8572f200,0x8875ef00,0x7d68d50f, + 0x775ac621,0x9988bf65,0x664a9c42,0x7e80b33e,0x8ea1b856,0xb2c1c19a,0xaac6adbd,0xbbbdadce,0x009cb36f,0x6e77dc00,0xeea5adff,0x886bfb00,0x775256ff,0x886bf300,0x886bef00,0x7f63da10, + 0x725abc1e,0xa89ace75,0x5d3f8a3d,0x7b77ab41,0x7282a141,0x87a2ab63,0x7faa9e6e,0x90a6ac7a,0x009aba6c,0x6a6fd900,0xf2bbc1ff,0x816df800,0x997d7dff,0x6c5e5bff,0x3f3f39ff,0x625d6bd1, + 0x6e5ab31c,0xb8aedc87,0x55357739,0x7a6fa444,0x73769d41,0x6c7d983f,0x6685923d,0x7588a846,0x0098c169,0x6668d600,0xf6d2d6ff,0x7b70f500,0x7f75f300,0x837af100,0x887fef00,0x7b6dd10e, + 0x6a5aaa1a,0xc8c0eb97,0x4c2b6535,0x78669c47,0x756a9a42,0x716d983d,0x6e719639,0x7b7bac42,0x0096c866,0x615fd300,0xfae9ebff,0xebdedbff,0xddd4ccff,0xcecabcff,0xbfbfadff,0xc2bfc1dc, + 0x665aa018,0xd8d4f9a9,0xccc6ff8c,0xccc6f78c,0xccc6ef8c,0xccc6e78c,0xccc6de8c,0xd8d4e7a9,0x0094d063,0x5d58d000,0x666bef00,0x6e75ef00,0x777fef00,0x7f8aef00,0x8894ef00,0x7777c80c, + 0x6a5aaa1a,0x5b438728,0x4c2b6535,0x48306828,0x44356c1a,0x3f3a6f0d,0x3b3f7300,0x484a9300,0x5555b300,0x009fdb72,0x00aaef7f,0x00a8e67f,0x00a7de7f,0x00a5d67f,0x00a4ce7f,0x0098c674, + }; + UInt32 res[kSizeX*kSizeY]; + DecompressPVRTC<false,true> (reinterpret_cast<const PVRTCBlock*>(src), kSizeX, kSizeY, reinterpret_cast<UInt8*>(res)); + CHECK_ARRAY_EQUAL (expected, res, kSizeX*kSizeY); + } + TEST (TwiddleUVPVRTC) + { + // 00000000, 11111111 = 0101010101010101 + CHECK_EQUAL (0x5555, TwiddleUVPVRTC (0x100, 0x100, 0xFF, 0x00)); + // 00011011, 11110000 = 0101011110001010 + CHECK_EQUAL (0x578A, TwiddleUVPVRTC (0x100, 0x100, 0xF0, 0x1B)); + + // 10100000, 1111 = 101001010101 + CHECK_EQUAL (0xA55, TwiddleUVPVRTC (0x10, 0x100, 0xF, 0xA0)); + // 0000, 11101111 = 111001010101 + CHECK_EQUAL (0xE55, TwiddleUVPVRTC (0x100, 0x10, 0xEF, 0x0)); + } + #endif // HAS_PVRTC_DECOMPRESSOR + #endif + +} // SUITE + +#endif // ENABLE_UNIT_TESTS diff --git a/Runtime/Graphics/S3Decompression.h b/Runtime/Graphics/S3Decompression.h new file mode 100644 index 0000000..d29a4bc --- /dev/null +++ b/Runtime/Graphics/S3Decompression.h @@ -0,0 +1,18 @@ +#ifndef S3DECOMPRESSION_H +#define S3DECOMPRESSION_H + +#include "TextureFormat.h" + +// Decompresses into RGBA32 +bool DecompressNativeTextureFormat ( + TextureFormat srcFormat, int srcWidth, int srcHeight, const UInt32* srcData, + int destWidth, int destHeight, UInt32* destData ); + +// Some texture formats also need to know the mipLevel +bool DecompressNativeTextureFormatWithMipLevel( TextureFormat srcFormat, int srcWidth, int srcHeight, int mipLevel, const UInt32* sourceData, + int destWidth, int destHeight, UInt32* destData ); + + +void DecompressDXT1 (int xblocks, int yblocks, int destWidth, const UInt32* m_pCompBytes, UInt32* m_pDecompBytes); + +#endif diff --git a/Runtime/Graphics/ScreenManager.cpp b/Runtime/Graphics/ScreenManager.cpp new file mode 100644 index 0000000..4094338 --- /dev/null +++ b/Runtime/Graphics/ScreenManager.cpp @@ -0,0 +1,201 @@ +#include "UnityPrefix.h" +#include "ScreenManager.h" +#include "Runtime/Shaders/GraphicsCaps.h" +#include "Runtime/GfxDevice/GfxDevice.h" +#include "Runtime/GfxDevice/GfxDeviceSetup.h" + +static ScreenManagerPlatform* gScreenManager = NULL; + +#if UNITY_IPHONE + extern "C" void NotifyAutoOrientationChange(); +#endif + + +void InitScreenManager() +{ + Assert(gScreenManager == NULL); + gScreenManager = new ScreenManagerPlatform(); +} + +void ReleaseScreenManager() +{ + Assert(gScreenManager != NULL); + delete gScreenManager; + gScreenManager = NULL; +} + +ScreenManagerPlatform &GetScreenManager() +{ + Assert(gScreenManager != NULL); + return *gScreenManager; +} + +ScreenManagerPlatform* GetScreenManagerPtr() +{ + return gScreenManager; +} + +ScreenManager::ScreenManager () +{ + m_Width = 0; + m_Height = 0; + m_SwitchResolutionCallback = NULL; + m_CursorInWindow = false; + m_IsFullscreen = false; + m_AllowCursorHide = true; + m_AllowCursorLock = true; + + m_ScreenOrientation = kPortrait; + m_RequestedOrientation = kScreenOrientationUnknown; + + // Do not allow to autorotate by default. + m_EnabledOrientations = 0; +} + +void ScreenManager::RequestResolution (int width, int height, bool fullscreen, int preferredRefreshRate) +{ + m_ResolutionRequest.width = width; + m_ResolutionRequest.height = height; + m_ResolutionRequest.fullScreen = fullscreen ? 1 : 0; + m_ResolutionRequest.refreshRate = preferredRefreshRate; +} + +void ScreenManager::RequestSetFullscreen (bool fullscreen) +{ + m_ResolutionRequest.fullScreen = fullscreen ? 1 : 0; +} + +bool ScreenManager::HasFullscreenRequested () const +{ + return m_ResolutionRequest.fullScreen == 1; +} + +void ScreenManager::SetCursorInsideWindow (bool insideWindow) +{ + m_CursorInWindow = insideWindow; + SetShowCursor(GetShowCursor()); +} + +int ScreenManager::FindClosestResolution (const ScreenManager::Resolutions& resolutions, int width, int height) const +{ + if (resolutions.empty ()) + return -1; + + int maxDistance = std::numeric_limits<int>::max (); + int index = 0; + for (int i=0;i<resolutions.size ();i++) + { + int curWidth = resolutions[i].width; + int curHeight = resolutions[i].height; + int distance = Abs (width - curWidth) + Abs (height - curHeight); + if (distance < maxDistance) + { + index = i; + maxDistance = distance; + } + } + return index; +} + +void ScreenManager::RegisterDidSwitchResolutions (DidSwitchResolutions* resolution) +{ + m_SwitchResolutionCallback = resolution; +} + +void ScreenManager::SetRequestedResolution () +{ + if (m_ResolutionRequest.width != -1 && m_ResolutionRequest.height != -1) + { + SetResolutionImmediate (m_ResolutionRequest.width, m_ResolutionRequest.height, m_ResolutionRequest.IsFullScreen(), m_ResolutionRequest.refreshRate); + m_ResolutionRequest.Reset(); + } + + if (m_ResolutionRequest.fullScreen != -1) + { + SetIsFullScreenImmediate (m_ResolutionRequest.fullScreen ? true : false); + m_ResolutionRequest.fullScreen = -1; + } +} + +void ScreenManager::SetIsFullScreenImmediate (bool fullscreen) +{ + if (fullscreen != IsFullScreen()) + SetResolutionImmediate (GetWidth (), GetHeight (), fullscreen, 0); +} + +ScreenManager::Resolution ScreenManager::GetCurrentResolution() const +{ + Resolution res; + res.width = GetWidth(); + res.height = GetHeight(); + res.refreshRate = 0; + return res; //@TODO +} + +void ScreenManager::SetupScreenManagerEditor( float w, float h ) +{ + m_Width = RoundfToIntPos(w); + m_Height = RoundfToIntPos(h); +} + + +void ScreenManager::SetAllowCursorHide (bool allowHide) +{ + m_AllowCursorHide = allowHide; + if (!m_AllowCursorHide) + SetShowCursor(true); +} + +void ScreenManager::SetAllowCursorLock (bool allowLock) +{ + m_AllowCursorLock = allowLock; + if (!m_AllowCursorLock) + SetLockCursor(false); +} + + +void ScreenManager::SetIsOrientationEnabled(EnabledOrientation orientation, bool enabled) +{ +#if UNITY_IPHONE + NotifyAutoOrientationChange(); +#endif + +#if UNITY_WP8 + // upside down portrait is not available on wp8 + if (orientation == EnabledOrientation::kAutorotateToPortraitUpsideDown) + enabled = false; +#endif + + if (enabled) + m_EnabledOrientations |= orientation; + else + m_EnabledOrientations &= ~orientation; +} + + +#include "Runtime/Misc/PlayerSettings.h" + +void ScreenManager::EnableOrientationsFromPlayerSettings() +{ + SetIsOrientationEnabled(kAutorotateToPortrait, GetPlayerSettings().GetAutoRotationAllowed(0)); + SetIsOrientationEnabled(kAutorotateToPortraitUpsideDown, GetPlayerSettings().GetAutoRotationAllowed(1)); + SetIsOrientationEnabled(kAutorotateToLandscapeRight, GetPlayerSettings().GetAutoRotationAllowed(2)); + SetIsOrientationEnabled(kAutorotateToLandscapeLeft, GetPlayerSettings().GetAutoRotationAllowed(3)); +} + +#define INIT_ORIENT_FROM_PLAYER_SETTINGS_IMPL(name, func) \ +void ScreenManager::name(int playerSettingsOrient) \ +{ \ + switch(playerSettingsOrient) \ + { \ + case 0: func(kPortrait); break; \ + case 1: func(kPortraitUpsideDown); break; \ + case 2: func(kLandscapeRight); break; \ + case 3: func(kLandscapeLeft); break; \ + \ + default: Assert(false && #name "do not accept autorotation."); \ + } \ +} \ + +INIT_ORIENT_FROM_PLAYER_SETTINGS_IMPL(SetConcreteOrientationFromPlayerSettings, SetScreenOrientation); +INIT_ORIENT_FROM_PLAYER_SETTINGS_IMPL(RequestConcreteOrientationFromPlayerSettings, RequestOrientation); diff --git a/Runtime/Graphics/ScreenManager.h b/Runtime/Graphics/ScreenManager.h new file mode 100644 index 0000000..77adf85 --- /dev/null +++ b/Runtime/Graphics/ScreenManager.h @@ -0,0 +1,257 @@ +#pragma once + +#include <vector> + +enum ScreenOrientation +{ + kScreenOrientationUnknown, + + kPortrait, + kPortraitUpsideDown, + kLandscapeLeft, + kLandscapeRight, + + kAutoRotation, + + kScreenOrientationCount +}; + +inline ScreenOrientation PlayerSettingsToScreenOrientation(int defaultScreenOrientation) +{ + switch (defaultScreenOrientation) { + case 0 : + return kPortrait; + case 1 : + return kPortraitUpsideDown; + case 2 : + return kLandscapeRight; + case 3 : + return kLandscapeLeft; + case 4 : + return kAutoRotation; + default: + return kScreenOrientationUnknown; + } +} + +inline int ScreenOrientationToPlayerSettings(ScreenOrientation screenOrientation) +{ + switch (screenOrientation) { + case kPortrait : + return 0; + case kPortraitUpsideDown : + return 1; + case kLandscapeRight : + return 2; + case kLandscapeLeft : + return 3; + case kAutoRotation : + return 4; + default: + return 5; + } +} + + +enum SleepTimeout +{ + kNeverSleep = -1, + kSystemSetting = -2, +}; + +enum EnabledOrientation +{ + kAutorotateToPortrait = 1, + kAutorotateToPortraitUpsideDown = 2, + kAutorotateToLandscapeLeft = 4, + kAutorotateToLandscapeRight = 8 +}; + +#define kResolutionWidth "Screenmanager Resolution Width" +#define kResolutionHeight "Screenmanager Resolution Height" +#define kIsFullScreen "Screenmanager Is Fullscreen mode" +#define kGraphicsQuality "UnityGraphicsQuality" + +class ScreenManager +{ +public: + ScreenManager (); + + void RequestResolution (int width, int height, bool fullscreen, int preferredRefreshRate); + void RequestSetFullscreen (bool fullscreen); + bool HasFullscreenRequested () const; + + virtual void SetRequestedResolution (); + void SetIsFullScreenImmediate (bool fullscreen); + virtual bool SetResolutionImmediate (int /*width*/, int /*height*/, bool /*fullscreen*/, int /*preferredRefreshRate*/) { return false; } + + struct Resolution + { + // Keep in sync with .NET Resolution struct! + int width; + int height; + int refreshRate; + + friend bool operator < (const Resolution& lhs, const Resolution& rhs) + { + if( lhs.width != rhs.width ) + return lhs.width < rhs.width; + return lhs.height < rhs.height; + } + + /** + * Returns true if this resolution is >= given width/height. + * Used to filter out resolutions that are too big for windowed modes. + */ + bool IsTooBigFor( int w, int h ) const + { + return width >= w || height >= h; + } + + bool IsRotated() const { return height > width; } + + Resolution () + { + width = 0; + height = 0; + refreshRate = 0; + } + }; + + struct ResolutionRequest + { + ResolutionRequest() { Reset(); } + + int width; + int height; + int fullScreen; + int refreshRate; + + bool IsFullScreen() const { return fullScreen == 1; } + + void Reset() + { + width = -1; + height = -1; + fullScreen = -1; + refreshRate = -1; + } + }; + + typedef std::vector<Resolution> Resolutions; + virtual Resolutions GetResolutions (int /*preferredRefreshRate*/ = 0, bool /*clampToDesktopRes*/ = false) { return Resolutions(); } + int FindClosestResolution (const ScreenManager::Resolutions& resolutions, int width, int height) const; + + virtual Resolution GetCurrentResolution() const; + + virtual bool GetShowCursor () const { return true; } + virtual void SetShowCursor (bool /*show*/) { } + + virtual bool GetLockCursor () const { return false; } + virtual void SetLockCursor (bool /*lock*/) { } + + virtual bool GetAllowLayeredRendering () const { return true; } + virtual void SetAllowLayeredRendering (bool /*allow*/) { } + + virtual int GetScreenTimeout () const { return kNeverSleep; } + virtual void SetScreenTimeout (int /*value*/) { } + + bool GetAllowCursorHide() const { return m_AllowCursorHide; } + void SetAllowCursorHide (bool allowHide); + + bool GetAllowCursorLock() const { return m_AllowCursorLock; } + void SetAllowCursorLock (bool allowLock); + + // Named *IsFocus to avoid confusion with windows API + virtual bool GetIsFocused() const { return true; } + virtual void SetIsFocused (bool /*focus*/) { } + + virtual bool GetCursorInsideWindow () const { return m_CursorInWindow; } + virtual void SetCursorInsideWindow (bool insideWindow); + + // Do these need to be virtual? + virtual int GetWidth () const { return m_Width; } + virtual int GetHeight () const { return m_Height; } + + int GetWidthAsRequested () const { return m_ResolutionRequest.width == -1 ? GetWidth () : m_ResolutionRequest.width; } + int GetHeightAsRequested () const { return m_ResolutionRequest.height == -1 ? GetHeight () : m_ResolutionRequest.height; } + bool GetFullScreenAsRequested () const { return m_ResolutionRequest.fullScreen == -1 ? IsFullScreen () : m_ResolutionRequest.IsFullScreen (); } + int GetRefreshRateAsRequested () const { return m_ResolutionRequest.refreshRate == -1 ? GetCurrentResolution ().refreshRate : m_ResolutionRequest.refreshRate; } + + virtual float GetDPI () const { return 0.f; } + virtual bool IsFullScreen () const { return m_IsFullscreen; } + + void RequestOrientation(ScreenOrientation value) { m_RequestedOrientation = value; } + ScreenOrientation GetRequestedOrientation() const { return m_RequestedOrientation; } + + virtual ScreenOrientation GetScreenOrientation() const { return m_ScreenOrientation; }; + virtual void SetScreenOrientation (ScreenOrientation value) { m_ScreenOrientation = value; } + + typedef void DidSwitchResolutions (); + void RegisterDidSwitchResolutions (DidSwitchResolutions* resolution); + + virtual void SetupScreenManagerEditor(float w, float h); + + void SetIsOrientationEnabled(EnabledOrientation orientation, bool enabled); + bool GetIsOrientationEnabled(EnabledOrientation orientation) const { return m_EnabledOrientations & orientation; } + + // helpers for connecting ScreenManager and PlayerSettings + void EnableOrientationsFromPlayerSettings(); + void SetConcreteOrientationFromPlayerSettings(int playerSettingsOrient); + void RequestConcreteOrientationFromPlayerSettings(int playerSettingsOrient); + +protected: + ResolutionRequest m_ResolutionRequest; + + DidSwitchResolutions* m_SwitchResolutionCallback; + + bool m_AllowCursorHide; + bool m_AllowCursorLock; + bool m_CursorInWindow; + bool m_IsFullscreen; + + int m_Width; + int m_Height; + + unsigned int m_EnabledOrientations; + ScreenOrientation m_ScreenOrientation; + ScreenOrientation m_RequestedOrientation; +}; + +#if UNITY_PEPPER +#include "PlatformDependent/PepperPlugin/ScreenManagerPepper.h" +#elif UNITY_OSX +#include "PlatformDependent/OSX/ScreenManagerOSX.h" +#elif UNITY_WP8 +#include "PlatformDependent/WP8Player/ScreenManagerWP8.h" +#elif UNITY_METRO +#include "PlatformDependent/MetroPlayer/ScreenManagerMetro.h" +#elif UNITY_WIN +#include "../../PlatformDependent/WinPlayer/ScreenManagerWin.h" +#elif UNITY_XENON +#include "../../PlatformDependent/Xbox360/Source/ScreenManagerXenon.h" +#elif UNITY_PS3 +#include "../../PlatformDependent/PS3Player/ScreenManagerPS3.h" +#elif UNITY_ANDROID +#include "PlatformDependent/AndroidPlayer/ScreenManagerAndroid.h" +#elif UNITY_IPHONE +#include "PlatformDependent/iPhonePlayer/ScreenManagerIPhone.h" +#elif UNITY_LINUX && SUPPORT_X11 +#include "PlatformDependent/Linux/ScreenManagerLinux.h" +#elif UNITY_WII +#include "PlatformDependent/Wii/WiiScreenManager.h" +#elif UNITY_FLASH +#include "PlatformDependent/FlashSupport/cpp/FlashScreenManager.h" +#elif UNITY_BB10 +#include "PlatformDependent/BB10Player/ScreenManagerBB10.h" +#elif UNITY_TIZEN +#include "PlatformDependent/TizenPlayer/ScreenManagerTizen.h" +#else +#define ScreenManagerPlatform ScreenManager +#endif + +void InitScreenManager(); +void ReleaseScreenManager(); +ScreenManagerPlatform &GetScreenManager(); +ScreenManagerPlatform* GetScreenManagerPtr(); + diff --git a/Runtime/Graphics/SpriteFrame.cpp b/Runtime/Graphics/SpriteFrame.cpp new file mode 100644 index 0000000..80c568f --- /dev/null +++ b/Runtime/Graphics/SpriteFrame.cpp @@ -0,0 +1,272 @@ +#include "UnityPrefix.h" +#include "SpriteFrame.h" + +#if ENABLE_SPRITES + +#include "Runtime/Geometry/AABB.h" +#include "Runtime/Graphics/SpriteUtility.h" +#include "Runtime/GfxDevice/GfxDevice.h" +#include "Runtime/Misc/BuildSettings.h" +#if UNITY_EDITOR + #include "Editor/Src/EditorSettings.h" + #include "Editor/Src/SpritePacker/SpritePacker.h" + #include "Runtime/BaseClasses/IsPlaying.h" +#endif + +static const int kMinSpriteDimensionForMesh = 32; +static const float kSpriteAABBDepth = 0.1f; +using namespace Unity; + +IMPLEMENT_CLASS(Sprite) +IMPLEMENT_OBJECT_SERIALIZE(Sprite) + +Sprite::Sprite(MemLabelId label, ObjectCreationMode mode) +: Super(label, mode) +, m_PixelsToUnits(100.0f) +, m_Extrude(0) +#if UNITY_EDITOR +, m_AtlasReady(false) +#endif +{ +#if ENABLE_MULTITHREADED_CODE + m_CurrentCPUFence = 0; + m_WaitOnCPUFence = false; +#endif +} + +Sprite::~Sprite() +{ + WaitOnRenderThreadUse(); + m_IntermediateUsers.Notify( kImNotifyAssetDeleted ); +} + +void Sprite::AwakeFromLoad(AwakeFromLoadMode mode) +{ + Super::AwakeFromLoad(mode); + +#if UNITY_EDITOR + RefreshAtlasRD(); +#endif +} + +template<class TransferFunction> +void Sprite::Transfer(TransferFunction& transfer) +{ + Super::Transfer(transfer); + + TRANSFER(m_Rect); + TRANSFER(m_Offset); + TRANSFER(m_PixelsToUnits); + TRANSFER(m_Extrude); + + TRANSFER_EDITOR_ONLY_HIDDEN(m_AtlasName); + TRANSFER_EDITOR_ONLY_HIDDEN(m_PackingTag); + +#if UNITY_EDITOR + bool atlasPacked = !m_PackingTag.empty(); + if (!atlasPacked) + { + transfer.Transfer(m_RD, "m_RD"); + transfer.Align(); + } + else + { + if (transfer.IsSerializingForGameRelease()) + { + const bool packingEnabled = (GetEditorSettings().GetSpritePackerMode() != EditorSettings::kSPOff); // Make sure packing is not disabled + const bool serializePacked = (packingEnabled && GetIsPacked()); + + // Note: it is not expected that all sprites with atlas hints will be packed. + transfer.Transfer(serializePacked ? m_AtlasRD : m_RD, "m_RD"); + transfer.Align(); + } + else + { + transfer.Transfer(m_RD, "m_RD"); + transfer.Transfer(m_AtlasRD, "m_AtlasRD", kHideInEditorMask); + } + } +#else + transfer.Transfer(m_RD, "m_RD"); + transfer.Align(); +#endif + +#if ENABLE_SPRITECOLLIDER + TRANSFER(m_Poly); +#endif +} + +bool Sprite::GetIsPacked() const +{ +#if UNITY_EDITOR + return (m_AtlasReady != 0); +#else + return m_RD.settings.packed; +#endif +} + +#if UNITY_EDITOR +static bool s_packingAllowedInPlayMode = false; +void Sprite::OnEnterPlaymode() +{ + s_packingAllowedInPlayMode = (GetEditorSettings().GetSpritePackerMode() == EditorSettings::kSPOn); +} +#endif + +const SpriteRenderData& Sprite::GetRenderDataForPlayMode() const +{ +#if UNITY_EDITOR + return GetRenderData(s_packingAllowedInPlayMode && IsWorldPlaying()); +#else + return GetRenderData(true); +#endif +} + +const SpriteRenderData& Sprite::GetRenderData(bool getEditorOnlyAtlasRenderDataIfPacked) const +{ +#if UNITY_EDITOR + if (getEditorOnlyAtlasRenderDataIfPacked && GetIsPacked()) + return m_AtlasRD; +#endif + return m_RD; +} + +void Sprite::Initialize(Texture2D* texture, const Rectf& rect, const Vector2f& pivot, float pixelsToUnits, unsigned int extrude, SpriteMeshType meshType) +{ + const bool hasAdvancedVersion = GetBuildSettings().hasAdvancedVersion; + + Assert(texture); + bool generateRenderMesh = (meshType == kSpriteMeshTypeTight); + generateRenderMesh &= (rect.width >= kMinSpriteDimensionForMesh); + generateRenderMesh &= (rect.height >= kMinSpriteDimensionForMesh); + generateRenderMesh &= (texture->GetRawImageData() != NULL); //BUGFIX:569636 - if we can't access pixel data, generate a quad. + generateRenderMesh &= hasAdvancedVersion; //Note: tight mesh is Pro only. + + // Common data + m_Rect = rect; + m_Offset = PivotToOffset(rect, pivot); + m_PixelsToUnits = pixelsToUnits; + m_Extrude = hasAdvancedVersion ? extrude : 0; //Note: extrude is Pro only. + + // Render data + m_RD.texture = texture; + if (generateRenderMesh) + { + Rectf meshRect; + m_RD.GenerateFullMesh(m_Rect, m_Offset, pixelsToUnits, m_Extrude, &meshRect); + m_RD.textureRect = meshRect; + m_RD.textureRect.x += m_Rect.GetPosition().x; + m_RD.textureRect.y += m_Rect.GetPosition().y; + } + else + { + //Note: we could do alpha-trim here. But do we want to lose the texture space in this case? + m_RD.GenerateQuadMesh(m_Rect, m_Offset, pixelsToUnits); + m_RD.textureRect = m_Rect; + } + m_RD.textureRectOffset = m_RD.textureRect.GetPosition() - rect.GetPosition(); +} + +void Sprite::WaitOnRenderThreadUse() +{ +#if ENABLE_MULTITHREADED_CODE + if (m_WaitOnCPUFence) + { + GetGfxDevice().WaitOnCPUFence(m_CurrentCPUFence); + m_WaitOnCPUFence = false; + } +#endif +} + +void Sprite::GenerateOutline(float detail, unsigned char alphaTolerance, bool holeDetection, std::vector<dynamic_array<Vector2f> >& outVertices, int extrudeOverride) +{ + unsigned int extrude = (extrudeOverride < 0) ? m_Extrude : extrudeOverride; + GenerateSpriteOutline(m_RD.texture, m_PixelsToUnits, m_Rect, m_Offset, detail, alphaTolerance, holeDetection, extrude, kPathEmbed, &outVertices); +} + +SpriteRenderData::SpriteRenderData() +: settingsRaw(0) +{ +} + +void SpriteRenderData::GenerateFullMesh(const Rectf& rect, const Vector2f& rectOffset, float pixelsToUnits, unsigned int extrude, Rectf* meshRect) +{ + GenerateSpriteOutline(texture, pixelsToUnits, rect, rectOffset, kSpriteDefaultDetail, kSpriteDefaultAlphaTolerance, true, extrude, kPathEmbed, NULL, &vertices, &indices, meshRect); +} + +void SpriteRenderData::GenerateQuadMesh(const Rectf& rect, const Vector2f& rectOffset, float pixelsToUnits) +{ + const float scaler = 1.0f / pixelsToUnits; + const int texGlWidth = texture->GetGLWidth(); + const int texGlHeight = texture->GetGLHeight(); + + const float halfW = rect.width * 0.5f * scaler; + const float halfH = rect.height * 0.5f * scaler; + const Vector2f offset = rectOffset * scaler; + const Vector4f uv = Vector4f(rect.x / texGlWidth, rect.y / texGlHeight, rect.GetXMax() / texGlWidth, rect.GetYMax() / texGlHeight); + + vertices.resize(4); + vertices[0].pos = Vector3f(-halfW - offset.x, halfH - offset.y, 0.0f); + vertices[0].uv = Vector2f(uv[0], uv[3]); + vertices[1].pos = Vector3f( halfW - offset.x, halfH - offset.y, 0.0f); + vertices[1].uv = Vector2f(uv[2], uv[3]); + vertices[2].pos = Vector3f(-halfW - offset.x, -halfH - offset.y, 0.0f); + vertices[2].uv = Vector2f(uv[0], uv[1]); + vertices[3].pos = Vector3f( halfW - offset.x, -halfH - offset.y, 0.0f); + vertices[3].uv = Vector2f(uv[2], uv[1]); + + indices.resize(6); + indices[0] = 0; + indices[1] = 1; + indices[2] = 2; + indices[3] = 2; + indices[4] = 1; + indices[5] = 3; +} + +AABB Sprite::GetBounds(Vector2f extraOffset) const +{ + Vector2f halfRect(m_Rect.width / m_PixelsToUnits * 0.5f, m_Rect.height / m_PixelsToUnits * 0.5f); + Vector2f offset(m_Offset.x / m_PixelsToUnits, m_Offset.y / m_PixelsToUnits); + + MinMaxAABB minmax; + Vector3f minV(-halfRect.x - offset.x + extraOffset.x, halfRect.y - offset.y + extraOffset.y, kSpriteAABBDepth); + Vector3f maxV( halfRect.x - offset.x + extraOffset.x, -halfRect.y - offset.y + extraOffset.y, -kSpriteAABBDepth); + minmax.Encapsulate(minV); + minmax.Encapsulate(maxV); + return minmax; +} + +Vector2f Sprite::PivotToOffset(const Rectf& rect, const Vector2f& pivot) +{ + const Vector2f alignPos = rect.GetPosition() + rect.GetSize().Scale(pivot); + Vector2f offset = alignPos - rect.GetCenterPos(); + return offset; +} + +#if UNITY_EDITOR +void Sprite::RefreshAtlasRD () +{ + UnityStr atlasName; + const SpriteRenderData* packedRD = (m_PackingTag.empty() ? NULL : SpritePacker::GetPackedSpriteRD(*this, atlasName)); + if (packedRD) + { + WaitOnRenderThreadUse(); + m_AtlasReady = true; + m_AtlasRD = *packedRD; + m_AtlasName = atlasName; + } + else + ClearAtlasRD (); +} + +void Sprite::ClearAtlasRD () +{ + WaitOnRenderThreadUse(); + m_AtlasReady = false; + m_AtlasRD = SpriteRenderData (); + m_AtlasName.clear(); +} +#endif + +#endif //ENABLE_SPRITES diff --git a/Runtime/Graphics/SpriteFrame.h b/Runtime/Graphics/SpriteFrame.h new file mode 100644 index 0000000..e319e2c --- /dev/null +++ b/Runtime/Graphics/SpriteFrame.h @@ -0,0 +1,190 @@ +#pragma once +#include "Configuration/UnityConfigure.h" + +#if ENABLE_SPRITES + +#include "Runtime/BaseClasses/NamedObject.h" +#include "Runtime/Math/Rect.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "Runtime/Graphics/Texture2D.h" +#include "Runtime/Graphics/Polygon2D.h" +#include "Runtime/Shaders/Material.h" +#include "Runtime/Camera/IntermediateUsers.h" + +const float kSpriteDefaultDetail = -1.0f; // if less than zero, automatic lod evaluation will performed +const UInt8 kSpriteDefaultAlphaTolerance = 0; +const UInt32 kSpriteDefaultExtrude = 1; +const UInt8 kSpriteMaxAlphaTolerance = 254; + +class AABB; + +struct SpriteVertex +{ + DECLARE_SERIALIZE_NO_PPTR (SpriteVertex) + + Vector3f pos; + Vector2f uv; +}; + +enum SpritePackingMode +{ + kSPMTight = 0, + kSPMRectangle +}; + +enum SpritePackingRotation +{ + kSPRNone = 0, + // Reserved + kSPRAny = 15 +}; + +enum SpriteMeshType +{ + kSpriteMeshTypeFullRect = 0, + kSpriteMeshTypeTight = 1 +}; + +struct SpriteRenderData +{ + DECLARE_SERIALIZE (SpriteRenderData) + + SpriteRenderData(); + + PPtr<Texture2D> texture; + + // Mesh is scaled according to kTexturePixelsPerUnit + std::vector<SpriteVertex> vertices; + std::vector<UInt16> indices; + + Rectf textureRect; // In pixels (texture space). Invalid if packingMode is Tight. + Vector2f textureRectOffset; // In pixels (texture space). Invalid if packingMode is Tight. + + union + { + struct + { + UInt32 packed : 1; // bool + UInt32 packingMode : 1; // SpritePackingMode + UInt32 packingRotation : 4; // SpritePackingRotation + UInt32 reserved : 26; + } settings; + UInt32 settingsRaw; + }; + + void GenerateQuadMesh(const Rectf& rect, const Vector2f& rectOffset, float pixelsToUnits); + void GenerateFullMesh(const Rectf& rect, const Vector2f& rectOffset, float pixelsToUnits, unsigned int extrude, Rectf* meshRect); +}; + +class Sprite : public NamedObject +{ +public: + REGISTER_DERIVED_CLASS(Sprite, NamedObject) + DECLARE_OBJECT_SERIALIZE(Sprite) + + Sprite(MemLabelId label, ObjectCreationMode mode); + // ~Sprite(); declared-by-macro + + void Initialize(Texture2D* texture, const Rectf& rect, const Vector2f& pivot, float pixelsToUnits, unsigned int extrude, SpriteMeshType meshType); + + virtual void AwakeFromLoad(AwakeFromLoadMode mode); + + // detail [0; 1] where 0 is simplified and 1 is max quality. + void GenerateOutline(float detail, unsigned char alphaTolerance, bool holeDetection, std::vector<dynamic_array<Vector2f> >& outVertices, int extrudeOverride = -1); + + // Common data + Vector2f GetSize() const { return m_Rect.GetSize(); } + Vector2f GetSizeInUnits() const { return m_Rect.GetSize()/m_PixelsToUnits; } + + const Rectf& GetRect() const { return m_Rect; } + const Vector2f& GetOffset() const { return m_Offset; } + float GetPixelsToUnits() const { return m_PixelsToUnits; } + +#if ENABLE_SPRITECOLLIDER + Polygon2D& GetPoly() { return m_Poly; } +#endif + + // Rendering data + // Note: we want live preview of atlases when in editor, but only for rendering in play mode. + // GetRenderData(false) will always return the source texture render data. + // GetRenderData(true) will return the atlas texture render data if it is available, otherwise fall back to source texture render data. + // GetRenderDataForPlayMode() is a convenience function which tries to get atlas texture render data when world is playing. + const SpriteRenderData& GetRenderData(bool getEditorOnlyAtlasRenderDataIfPacked) const; + const SpriteRenderData& GetRenderDataForPlayMode() const; + + bool GetIsPacked() const; + AABB GetBounds(Vector2f extraOffset = Vector2f(0, 0)) const; + +#if UNITY_EDITOR + void SetPackingTag (const std::string& packingTag) { m_PackingTag = packingTag; } + std::string GetPackingTag() const { return m_PackingTag; } + + std::string GetActiveAtlasName() const { return m_AtlasReady ? m_AtlasName : ""; } + Texture2D* GetActiveAtlasTexture() const { return m_AtlasReady ? m_AtlasRD.texture : NULL; } + + // Changing texture import settings will rebuild Sprites and not use atlases until the next atlas rebuild, which will trigger RefreshAtlasRD or ClearAtlasRD. + void RefreshAtlasRD (); + void ClearAtlasRD (); + + static void OnEnterPlaymode (); +#endif + +#if ENABLE_MULTITHREADED_CODE + void SetCurrentCPUFence( UInt32 fence ) { m_CurrentCPUFence = fence; m_WaitOnCPUFence = true; } +#endif + void WaitOnRenderThreadUse(); + + static Vector2f PivotToOffset(const Rectf& rect, const Vector2f& pivot); // [0;1] to rect space + + void AddIntermediateUser( ListNode<IntermediateRenderer>& node ) { m_IntermediateUsers.AddUser(node); } + +private: + // Common data + Rectf m_Rect; // In pixels (originating texture, full rect definition) + Vector2f m_Offset; // In pixels (from center of final rect, defines pivot point based on alignment) + + // Rendering data + SpriteRenderData m_RD; + float m_PixelsToUnits; + unsigned int m_Extrude; + +#if UNITY_EDITOR + int m_AtlasReady; + SpriteRenderData m_AtlasRD; + UnityStr m_AtlasName; + UnityStr m_PackingTag; +#endif + +#if ENABLE_SPRITECOLLIDER + Polygon2D m_Poly; // Collider +#endif + +#if ENABLE_MULTITHREADED_CODE + UInt32 m_CurrentCPUFence; + bool m_WaitOnCPUFence; +#endif + + IntermediateUsers m_IntermediateUsers; // IntermediateRenderer users of this sprite +}; + +template<class TransferFunction> +void SpriteVertex::Transfer(TransferFunction& transfer) +{ + transfer.Transfer(pos, "pos"); + transfer.Transfer(uv, "uv"); +} + +template<class TransferFunction> +void SpriteRenderData::Transfer(TransferFunction& transfer) +{ + TRANSFER(texture); + TRANSFER(vertices); + TRANSFER(indices); + + transfer.Align(); + TRANSFER(textureRect); + TRANSFER(textureRectOffset); + TRANSFER(settingsRaw); +} + +#endif //ENABLE_SPRITES diff --git a/Runtime/Graphics/SpriteUtility.cpp b/Runtime/Graphics/SpriteUtility.cpp new file mode 100644 index 0000000..5c131c3 --- /dev/null +++ b/Runtime/Graphics/SpriteUtility.cpp @@ -0,0 +1,155 @@ +#include "UnityPrefix.h" +#include "SpriteUtility.h" + +#if ENABLE_SPRITES + +#include "Runtime/Geometry/SpriteMeshGenerator.h" + +static const float kSpriteEdgeBias = 1.0f; + +bool GetSpriteMeshRectPixelBounds (const Texture2D& texture, const Rectf& rectInaccurate, int& rectX, int& rectY, int& rectRight, int& rectBottom) +{ + rectX = Floorf(rectInaccurate.x); + rectY = Floorf(rectInaccurate.y); + rectRight = Ceilf(rectInaccurate.GetRight()); + rectBottom = Ceilf(rectInaccurate.GetBottom()); + + // Sanity check to see if out of texture rect or if error is more than 1 pixel for whole-texture sprites. + const bool errorX = (rectX < 0); + const bool errorY = (rectY < 0); + const bool errorR = (rectRight > texture.GetGLWidth() + 1); + const bool errorB = (rectBottom > texture.GetGLHeight() + 1); + const bool hasError = (errorX || errorY || errorR || errorB); + Assert(!hasError); + + // In rare cases when a texture is downscaled to match the max texture size limit it might introduce an odd scaling factor, cause a 1 pixel error and grow beyond the texture size. + rectRight = std::min(rectRight, texture.GetGLWidth()); + rectBottom = std::min(rectBottom, texture.GetGLHeight()); + + return hasError; +} + +void GenerateSpriteOutline(PPtr<Texture2D> texture, float pixelsToUnits, const Rectf& rectInaccurate, const Vector2f& rectOffset, float detail, unsigned char alphaTolerance, bool holeDetection, unsigned int extrude, int simplifyMode, std::vector<dynamic_array<Vector2f> >* outLine, std::vector<SpriteVertex>* outVertices, std::vector<UInt16>* outIndices, Rectf* meshRect) +{ + if(texture.IsNull()) + return; + + const int texW = texture->GetGLWidth(); + const int texH = texture->GetGLHeight(); + + // Figure out what pixels to use. + // If a platform texture size limit is set, rectInaccurate will not define actual pixels. + int rectX, rectY, rectRight, rectBottom; + GetSpriteMeshRectPixelBounds(*texture, rectInaccurate, rectX, rectY, rectRight, rectBottom); + const int rectWidth = rectRight - rectX; + const int rectHeight = rectBottom - rectY; + const float rectAdjustX = rectInaccurate.x - rectX; + const float rectAdjustY = rectInaccurate.y - rectY; + + // Extract rectangle + const int imageSize = rectWidth * rectHeight * sizeof(ColorRGBA32); + ColorRGBA32* imageData = (ColorRGBA32*)UNITY_MALLOC(kMemDefault, imageSize); + { + const int textureSize = texW * texH * sizeof(ColorRGBA32); + ColorRGBA32* texData = (ColorRGBA32*)UNITY_MALLOC(kMemDefault, textureSize); + texture->GetPixels32(0, texData); + + for (int row = 0; row < rectHeight; ++row) + { + ColorRGBA32* dest = imageData + row * rectWidth; + ColorRGBA32* src = texData + (rectY + row) * texW + rectX; + UNITY_MEMCPY(dest, src, sizeof(ColorRGBA32) * rectWidth); + } + + UNITY_FREE(kMemDefault, texData); + } + + // Detect shape + const float hullTolerance = (detail>=0.0f) ? 1.0f - clamp01(detail) : detail; + extrude = clamp<unsigned int>(extrude, 0, 32); + SpriteMeshGenerator smg; + smg.MakeShape(imageData, rectWidth, rectHeight, hullTolerance, alphaTolerance, holeDetection, extrude, kSpriteEdgeBias, simplifyMode); + + // Adjustments for offset and pixel-to-unity scale. + const float scale = 1.0 / pixelsToUnits; + const float scale_x = 1.0 / texW; + const float scale_y = 1.0 / texH; + const Vector2f oxy(float(rectWidth) * 0.5f + rectOffset.x - rectAdjustX, float(rectHeight) * 0.5f + rectOffset.y - rectAdjustY); + + // Fill outline + if (outLine) + { + const std::vector<SpriteMeshGenerator::path>& paths = smg.GetPaths(); + outLine->resize(paths.size()); + + int ci = 0; + for (std::vector<SpriteMeshGenerator::path>::const_iterator p = paths.begin(); p != paths.end(); ++p) + { + const SpriteMeshGenerator::path& path = *p; + const std::vector<SpriteMeshGenerator::vertex>& poly = path.m_path; + dynamic_array<Vector2f> finalPath; + finalPath.reserve(poly.size()); + for (std::vector<SpriteMeshGenerator::vertex>::const_iterator it = poly.begin(); it != poly.end(); ++it) + { + Vector2f polyPoint = it->p; + Vector2f finalPoint((polyPoint.x-oxy.x)*scale, (polyPoint.y-oxy.y)*scale); + finalPath.push_back(finalPoint); + } + (*outLine)[ci++] = finalPath; + } + } + + // Fill mesh + if (outVertices) + { + Assert(outIndices); + + // Decompose outline + std::vector<Vector2f> vertices; + std::vector<int> indices; + smg.Decompose(&vertices, &indices); + + if (indices.size() > 0) + { + outVertices->clear(); + outIndices->clear(); + // Assign indices + std::reverse(indices.begin(), indices.end()); + outIndices->assign(indices.begin(), indices.end()); + + // Assign vertices + outVertices->reserve(vertices.size()); + for (std::vector<SpriteMeshGenerator::vertex>::size_type i = 0; i < vertices.size(); ++i) + { + SpriteVertex v; + v.pos = Vector3f((vertices[i].x-oxy.x)*scale, (vertices[i].y-oxy.y)*scale, 0.0f); + v.uv = Vector2f((vertices[i].x+rectInaccurate.x)*scale_x, (vertices[i].y+rectInaccurate.y)*scale_y); + outVertices->push_back(v); + } + } + } + else + { + Assert(outIndices == NULL); + } + + // Set mesh rect + if (meshRect) + { + if (!smg.FindBounds(*meshRect)) + *meshRect = Rectf(rectX, rectY, rectWidth, rectHeight); + } + + // Clean up + UNITY_FREE(kMemDefault, imageData); +} + +void GetAABBVerticesForSprite (const AABB& aabb, Vector3f* outVertices) +{ + outVertices[0] = aabb.m_Center + Vector3f (-aabb.m_Extent.x, -aabb.m_Extent.y, 0.0f); + outVertices[1] = aabb.m_Center + Vector3f (+aabb.m_Extent.x, -aabb.m_Extent.y, 0.0f); + outVertices[2] = aabb.m_Center + Vector3f (-aabb.m_Extent.x, +aabb.m_Extent.y, 0.0f); + outVertices[3] = aabb.m_Center + Vector3f (+aabb.m_Extent.x, +aabb.m_Extent.y, 0.0f); +} + +#endif //ENABLE_SPRITES diff --git a/Runtime/Graphics/SpriteUtility.h b/Runtime/Graphics/SpriteUtility.h new file mode 100644 index 0000000..aff8491 --- /dev/null +++ b/Runtime/Graphics/SpriteUtility.h @@ -0,0 +1,22 @@ +#pragma once +#include "Configuration/UnityConfigure.h" + +#if ENABLE_SPRITES +#include "Runtime/Graphics/Texture2D.h" +#include "Runtime/Graphics/SpriteFrame.h" + +enum PathSimplifyMode +{ + kPathReduce=0, + kPathEmbed +}; + +void GenerateSpriteOutline(PPtr<Texture2D> texture, float pixelsToUnits, const Rectf& rect, const Vector2f& rectOffset, float detail, unsigned char alphaTolerance, bool holeDetection, unsigned int extrude, int simplifyMode, std::vector<dynamic_array<Vector2f> >* outLine = NULL, std::vector<SpriteVertex>* outVertices = NULL, std::vector<UInt16>* outIndices = NULL, Rectf* meshRect = NULL); + +// Returns the front face of the AABB. +// Sprite is on local Z = 0, however the AABB has volume. Flatten it on Z and return 4 vertices only. +void GetAABBVerticesForSprite (const AABB& aabb, Vector3f* outVertices); + +bool GetSpriteMeshRectPixelBounds (const Texture2D& texture, const Rectf& rectInaccurate, int& rectX, int& rectY, int& rectRight, int& rectBottom); + +#endif //ENABLE_SPRITES diff --git a/Runtime/Graphics/SubstanceArchive.cpp b/Runtime/Graphics/SubstanceArchive.cpp new file mode 100644 index 0000000..33bc5a7 --- /dev/null +++ b/Runtime/Graphics/SubstanceArchive.cpp @@ -0,0 +1,121 @@ +#include "UnityPrefix.h" +#include "SubstanceArchive.h" +#include "SubstanceSystem.h" + +#if UNITY_EDITOR +#include "Editor/Src/AssetPipeline/SubstanceImporter.h" +#include "Editor/Src/AssetPipeline/SubstanceCache.h" +#include "Editor/Src/EditorUserBuildSettings.h" +#endif + +using namespace std; + +IMPLEMENT_CLASS_HAS_POSTINIT( SubstanceArchive ) + +IMPLEMENT_OBJECT_SERIALIZE( SubstanceArchive ) + +SubstanceArchive::SubstanceArchive( MemLabelId label, ObjectCreationMode mode ) + : Super( label, mode ) +{ +#if ENABLE_SUBSTANCE + m_isPushed = false; + m_generatedGraphs.clear(); + m_linkedBinaryData.clear(); +#endif +} + +SubstanceArchive::~SubstanceArchive() +{ +#if ENABLE_SUBSTANCE + GetSubstanceSystem().NotifyPackageDestruction(this); + for (std::map< UnityStr, UInt8* >::iterator i=m_linkedBinaryData.begin() ; i != m_linkedBinaryData.end() ; ++i) + { + UNITY_FREE(kMemSubstance, i->second); + } + m_linkedBinaryData.clear(); +#endif +} + +void SubstanceArchive::AwakeFromLoad( AwakeFromLoadMode awakeMode ) +{ + Super::AwakeFromLoad( awakeMode ); +} + +UInt8* SubstanceArchive::GetBufferData () +{ + return &m_PackageData[0]; +} + +unsigned SubstanceArchive::GetBufferSize () +{ + return m_PackageData.size(); +} + +#if ENABLE_SUBSTANCE +bool SubstanceArchive::SaveLinkedBinaryData( const UnityStr& prototypeName, const UInt8* data, const int size) +{ + if (IsCloneDataAvailable(prototypeName)) + { + WarningStringMsg("Trying to save linked substance data to a package that already has it"); + return false; + } + + UInt8* linkedBinaryData = (UInt8*) UNITY_MALLOC_ALIGNED_NULL(kMemSubstance, size, 32); + if (!linkedBinaryData) + { + WarningStringMsg("Could not allocate memory for a Substance package linked data"); + return false; + } + + memcpy( (void *) linkedBinaryData, (const void *) data, size); + m_linkedBinaryData[prototypeName] = linkedBinaryData; + return true; +} + +UInt8* SubstanceArchive::GetLinkedBinaryData( const UnityStr& prototypeName ) const +{ + return (m_linkedBinaryData.find(prototypeName)->second); +} + +bool SubstanceArchive::IsCloneDataAvailable( const UnityStr& prototypeName ) const +{ + return (m_linkedBinaryData.count(prototypeName) == 1); +} +#endif + +#if UNITY_EDITOR +void SubstanceArchive::Init( const UInt8* _pPackage, unsigned int _PackageLength ) +{ + // Copy the package content + m_PackageData.assign( _pPackage, _pPackage + _PackageLength ); + + AwakeFromLoad(kDefaultAwakeFromLoad); +} +#endif + +template<class T> +void SubstanceArchive::Transfer( T& transfer ) +{ + Super::Transfer( transfer ); + + transfer.Transfer( m_PackageData, "m_PackageData" ); + transfer.Align(); +} + +void SubstanceArchive::PostInitializeClass () +{ +#if ENABLE_SUBSTANCE + g_SubstanceSystem = new SubstanceSystem(); +#endif +} + +void SubstanceArchive::CleanupClass () +{ +#if ENABLE_SUBSTANCE + delete g_SubstanceSystem; +#endif +} + +#if ENABLE_SUBSTANCE +SubstanceSystem* SubstanceArchive::g_SubstanceSystem = NULL; +#endif diff --git a/Runtime/Graphics/SubstanceArchive.h b/Runtime/Graphics/SubstanceArchive.h new file mode 100644 index 0000000..ac49912 --- /dev/null +++ b/Runtime/Graphics/SubstanceArchive.h @@ -0,0 +1,75 @@ +#pragma once + +#include "Configuration/UnityConfigure.h" + +#include "Runtime/BaseClasses/NamedObject.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "Runtime/Utilities/dynamic_array.h" +#include "External/Allegorithmic/builds/Engines/include/substance/handle.h" + +class ColorRGBAf; +class ColorRGBA32; +class ProceduralTexture; +class SubstanceSystem; + +/* A SubstanceArchive is a resource representation for an imported Substance package. + * It hosts the persisted binary version of the package that is either imported + * or reloaded from disk. + */ + + +class SubstanceArchive : public NamedObject +{ +protected: // FIELDS + + // The SBSAR package as a binary + dynamic_array<UInt8> m_PackageData; + +public: // METHODS + + REGISTER_DERIVED_CLASS( SubstanceArchive, NamedObject ) + DECLARE_OBJECT_SERIALIZE( SubstanceArchive ) + + SubstanceArchive( MemLabelId label, ObjectCreationMode mode ); + + // Reloads the package from disk + void AwakeFromLoad( AwakeFromLoadMode awakeMode ); + +#if UNITY_EDITOR + // Creates the package from a binary SBSBIN file and the XML description file (one time call only when importing the SBSBIN file) + void Init( const UInt8* _pPackage, unsigned int _PackageLength ); +#endif + + UInt8* GetBufferData (); + unsigned GetBufferSize (); + +#if ENABLE_SUBSTANCE +public: + // Flag indicating whether the package's SBSASM has already pushed for linking + bool m_isPushed; + // Set of graph names that have already been generated from this package + std::set<UnityStr> m_generatedGraphs; + + // Cache of single-package no-const-inputs SBSBIN data used for cloning +public: + bool SaveLinkedBinaryData (const UnityStr& prototypeName, const UInt8* data, const int size); + UInt8* GetLinkedBinaryData (const UnityStr& prototypeName) const; + bool IsCloneDataAvailable (const UnityStr& prototypeName) const; +private: + std::map< UnityStr, UInt8* > m_linkedBinaryData; +#endif + +public: + + // Substance system initialization + static void InitializeClass (){} + static void PostInitializeClass (); + static void CleanupClass (); + +#if ENABLE_SUBSTANCE + static SubstanceSystem& GetSubstanceSystem() { return *g_SubstanceSystem; } + static SubstanceSystem* GetSubstanceSystemPtr() { return g_SubstanceSystem; } +private: + static SubstanceSystem* g_SubstanceSystem; +#endif +}; diff --git a/Runtime/Graphics/SubstanceInput.h b/Runtime/Graphics/SubstanceInput.h new file mode 100644 index 0000000..bb0a661 --- /dev/null +++ b/Runtime/Graphics/SubstanceInput.h @@ -0,0 +1,243 @@ +#pragma once + +#include "Configuration/UnityConfigure.h" + +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "External/Allegorithmic/builds/Engines/include/substance/inputdesc.h" +#include "Texture2D.h" + +enum ProceduralPropertyType +{ + ProceduralPropertyType_Boolean = 0, + ProceduralPropertyType_Float, + ProceduralPropertyType_Vector2, + ProceduralPropertyType_Vector3, + ProceduralPropertyType_Vector4, + ProceduralPropertyType_Color3, + ProceduralPropertyType_Color4, + ProceduralPropertyType_Enum, + ProceduralPropertyType_Texture +}; + +inline bool IsSubstanceAnyFloatType (SubstanceInputType type) { return type >= Substance_IType_Float && type <= Substance_IType_Float4; } +inline bool IsSubstanceAnyIntType (SubstanceInputType type) { return (type == Substance_IType_Integer) || (type == Substance_IType_Integer2) || (type == Substance_IType_Integer3) || (type == Substance_IType_Integer4); } +inline int GetRequiredInputComponentCount (SubstanceInputType type) +{ + switch (type) + { + case Substance_IType_Float: + case Substance_IType_Integer: + return 1; + case Substance_IType_Float2: + case Substance_IType_Integer2: + return 2; + case Substance_IType_Float3: + case Substance_IType_Integer3: + return 3; + case Substance_IType_Float4: + case Substance_IType_Integer4: + return 4; + default: + AssertString("Not supported"); + return -1; + } +} + +struct SubstanceEnumItem +{ + DECLARE_SERIALIZE(SubstanceEnumItem) + + int value; + UnityStr text; +}; + +struct SubstanceValue +{ + DECLARE_SERIALIZE(SubstanceValue) + + float scalar[4]; + PPtr<Texture2D> texture; + + SubstanceValue() { memset(scalar, 0, sizeof(float)*4); } +}; + +struct SubstanceInput +{ + DECLARE_SERIALIZE(SubstanceInput) + + // type & names + UnityStr name; + UnityStr label; + UnityStr group; + ProceduralPropertyType type; + SubstanceValue value; + SubstanceInputType internalType; + unsigned int internalIndex; + unsigned int internalIdentifier; + unsigned int shuffledIdentifier; + + // Ranges and step + float minimum; + float maximum; + float step; + + // Enum values + std::vector<SubstanceEnumItem> enumValues; + std::vector<std::string> GetEnumOptions() const + { + std::vector<std::string> enumOptions; + std::vector<SubstanceEnumItem>::const_iterator it; + for (it=enumValues.begin();it!=enumValues.end();++it) + { + enumOptions.push_back(it->text); + } + return enumOptions; + } + + // Flags + enum Flag + { + Flag_SkipHint = 1<<0, // input is in head of the graph so the cache is not usefull + Flag_Modified = 1<<1, // input has been modified + Flag_Cached = 1<<2, // input is cached to speed up modifications + Flag_Awake = 1<<3, // this flag is set when AwakeFromLoad comes, this to rebuild all without having hints everywhere + Flag_Clamp = 1<<4 // needs to clamp value when set + }; + unsigned int flags; + bool IsFlagEnabled(const Flag& flag) const { return flags & (unsigned int)flag; } + void EnableFlag(const Flag& flag, bool enabled=true) { if (enabled) flags |= (unsigned int)flag; else flags &= ~(unsigned int)flag; } + + // Altered texture links + std::set<unsigned int> alteredTexturesUID; + + SubstanceInput () + { + type = ProceduralPropertyType_Float; + flags = SubstanceInput::Flag_Awake; + value.scalar[0] = value.scalar[1] = value.scalar[2] = value.scalar[3] = 0.0F; + internalType = Substance_IType_Float; + internalIndex = 0; + internalIdentifier = 0; + + minimum = -std::numeric_limits<float>::max(); + maximum = std::numeric_limits<float>::max(); + } +}; + +inline void ClampSubstanceInputValues (const SubstanceInput& input, SubstanceValue& value) +{ + // validate combo value + if (input.type==ProceduralPropertyType_Enum && input.enumValues.size()>0) + { + std::vector<SubstanceEnumItem>::const_iterator it; + for (it=input.enumValues.begin();it!=input.enumValues.end();++it) + { + if ((int)value.scalar[0]==it->value) + return; + } + value.scalar[0] = (float)input.enumValues[0].value; + return; + } + + // clamp scalar values + if (input.IsFlagEnabled(SubstanceInput::Flag_Clamp)) + { + int count = GetRequiredInputComponentCount(input.internalType); + + if (IsSubstanceAnyIntType(input.internalType)) + { + for (int index=0;index<count;++index) + value.scalar[index] = (float)clamp<int>((int)(value.scalar[index]+0.5f), (int)input.minimum, (int)input.maximum); + } + else + { + for (int index=0;index<count;++index) + value.scalar[index] = clamp<float>(value.scalar[index], input.minimum, input.maximum); + } + } // step integers + else if (IsSubstanceAnyIntType(input.internalType)) + { + int count = GetRequiredInputComponentCount(input.internalType); + for (int index=0;index<count;++index) + value.scalar[index] = (float)((int)(value.scalar[index]+0.5f)); + } +} + +inline bool AreSubstanceInputValuesEqual(SubstanceInputType type, SubstanceValue& first, SubstanceValue& second) +{ + if (IsSubstanceAnyFloatType(type)) + { + return memcmp(first.scalar, second.scalar, + sizeof(float)*GetRequiredInputComponentCount(type))==0; + } + else if (IsSubstanceAnyIntType(type)) + { + int count = GetRequiredInputComponentCount(type); + + for (int index=0;index<count;++index) + { + if ((int)first.scalar[index]!=(int)second.scalar[index]) + return false; + } + return true; + } + else if (type==Substance_IType_Image) + { + return first.texture==second.texture; + } + return false; +} + +typedef std::vector<SubstanceInput> SubstanceInputs; + +template<class T> +void SubstanceEnumItem::Transfer(T& transfer) +{ + TRANSFER(value); + TRANSFER(text); +} + +template<class T> +void SubstanceValue::Transfer(T& transfer) +{ + TRANSFER(scalar[0]); + TRANSFER(scalar[1]); + TRANSFER(scalar[2]); + TRANSFER(scalar[3]); + TRANSFER(texture); +} + +template<class T> +void SubstanceInput::Transfer(T& transfer) +{ + // type & names + TRANSFER(name); + TRANSFER(label); + TRANSFER(group); + transfer.Transfer(reinterpret_cast<int&> (type), "type"); + TRANSFER(value); + transfer.Transfer(reinterpret_cast<int&> (internalType), "internalType"); + TRANSFER(internalIndex); + TRANSFER(internalIdentifier); + + // Range and step + TRANSFER(minimum); + TRANSFER(maximum); + TRANSFER(step); + + // Flags + TRANSFER(flags); + + // Altered texture links + TRANSFER(alteredTexturesUID); + + // Enum + TRANSFER(enumValues); + + // Update status + if (transfer.IsReading()) + { + EnableFlag(Flag_Awake, true); + EnableFlag(Flag_Cached, false); + } +} diff --git a/Runtime/Graphics/SubstanceSystem.cpp b/Runtime/Graphics/SubstanceSystem.cpp new file mode 100644 index 0000000..2231bbf --- /dev/null +++ b/Runtime/Graphics/SubstanceSystem.cpp @@ -0,0 +1,876 @@ +#include "UnityPrefix.h" +#include "SubstanceSystem.h" + +#if ENABLE_SUBSTANCE +#include "ProceduralMaterial.h" +#include "Runtime/BaseClasses/BaseObject.h" +#include "Runtime/Input/TimeManager.h" +#include "Runtime/Misc/SystemInfo.h" +#include "Runtime/BaseClasses/IsPlaying.h" +#include "Runtime/Serialize/LoadProgress.h" +#include "Runtime/BaseClasses/ManagerContext.h" +#include "Runtime/BaseClasses/GameManager.h" + +#if UNITY_EDITOR +#include "Editor/Src/AssetPipeline/SubstanceImporter.h" +#include "Editor/Src/Application.h" +#endif + +using namespace std; + +int SubstanceSystem::s_maximumSubstancePerFrame = 40; + +SubstanceSystem::SubstanceSystem() : + processedSubstance(NULL), + outputCallback(NULL), + lastProcessedSubstance(NULL), + queryClearCache(NULL), + isIntegrating(false), + integrationTimeStamp(0) +{ + int Error = substanceContextInit( &m_Context, &m_Device ); + Assert( !Error && "Failed to initialize blend platform context!" ); // We use the BLEND platform + + // Setup memory callback + Error = substanceContextSetCallback( m_Context, Substance_Callback_Malloc, (void*) OnMalloc ); + Assert( !Error && "Failed to setup malloc callback!" ); + Error = substanceContextSetCallback( m_Context, Substance_Callback_Free, (void*) OnFree ); + Assert( !Error && "Failed to setup free callback!" ); + + // Setup the callback for image inputs + Error = substanceContextSetCallback( m_Context, Substance_Callback_InputImageLock, (void*) OnInputImageLock ); + Assert( !Error && "Failed to setup image lock callback!" ); + Error = substanceContextSetCallback( m_Context, Substance_Callback_InputImageUnlock, (void*) OnInputImageUnlock ); + Assert( !Error && "Failed to setup image unlock callback!" ); + Error = substanceContextSetCallback( m_Context, Substance_Callback_OutOfMemory, (void*) OnOutOfMemory ); + Assert( !Error && "Failed to setup out_of_memory callback!" ); + + SetOutputCallback(); + processingThread.SetName ("UnitySubstanceThread"); + processingThread.Run(ThreadMain, this); + SetProcessorUsage(ProceduralProcessorUsage_One); +} + +SubstanceSystem::~SubstanceSystem() +{ + processingThread.SignalQuit(); + threadSemaphore.Signal(); + processingThread.WaitForExit(); + + if ( m_Context == NULL ) + return; // Already released ! + + // Release the context + substanceContextRelease( m_Context ); + m_Context = NULL; +} + +void SubstanceSystem::SetOutputCallback(void* callback) +{ + outputCallback = callback==NULL?(void*)OnOutputCompleted:callback; + int err = substanceContextSetCallback( m_Context, Substance_Callback_OutputCompleted, outputCallback ); + AssertMsg(err == 0, "Failed to sertup render callback!"); +} + +bool SubstanceSystem::AreQueuesEmpty() +{ + return integrationQueue.size()==0 + && waitingSubstanceQueue.size()==0 + && processingSubstanceQueue.size()==0 + && processedSubstance==NULL + && updatingSubstanceQueue.size()==0; +} + +bool SubstanceSystem::AreIntegratingQueuesEmpty() +{ + { + Mutex::AutoLock integrationLocker(integrationMutex); + if (integrationQueue.size()>0) + for (std::vector<ProceduralMaterial*>::iterator it=integrationQueue.begin() ; it!=integrationQueue.end() ; ++it) + if ((*it)->integrationTimeStamp==integrationTimeStamp) + return false; + } + + { + Mutex::AutoLock waitingLocker(waitingMutex); + if (waitingSubstanceQueue.size()>0) + for (std::list<Substance*>::iterator it=waitingSubstanceQueue.begin() ; it!=waitingSubstanceQueue.end() ; ++it) + if ((*it)->material->integrationTimeStamp==integrationTimeStamp) + return false; + } + + { + Mutex::AutoLock processingLocker(processingMutex); + if (processingSubstanceQueue.size()>0) + for (std::list<Substance*>::iterator it=processingSubstanceQueue.begin() ; it!=processingSubstanceQueue.end() ; ++it) + if ((*it)->material->integrationTimeStamp==integrationTimeStamp) + return false; + } + + { + Mutex::AutoLock processedLocker(processedMutex); + if (processedSubstance!=NULL && processedSubstance->material->integrationTimeStamp==integrationTimeStamp) + return false; + } + + { + Mutex::AutoLock updatingLocker(updatingMutex); + if (updatingSubstanceQueue.size()>0) + for (std::list<Substance*>::iterator it=updatingSubstanceQueue.begin() ; it!=updatingSubstanceQueue.end() ; ++it) + if ((*it)->material->integrationTimeStamp==integrationTimeStamp) + return false; + } + + return true; +} + +bool SubstanceSystem::IsSubstanceProcessing(const ProceduralMaterial* substance) +{ + // Search into integration queue + { + Mutex::AutoLock integrationLocker(integrationMutex); + std::vector<ProceduralMaterial*>::iterator it = std::find(integrationQueue.begin(), integrationQueue.end(), substance); + if (it!=integrationQueue.end()) + return true; + } + + // Search into processing queue + { + Mutex::AutoLock processingLocker(processingMutex); + std::list<Substance*>::iterator it = std::find_if(processingSubstanceQueue.begin(), processingSubstanceQueue.end(), QueueFinder(substance)); + if (it!=processingSubstanceQueue.end()) + return true; + } + + // Is currently processing ? + { + Mutex::AutoLock processedLocker(processedMutex); + if (processedSubstance!=NULL && processedSubstance->material==substance) + return true; + } + + // Search into updating queue + { + Mutex::AutoLock updatingLocker(updatingMutex); + std::list<Substance*>::iterator it = std::find_if(updatingSubstanceQueue.begin(), updatingSubstanceQueue.end(), QueueFinder(substance)); + if (it!=updatingSubstanceQueue.end()) + return true; + } + + return false; +} + +void SubstanceSystem::WaitFinished(LoadProgress* progress) +{ + isIntegrating = true; + int integrate_count = integrationQueue.size(); + if (progress!=NULL) + { + progress->totalItems += integrate_count; + } + + while (!AreIntegratingQueuesEmpty()) + { + if (Thread::CurrentThreadIsMainThread()) + Update(); + + if (progress!=NULL) + { + int new_count = integrationQueue.size(); + progress->ItemProcessed(integrate_count-new_count); + integrate_count = integrationQueue.size(); + } + + Thread::Sleep(0.001); + } + isIntegrating = false; +} + +void SubstanceSystem::WaitFinished(const ProceduralMaterial* substance) +{ + isIntegrating = true; + while (IsSubstanceProcessing(substance)) + { + if (Thread::CurrentThreadIsMainThread()) + Update(); + + Thread::Sleep(0.001); + } + isIntegrating = false; +} + +void SubstanceSystem::Update(bool isQuitSignaled) +{ + if (!AreQueuesEmpty()) + threadSemaphore.Signal(); + + // Clear waiting queue + { + Mutex::AutoLock waitingLocker(waitingMutex); + while (waitingSubstanceQueue.size()>0) + { + delete waitingSubstanceQueue.back(); + waitingSubstanceQueue.pop_back(); + } + } + + // Update animated substances + if (!isIntegrating && !isQuitSignaled) + { + Mutex::AutoLock deleteLocker(deletePingMutex); + for (std::vector<ProceduralMaterial*>::iterator it=animatedSubstanceArray.begin();it!=animatedSubstanceArray.end();++it) + { + ProceduralMaterial *substance = *it; + substance->UpdateAnimation(GetTimeManager().GetCurTime()); + } + } + + // Upload updated substances + int uploadedCount = s_maximumSubstancePerFrame; + Substance* updatedSubstance; + while (uploadedCount-- && updatingSubstanceQueue.size()>0) + { + // Pick one if available + { + Mutex::AutoLock updatingLocker(updatingMutex); + std::list<Substance*>::iterator it = updatingSubstanceQueue.begin(); + if (it==updatingSubstanceQueue.end()) + break; + updatedSubstance = *it; + updatingSubstanceQueue.erase(it); + } + + // Upload textures + { + //WarningStringMsg("SUBSTANCE: Uploading %d textures from %s", updatedSubstance->updatedTextures.size(), updatedSubstance->material->GetName()); + for (std::map< ProceduralTexture*, SubstanceTexture >::iterator it=updatedSubstance->updatedTextures.begin();it!=updatedSubstance->updatedTextures.end();++it) + { + it->first->UploadSubstanceTexture(it->second); +#if UNITY_EDITOR + SubstanceImporter::OnTextureModified(*updatedSubstance, *it->first, it->second); + //WarningStringMsg("SUBSTANCE: Uploading %s from material %s", it->first->GetName(), it->first->GetSubstanceMaterial()->GetName()); +#endif + } + +#if UNITY_EDITOR + SubstanceImporter::OnSubstanceModified(*updatedSubstance); +#endif + for (std::map< ProceduralTexture*, SubstanceTexture >::iterator it=updatedSubstance->updatedTextures.begin();it!=updatedSubstance->updatedTextures.end();++it) + { + OnFree(it->second.buffer); + } + delete updatedSubstance; + } + } + + if (isQuitSignaled) + { + // Clear processing queue + Mutex::AutoLock processingLocker(processingMutex); + for (std::list<Substance*>::iterator it=processingSubstanceQueue.begin();it!=processingSubstanceQueue.end();++it) + delete *it; + processingSubstanceQueue.clear(); + } +} + +void SubstanceSystem::UpdateThreaded() +{ + // Integrate loaded substances, if any + Integrate(); + + // Retrieve next substance to process + { + Mutex::AutoLock processingLocker(processingMutex); + std::list<Substance*>::iterator it = processingSubstanceQueue.begin(); + if (it==processingSubstanceQueue.end()) + return; + + { + Mutex::AutoLock processedLocker(processedMutex); + processedSubstance = *it; + } + processingSubstanceQueue.erase(it); + } + + // Set input values + for (std::map<std::string, SubstanceValue>::iterator i=processedSubstance->inputValues.begin();i!=processedSubstance->inputValues.end();++i) + { + processedSubstance->material->Callback_SetSubstanceInput(i->first, i->second); + } + + // Clear cache if required + if (processedSubstance->material==queryClearCache) + { + ApplyMemoryBudget(processedSubstance->material, false); + queryClearCache = NULL; + } + + // Generate textures + processedTextures.clear(); + if (!processedSubstance->material->ProcessTexturesThreaded(processedSubstance->updatedTextures)) + { + Mutex::AutoLock processedLocker(processedMutex); + delete processedSubstance; + processedSubstance = NULL; + return; + } + + // Push substance into the updating queue + { + Mutex::AutoLock updatingLocker(updatingMutex); + updatingSubstanceQueue.push_back(processedSubstance); + } + // Clear + { + Mutex::AutoLock processedLocker(processedMutex); + processedSubstance = NULL; + } +} + +void SubstanceSystem::QueueInput( ProceduralMaterial* material, std::string inputName, SubstanceValue& inputValue ) +{ + // Skip if the material is broken + if (material->IsFlagEnabled(ProceduralMaterial::Flag_Broken)) + return; + + material->SetDirty(); + +#if UNITY_EDITOR + SubstanceImporter::OnInputModified(*material, inputName, inputValue); +#endif + + // Directly apply value if not loaded + if (material->GetSubstanceHandle()==NULL) + { + material->Callback_SetSubstanceInput(inputName, inputValue); + return; + } + + // Search if the substance is already in the process queue + { + Mutex::AutoLock locker(processingMutex); + std::list<Substance*>::iterator it = std::find_if(processingSubstanceQueue.begin(), processingSubstanceQueue.end(), QueueFinder(material)); + if (it!=processingSubstanceQueue.end()) + { + (*it)->inputValues[inputName] = inputValue; + return; + } + } + + // Search if is already in the waiting queue + { + Mutex::AutoLock waitingLocker(waitingMutex); + std::list<Substance*>::iterator it = std::find_if(waitingSubstanceQueue.begin(), waitingSubstanceQueue.end(), QueueFinder(material)); + if (it!=waitingSubstanceQueue.end()) + { + (*it)->inputValues[inputName] = inputValue; + return; + } + } + + // Search if is already in the integration queue + { + Mutex::AutoLock integrationLocker(integrationMutex); + std::vector<ProceduralMaterial*>::iterator i = std::find(integrationQueue.begin(), integrationQueue.end(), material); + if (i!=integrationQueue.end()) + { + material->Callback_SetSubstanceInput(inputName, inputValue); + return; + } + } + + // Else add it in the waiting queue + Substance* substance = new Substance(); + substance->material = material; + substance->inputValues[inputName] = inputValue; + { + Mutex::AutoLock waitingLocker(waitingMutex); + waitingSubstanceQueue.push_back(substance); + } +} + +void SubstanceSystem::QueueSubstance(ProceduralMaterial* material) +{ + // Skip if the material is broken + if (material->IsFlagEnabled(ProceduralMaterial::Flag_Broken)) + return; + + if (material->GetSubstanceHandle()==NULL) + { + if (IsWorldPlaying() && material->GetLoadingBehavior()==ProceduralLoadingBehavior_None) + material->EnableFlag(ProceduralMaterial::Flag_ForceGenerate); + QueueLoading(material); + return; + } + + // If it exists in the processing queue, input values are already pushed -> leave it as is + if (processingSubstanceQueue.size()>0) + { + Mutex::AutoLock locker(processingMutex); + std::list<Substance*>::iterator it = std::find_if(processingSubstanceQueue.begin(), processingSubstanceQueue.end(), QueueFinder(material)); + if (it!=processingSubstanceQueue.end()) + return; + } + + // If it exists in the waiting queue, push it into the processing queue + if (waitingSubstanceQueue.size()>0) + { + Mutex::AutoLock waitingLocker(waitingMutex); + std::list<Substance*>::iterator it = std::find_if(waitingSubstanceQueue.begin(), waitingSubstanceQueue.end(), QueueFinder(material)); + if (it!=waitingSubstanceQueue.end()) + { + Mutex::AutoLock locker(processingMutex); + processingSubstanceQueue.push_back(*it); + waitingSubstanceQueue.erase(it); + return; + } + } + + // Push it into the processing queue + Substance* substance = new Substance(); + substance->material = material; + Mutex::AutoLock locker(processingMutex); + processingSubstanceQueue.push_back(substance); +} + +void SubstanceSystem::QueueLoading(ProceduralMaterial* material) +{ + // Skip if the material is broken + if (material->IsFlagEnabled(ProceduralMaterial::Flag_Broken)) + return; + + // Already loaded ? + if (material->GetSubstanceHandle()!=NULL) + return; + + // If it exists in the waiting queue, directly assign input values + { + Mutex::AutoLock waitingLocker(waitingMutex); + std::list<Substance*>::iterator it = std::find_if(waitingSubstanceQueue.begin(), waitingSubstanceQueue.end(), QueueFinder(material)); + if (it!=waitingSubstanceQueue.end()) + { +#if UNITY_EDITOR + material->EnableFlag(ProceduralMaterial::Flag_Awake, false); +#endif + for (std::map<std::string, SubstanceValue>::iterator i=(*it)->inputValues.begin() ; i!=(*it)->inputValues.end() ; ++i) + material->Callback_SetSubstanceInput(i->first, i->second); + delete *it; + waitingSubstanceQueue.erase(it); + } + } + + // Push it to the integrationQueue if it isn't + Mutex::AutoLock integrationLocker(integrationMutex); + std::vector<ProceduralMaterial*>::iterator i = std::find(integrationQueue.begin(), integrationQueue.end(), material); + if (i==integrationQueue.end()) + integrationQueue.push_back(material); +} + +void SubstanceSystem::QueryClearCache(ProceduralMaterial* material) +{ + queryClearCache = material; + QueueSubstance(material); +} + +void* SubstanceSystem::ThreadMain(void*data) +{ + SubstanceSystem* system = static_cast<SubstanceSystem*>(data); + while (!system->processingThread.IsQuitSignaled()) + { + system->threadSemaphore.WaitForSignal(); + system->threadSemaphore.Reset(); + + for (int i=0 ; i<s_maximumSubstancePerFrame ; ++i) + system->UpdateThreaded(); + } + return NULL; +} + +#define SUBSTANCE_TRACE_MEM_CALLBACKS 0 + +void* SUBSTANCE_CALLBACK SubstanceSystem::OnMalloc(size_t bytesCount, size_t alignment) +{ + void *ptr = UNITY_MALLOC_ALIGNED_NULL(kMemSubstance, bytesCount, alignment); + if (!ptr) + { + ErrorString("Could not allocate memory in OnMalloc (SubstanceSystem)"); + } +#if SUBSTANCE_TRACE_MEM_CALLBACKS + Substance *substance = GetSubstanceSystem().processedSubstance; + WarningStringMsg("\n%08X %d Alloc %s", ptr, bytesCount, (substance != NULL ? substance->material->GetName() : "No Substance")); +#endif + return ptr; +} + +void SUBSTANCE_CALLBACK SubstanceSystem::OnFree(void* bufferPtr) +{ +#if SUBSTANCE_TRACE_MEM_CALLBACKS + Substance *substance = GetSubstanceSystem().processedSubstance; + WarningStringMsg("\n%08X Free %s", bufferPtr, (substance != NULL ? substance->material->GetName() : "No Substance")); +#endif + UNITY_FREE(kMemSubstance, bufferPtr); +} + +void SUBSTANCE_CALLBACK SubstanceSystem::OnOutputCompleted( SubstanceHandle* _pHandle, unsigned int _OutputIndex, size_t _JobUserData ) +{ + SubstanceOutputDesc outputDesc; + if (substanceHandleGetOutputDesc(_pHandle, _OutputIndex, &outputDesc)==0) + { + std::map<unsigned int, ProceduralTexture*>::iterator it; + it = GetSubstanceSystem ().processedTextures.find(outputDesc.outputId); + if (it!=GetSubstanceSystem ().processedTextures.end()) + { + ProceduralTexture* texture = it->second; + Substance* substance = GetSubstanceSystem().processedSubstance; + Assert( substance!=NULL && "Failed to update substance!" ); + //WarningStringMsg("SUBSTANCE: OnOutputCompleted received texture %s from substance %s", it->second->GetName(), substance->material->GetName()); + if (substanceHandleGetOutputs(substance->material->GetSubstanceHandle(), Substance_OutOpt_TextureId, texture->GetSubstanceTextureUID(), 1, &substance->updatedTextures[texture])!=0) + { + ErrorStringObject("Failed to retrieve substance texture data", substance->material); + return; + } + } + } +} + +void SUBSTANCE_CALLBACK SubstanceSystem::OnInputImageLock( SubstanceHandle* _pHandle, size_t _JobUserData, unsigned int _InputIndex, SubstanceTextureInput** _ppCurrentTextureInputDesc, const SubstanceTextureInput* _pPreferredTextureInputDesc ) +{ + Substance* substance = GetSubstanceSystem().processedSubstance; + Assert( substance!=NULL && "Failed to update substance input texture!" ); + substance->material->ApplyTextureInput(_InputIndex, *_pPreferredTextureInputDesc); +} + +void SUBSTANCE_CALLBACK SubstanceSystem::OnInputImageUnlock( SubstanceHandle* _pHandle, size_t _JobUserData, unsigned int _InputIndex, SubstanceTextureInput* _ppCurrentTextureInputDesc ) +{ +} + +void SUBSTANCE_CALLBACK SubstanceSystem::OnOutOfMemory(SubstanceHandle *handle, int memoryType) +{ + Substance* substance = GetSubstanceSystem().processedSubstance; + if (substance->material->GetProceduralMemoryWorkBudget()!=ProceduralCacheSize_NoLimit) + { + GetSubstanceSystem().ApplyMemoryBudget(substance->material, true, true); + } +} + +void SubstanceSystem::NotifySubstanceCreation(ProceduralMaterial* substance) +{ + // Add it into the animated substance array if needed + if (substance->IsFlagEnabled(ProceduralMaterial::Flag_Animated)) + { + if (std::find(animatedSubstanceArray.begin(), animatedSubstanceArray.end(), substance)==animatedSubstanceArray.end()) + { + animatedSubstanceArray.push_back(substance); + } + } +} + +void SubstanceSystem::NotifySubstanceDestruction(ProceduralMaterial* substance) +{ + // Remove it from animated array if in + { + if (substance->IsFlagEnabled(ProceduralMaterial::Flag_Animated)) + { + Mutex::AutoLock deletePingLocker(deletePingMutex); + std::vector<ProceduralMaterial*>::iterator i(std::find(animatedSubstanceArray.begin(), animatedSubstanceArray.end(), substance)); + if (i!=animatedSubstanceArray.end()) + animatedSubstanceArray.erase(i); + } + } + + // Remove it from integration array if in + { + Mutex::AutoLock deletePingLocker(deletePingMutex); + Mutex::AutoLock deleteIntegrationLocker(deleteIntegrationMutex); + Mutex::AutoLock integrationLocker(integrationMutex); + std::vector<ProceduralMaterial*>::iterator i = std::find(integrationQueue.begin(), integrationQueue.end(), substance); + if (i!=integrationQueue.end()) + integrationQueue.erase(i); + } + + // Remove it from processing array if in + { + Mutex::AutoLock processingLocker(processingMutex); + std::list<Substance*>::iterator it; + while((it=std::find_if(processingSubstanceQueue.begin(), processingSubstanceQueue.end(), QueueFinder(substance)))!=processingSubstanceQueue.end()) + { + delete *it; + processingSubstanceQueue.erase(it); + } + } + + // Wait if it's processing + while(1) + { + { + Mutex::AutoLock processedLocker(processedMutex); + if (processedSubstance==NULL || substance->GetSubstanceData()!=processedSubstance->material->GetSubstanceData()) + break; + } + Thread::Sleep(0.001); + } + + // Remove it from updating array if in + { + Mutex::AutoLock updatingLocker(updatingMutex); + std::list<Substance*>::iterator it = std::find_if(updatingSubstanceQueue.begin(), updatingSubstanceQueue.end(), QueueFinder(substance)); + if (it!=updatingSubstanceQueue.end()) + { + // Delete the output buffers from the Substance if they have not yet been pushed to the GPU + // This can happen when building for example, where very quick cycles of load/generate/discard are performed + for (std::map<ProceduralTexture*,SubstanceTexture>::iterator itTex = (*it)->updatedTextures.begin() ; + itTex != (*it)->updatedTextures.end() ; ++itTex) + { + OnFree(itTex->second.buffer); + } + + delete *it; + updatingSubstanceQueue.erase(it); + } + } + + // Clear last processed if needed + { + Mutex::AutoLock lastSubstanceLocker(lastProcessedSubstanceMutex); + if (substance==lastProcessedSubstance) + lastProcessedSubstance = NULL; + } +} + +void SubstanceSystem::NotifyTextureDestruction(ProceduralTexture* texture) +{ + if (texture->GetSubstanceMaterial()!=NULL) + NotifySubstanceDestruction(texture->GetSubstanceMaterial()); +} + +void SubstanceSystem::NotifyPackageDestruction(SubstanceArchive* package) +{ + // Remove it from animated array if in + { + Mutex::AutoLock deletePingLocker(deletePingMutex); + std::vector<ProceduralMaterial*>::iterator it; + while((it=std::find_if(animatedSubstanceArray.begin(), animatedSubstanceArray.end(), ArrayPackageFinder(package)))!=animatedSubstanceArray.end()) + { + animatedSubstanceArray.erase(it); + } + } + + // Remove it from integration array if in + { + Mutex::AutoLock deletePingLocker(deletePingMutex); + Mutex::AutoLock deleteIntegrationLocker(deleteIntegrationMutex); + Mutex::AutoLock integrationLocker(integrationMutex); + std::vector<ProceduralMaterial*>::iterator i; + while((i=std::find_if(integrationQueue.begin(), integrationQueue.end(), ArrayPackageFinder(package)))!=integrationQueue.end()) + integrationQueue.erase(i); + } + + // Remove it from processing array if in + { + Mutex::AutoLock processingLocker(processingMutex); + std::list<Substance*>::iterator it; + while((it=std::find_if(processingSubstanceQueue.begin(), processingSubstanceQueue.end(), QueuePackageFinder(package)))!=processingSubstanceQueue.end()) + { + delete *it; + processingSubstanceQueue.erase(it); + } + } + + // Wait if it's processing + while(1) + { + { + Mutex::AutoLock processedLocker(processedMutex); + if (processedSubstance==NULL || package!=processedSubstance->material->GetSubstancePackage()) + break; + } + Thread::Sleep(0.001); + } + + // Remove it from updating array if in + { + Mutex::AutoLock updatingLocker(updatingMutex); + std::list<Substance*>::iterator it; + while((it=std::find_if(updatingSubstanceQueue.begin(), updatingSubstanceQueue.end(), QueuePackageFinder(package)))!=updatingSubstanceQueue.end()) + { + // Delete the output buffers from the Substance if they have not yet been pushed to the GPU + // This can happen when building for example, where very quick cycles of load/generate/discard are performed + for (std::map<ProceduralTexture*,SubstanceTexture>::iterator itTex = (*it)->updatedTextures.begin() ; + itTex != (*it)->updatedTextures.end() ; ++itTex) + { + OnFree(itTex->second.buffer); + } + + delete *it; + updatingSubstanceQueue.erase(it); + } + } + + // Clear last processed if needed + { + Mutex::AutoLock lastSubstanceLocker(lastProcessedSubstanceMutex); + if (lastProcessedSubstance!=NULL && package==lastProcessedSubstance->GetSubstancePackage()) + lastProcessedSubstance = NULL; + } +} + +void SubstanceSystem::SetProcessorUsage(ProceduralProcessorUsage usage) +{ + m_ProcessorUsage = usage; + + switch(m_ProcessorUsage) + { + case ProceduralProcessorUsage_Half: + processingThread.SetPriority(kNormalPriority); + break; + case ProceduralProcessorUsage_All: + processingThread.SetPriority(kNormalPriority); + break; + default: + processingThread.SetPriority(kLowPriority); + } +} + +void SubstanceSystem::ClearProcessingQueue() +{ + Mutex::AutoLock processingLocker(processingMutex); + for (std::list<Substance*>::iterator it=processingSubstanceQueue.begin() ; it!=processingSubstanceQueue.end() ; ++it) + { + delete *it; + } + processingSubstanceQueue.clear(); +} + +SubstanceSystem::Context::Context(ProceduralProcessorUsage usage) +{ + m_OldProcessorUsage = GetSubstanceSystem().GetProcessorUsage(); + GetSubstanceSystem().SetProcessorUsage(usage); +} + +SubstanceSystem::Context::~Context() +{ + GetSubstanceSystem().SetProcessorUsage(m_OldProcessorUsage); +} + +void SubstanceSystem::ForceSubstanceResults(std::map<ProceduralTexture*, SubstanceTexture>& results) +{ + Assert( processedSubstance!=NULL && "Substance caching incorrect behavior!" ); + processedSubstance->updatedTextures = results; +} + +void SubstanceSystem::UpdateMemoryBudget() +{ + Mutex::AutoLock lastSubstanceLocker(lastProcessedSubstanceMutex); + if (lastProcessedSubstance!=NULL && + lastProcessedSubstance->GetSubstanceHandle()!=processedSubstance->material->GetSubstanceHandle()) + { + //WarningStringMsg("Emptying Substance cache for handle %p, for substance %s\n", lastProcessedSubstance->GetSubstanceHandle(), lastProcessedSubstance->GetName()); + ApplyMemoryBudget(lastProcessedSubstance, false); + } + + lastProcessedSubstance = processedSubstance->material; + ApplyMemoryBudget(lastProcessedSubstance, true); +} + +void SubstanceSystem::ApplyMemoryBudget(ProceduralMaterial* substance, bool requireMaximum, bool outOfMemory) +{ + if (substance->GetSubstanceHandle()==NULL) + { + // The substance is not loaded now, so skip it. + return; + } + + // Always use tiny memory budget in the editor if the material is loading, this to lower memory usage. + // Only do this when NOT playing (in Play mode, the runtime-set mem. budget must be used) +#if UNITY_EDITOR + if (!IsWorldPlaying() && requireMaximum && substance->IsFlagEnabled(ProceduralMaterial::Flag_Awake)) + substance->SetProceduralMemoryWorkBudget(ProceduralCacheSize_Tiny); +#endif + + SubstanceHardResources memoryBudget; + memset(&memoryBudget, 0, sizeof(SubstanceHardResources)); + int processorCount = max(systeminfo::GetProcessorCount(), 1); + int mediumLimit = max(processorCount/2, 1); + + for (int index=0;index<SUBSTANCE_CPU_COUNT_MAX;++index) + { + unsigned char processorUsage(Substance_Resource_FullUse); + + if ((m_ProcessorUsage==ProceduralProcessorUsage_Half && index>=mediumLimit) + || (m_ProcessorUsage==ProceduralProcessorUsage_One && index>0)) + { + processorUsage = Substance_Resource_DoNotUse; + } + + memoryBudget.cpusUse[index] = processorUsage; + } + + if (outOfMemory) + { + substance->SetProceduralMemoryWorkBudget( + (ProceduralCacheSize)((int)substance->GetProceduralMemoryWorkBudget()+1)); + } + + size_t workSize = GetProceduralMemoryBudget(substance->GetProceduralMemoryWorkBudget()); + if (workSize==1) + workSize = 128 * 1024 * 1024; + size_t sleepSize = GetProceduralMemoryBudget(substance->GetProceduralMemorySleepBudget()); + + memoryBudget.systemMemoryBudget = requireMaximum?workSize:sleepSize; + + if (substanceHandleSwitchHard(substance->GetSubstanceHandle(), Substance_Sync_Synchronous, &memoryBudget, NULL, 0)!=0) + ErrorStringObject("Failed to set substance memory budget", substance); + + if (!outOfMemory) + { + if (substanceHandleStart(substance->GetSubstanceHandle(), Substance_Sync_Synchronous)!=0) + ErrorStringObject("Failed to update substance memory budget", substance); + } +} + +void SubstanceSystem::Integrate() +{ + if (integrationQueue.size()>0) + { + Mutex::AutoLock deleteLocker(deleteIntegrationMutex); + + // Retrieve materials to pack + std::vector<ProceduralMaterial*> packedMaterials; + { + Mutex::AutoLock integrationLocker(integrationMutex); + packedMaterials = integrationQueue; + } + + // Pack all available substances + ProceduralMaterial::PackSubstances(packedMaterials); + + for (std::vector<ProceduralMaterial*>::iterator it=packedMaterials.begin() ; it!=packedMaterials.end() ; ++it) + { + if (!(*it)->IsFlagEnabled(ProceduralMaterial::Flag_Broken) + && ((*it)->IsFlagEnabled(ProceduralMaterial::Flag_ForceGenerate) + || !IsWorldPlaying() || (*it)->GetLoadingBehavior()!=ProceduralLoadingBehavior_None)) + { + QueueSubstance(*it); + queryClearCache = *it; + } + + // Remove it from the integrationQueue + Mutex::AutoLock integrationLocker(integrationMutex); + std::vector<ProceduralMaterial*>::iterator i = std::find(integrationQueue.begin(), integrationQueue.end(), *it); + integrationQueue.erase(i); + } + } +} + +SubstanceSystem& GetSubstanceSystem () +{ + return SubstanceArchive::GetSubstanceSystem(); +} + +SubstanceSystem* GetSubstanceSystemPtr () +{ + return SubstanceArchive::GetSubstanceSystemPtr(); +} + + +#endif //ENABLE_SUBSTANCE diff --git a/Runtime/Graphics/SubstanceSystem.h b/Runtime/Graphics/SubstanceSystem.h new file mode 100644 index 0000000..e87b49d --- /dev/null +++ b/Runtime/Graphics/SubstanceSystem.h @@ -0,0 +1,160 @@ +#pragma once + +#include "ProceduralMaterial.h" +#include "SubstanceArchive.h" +#include "Texture2D.h" +#include "Runtime/Threads/Thread.h" +#include "Runtime/Threads/Mutex.h" +#include "Runtime/Threads/Semaphore.h" +#include <queue> +#include <algorithm> + +#if ENABLE_SUBSTANCE +#include "External/Allegorithmic/builds/Engines/include/substance/handle.h" +#include "External/Allegorithmic/builds/Engines/include/substance/device.h" + +class LoadProgress; + +class SubstanceSystem +{ + SubstanceDevice m_Device; + SubstanceContext* m_Context; + +public: + + SubstanceSystem (); + ~SubstanceSystem (); + + SubstanceContext* GetContext () { return m_Context; } + + // Set the callback handling generated texture + // The default substance system is used when set to NULL + void SetOutputCallback(void* callback=NULL); + + // State queries + bool AreQueuesEmpty(); + bool AreIntegratingQueuesEmpty(); + bool IsSubstanceProcessing(const ProceduralMaterial* substance); + + // State wait + void WaitFinished(LoadProgress* progress = NULL); + void WaitFinished(const ProceduralMaterial* substance); + + // Update substance system, called from the main thread + void Update(bool isQuitSignaled=false); + + // Update substance system, called from the substance thread + // . generate queued substances + // . integrate substances + void UpdateThreaded(); + + // Queued updates coming from ProceduralMaterial + void QueueInput(ProceduralMaterial* material, std::string inputName, SubstanceValue& inputValue); + void QueueSubstance(ProceduralMaterial* material); + void QueueLoading(ProceduralMaterial* material); + + void QueryClearCache(ProceduralMaterial* material); + + static void * ThreadMain(void*data); + + // Substance callbacks + static void* SUBSTANCE_CALLBACK OnMalloc(size_t bytesCount, size_t alignment=16); + static void SUBSTANCE_CALLBACK OnFree(void* bufferPtr); + static void SUBSTANCE_CALLBACK OnOutputCompleted(SubstanceHandle* _pHandle, unsigned int _OutputIndex, size_t _JobUserData); + static void SUBSTANCE_CALLBACK OnInputImageLock(SubstanceHandle* _pHandle, size_t _JobUserData, unsigned int _InputIndex, SubstanceTextureInput** _ppCurrentTextureInputDesc, const SubstanceTextureInput* _pPreferredTextureInputDesc); + static void SUBSTANCE_CALLBACK OnInputImageUnlock(SubstanceHandle* _pHandle, size_t _JobUserData, unsigned int _InputIndex, SubstanceTextureInput* _ppCurrentTextureInputDesc); + static void SUBSTANCE_CALLBACK OnOutOfMemory(SubstanceHandle *handle, int memoryType); + + // Substance notifications + void NotifySubstanceCreation(ProceduralMaterial* substance); + void NotifySubstanceDestruction(ProceduralMaterial* substance); + void NotifyTextureDestruction(ProceduralTexture* texture); + void NotifyPackageDestruction(SubstanceArchive* package); + + // Priority handling + void SetProcessorUsage(ProceduralProcessorUsage usage); + ProceduralProcessorUsage GetProcessorUsage() const { return m_ProcessorUsage; } + void ClearProcessingQueue(); + + // Queued substance data + struct Substance + { + ProceduralMaterial* material; + std::map<std::string, SubstanceValue> inputValues; + std::map<ProceduralTexture*, SubstanceTexture> updatedTextures; + }; + std::map<unsigned int, ProceduralTexture*> processedTextures; + void ForceSubstanceResults(std::map<ProceduralTexture*, SubstanceTexture>& results); + + // Memory budget management + void UpdateMemoryBudget(); + + // Context of generation which temporary set processor usage + class Context + { + public: + Context(ProceduralProcessorUsage usage); + ~Context(); + private: + ProceduralProcessorUsage m_OldProcessorUsage; + }; + + void BeginPreloading() { ++integrationTimeStamp; } + unsigned int integrationTimeStamp; + +private: + void ApplyMemoryBudget(ProceduralMaterial* substance, bool requireMaximum, bool outOfMemory=false); + + // Threaded integration / packing of substances + void Integrate(); + + // Substance queue helpers + struct QueueFinder + { + const ProceduralMaterial* m_Material; + QueueFinder(const ProceduralMaterial* material) : m_Material(material) {} + bool operator()(const Substance* substance) { return substance->material == m_Material; } + }; + + struct QueuePackageFinder + { + const SubstanceArchive* m_Package; + QueuePackageFinder(const SubstanceArchive* package) : m_Package(package) {} + bool operator()(const Substance* substance) { return substance->material->GetSubstancePackage()==m_Package; } + }; + + struct ArrayPackageFinder + { + const SubstanceArchive* m_Package; + ArrayPackageFinder(const SubstanceArchive* package) : m_Package(package) {} + bool operator()(const ProceduralMaterial* substance) { return substance->GetSubstancePackage()==m_Package; } + }; + + std::list<Substance*> waitingSubstanceQueue; + std::list<Substance*> processingSubstanceQueue; + std::list<Substance*> updatingSubstanceQueue; + std::vector<ProceduralMaterial*> integrationQueue; + Thread processingThread; + Mutex processingMutex; + Mutex updatingMutex; + Mutex processedMutex; + Mutex integrationMutex; + Mutex waitingMutex; + Substance* processedSubstance; + void* outputCallback; + ProceduralMaterial* lastProcessedSubstance; + Mutex lastProcessedSubstanceMutex; + std::vector<ProceduralMaterial*> animatedSubstanceArray; + ProceduralProcessorUsage m_ProcessorUsage; + ProceduralMaterial* queryClearCache; + Mutex deleteIntegrationMutex; + Mutex deletePingMutex; + bool isIntegrating; + Semaphore threadSemaphore; + static int s_maximumSubstancePerFrame; +}; + +SubstanceSystem& GetSubstanceSystem (); +SubstanceSystem* GetSubstanceSystemPtr (); + +#endif diff --git a/Runtime/Graphics/Texture.cpp b/Runtime/Graphics/Texture.cpp new file mode 100644 index 0000000..a343da4 --- /dev/null +++ b/Runtime/Graphics/Texture.cpp @@ -0,0 +1,332 @@ +#include "UnityPrefix.h" +#include "Texture.h" +#include "Image.h" +#include "Runtime/Utilities/BitUtility.h" +#include "Runtime/Shaders/GraphicsCaps.h" +#include "Runtime/GfxDevice/GfxDevice.h" +#include "External/shaderlab/Library/texenv.h" +#include "Runtime/Graphics/ProceduralMaterial.h" + +using namespace std; + +static int gTextureBaseLevel = 0; +static int gAnisoSetting = Texture::kEnableAniso; + +static const int kForceAnisoMinLevelDefault = 9; +static const int kAnisoMaxLevelDefault = 16; + +static int gForceAnisoMinLevel = kForceAnisoMinLevelDefault; +static int gAnisoMaxLevel = kAnisoMaxLevelDefault; + + +Texture::TextureIDMap Texture::s_TextureIDMap; + + +///@todo: texture should not allocate memory based on texture base level. + +Texture::Texture(MemLabelId label, ObjectCreationMode mode) +: Super(label, mode), m_UVScaleX(1.0f), m_UVScaleY(1.0f), m_TexelSizeX(1.0f), m_TexelSizeY(1.0f) +{ + // We use unchecked version since we may not be on the main thread + // This means CreateTextureID() implementation must be thread safe! + m_TexID = GetUncheckedGfxDevice().CreateTextureID(); + m_UsageMode = kTexUsageNone; + m_ColorSpace = kTexColorSpaceLinear; +} + +Texture::~Texture () +{ + MainThreadCleanup (); +} + +bool Texture::MainThreadCleanup () +{ + Texture::s_TextureIDMap.erase (m_TexID); + + // FreeTextureID() implementation must be thread safe! + GetUncheckedGfxDevice().FreeTextureID(m_TexID); + m_TexID = TextureID(); + + // Notify TexEnvs using this texture + for( size_t i = 0; i < m_TexEnvUsers.size(); ++i ) + { + ASSERT_RUNNING_ON_MAIN_THREAD + m_TexEnvUsers[i]->SetUsedTexture(NULL, 0); + } + m_TexEnvUsers.clear(); + return true; +} + + +void Texture::CheckConsistency() +{ + Super::CheckConsistency(); + m_TextureSettings.CheckConsistency (); +} + +void Texture::Reset () +{ + Super::Reset (); + m_UsageMode = kTexUsageNone; + m_ColorSpace = kTexColorSpaceLinear; +} + +void Texture::SetMasterTextureLimit (int i, bool reloadTextures /*= true*/) +{ + if (gTextureBaseLevel == i) + return; + + gTextureBaseLevel = i; + if (!reloadTextures) + return; + + vector<SInt32> objects; + Object::FindAllDerivedObjects (ClassID (Texture), &objects); + for( size_t j = 0; j < objects.size(); ++j ) + { + Texture& tex = *PPtr<Texture> (objects[j]); +#if UNITY_EDITOR + if (tex.IgnoreMasterTextureLimit ()) + continue; +#endif + tex.UnloadFromGfxDevice(false); + tex.UploadToGfxDevice(); + } + + ProceduralMaterial::ReloadAll(); +} + +void Texture::ReloadAll (bool unload, bool load, bool forceUnloadAll) +{ + vector<SInt32> objects; + Object::FindAllDerivedObjects (ClassID (Texture), &objects, true); + for (size_t i=0;i<objects.size ();++i) + { + Texture& tex = *PPtr<Texture> (objects[i]); + + if (unload) + tex.UnloadFromGfxDevice(forceUnloadAll); + if (load) + tex.UploadToGfxDevice(); + } + + ProceduralMaterial::ReloadAll(unload, load); +} + +static void SetAnisoLimitEnumImpl(int aniso, bool forced = false) +{ + if (aniso == gAnisoSetting && !forced) + return; + + gAnisoSetting = aniso; + if (gAnisoSetting == Texture::kDisableAniso) + TextureSettings::SetAnisoLimits (1, 1); + else if (gAnisoSetting == Texture::kForceEnableAniso) + TextureSettings::SetAnisoLimits (gForceAnisoMinLevel, gAnisoMaxLevel); + else + TextureSettings::SetAnisoLimits (1, gAnisoMaxLevel); + + vector<Texture*> objects; + Object::FindObjectsOfType (&objects); + for (int i=0;i<objects.size ();i++) + objects[i]->ApplySettings (); +} + +void Texture::SetAnisoLimit (int aniso) +{ + SetAnisoLimitEnumImpl(aniso, false); +} + + +void Texture::SetGlobalAnisoLimits(int forcedMin, int globalMax) +{ + if(forcedMin == -1) + forcedMin = kForceAnisoMinLevelDefault; + if(globalMax == -1) + globalMax = kAnisoMaxLevelDefault; + + if(gForceAnisoMinLevel == forcedMin && gAnisoMaxLevel == globalMax) + return; + + gForceAnisoMinLevel = forcedMin; + gAnisoMaxLevel = globalMax; + + SetAnisoLimitEnumImpl(gAnisoSetting, true); +} + +int Texture::GetAnisoLimit () +{ + return gAnisoSetting; +} + +bool Texture::HasMipMap () const {return false;} + +void Texture::ApplySettings() +{ + m_TextureSettings.Apply( GetTextureID(), GetDimension(), HasMipMap(), GetActiveTextureColorSpace() ); + NotifyMipBiasChanged(); +} + +void Texture::AddTexEnvUser(ShaderLab::TexEnv* texenv) +{ + SET_ALLOC_OWNER(this); + size_t index = m_TexEnvUsers.size(); + texenv->SetUsedTexture(this, index); + m_TexEnvUsers.push_back(texenv); +} + +void Texture::RemoveTexEnvUser(ShaderLab::TexEnv* texenv, size_t index) +{ + DebugAssert(m_TexEnvUsers[index] == texenv); + // Swap with last element and pop + m_TexEnvUsers[index] = m_TexEnvUsers.back(); + m_TexEnvUsers[index]->SetUsedTexture(this, index); + m_TexEnvUsers.pop_back(); + texenv->SetUsedTexture(NULL, 0); +} + +void Texture::NotifyMipBiasChanged() +{ + float mipBias = m_TextureSettings.m_MipBias; + for( size_t i = 0; i < m_TexEnvUsers.size(); ++i ) + { + ShaderLab::TexEnv* te = m_TexEnvUsers[i]; + te->TextureMipBiasChanged( mipBias ); + } +} + +void Texture::NotifyUVScaleChanged() +{ + float x = m_UVScaleX; + float y = m_UVScaleY; + for( size_t i = 0; i < m_TexEnvUsers.size(); ++i ) + { + ShaderLab::TexEnv* te = m_TexEnvUsers[i]; + te->TextureUVScaleChanged( x, y ); + } +} + + +void Texture::SetFilterMode (int mode) +{ + if (m_TextureSettings.m_FilterMode != mode) { + m_TextureSettings.m_FilterMode = mode; + ApplySettings(); + SetDirty(); + } +} + +void Texture::SetUsageMode ( TextureUsageMode mode) +{ + if (m_UsageMode != mode) { + m_UsageMode = mode; + ApplySettings(); + SetDirty(); + } +} + +void Texture::SetStoredColorSpace(TextureColorSpace space) +{ + if (m_ColorSpace != space) { + m_ColorSpace = space; + ApplySettings(); + SetDirty(); + } +} + +void Texture::SetStoredColorSpaceNoDirtyNoApply(TextureColorSpace space) +{ + if (m_ColorSpace != space) { + m_ColorSpace = space; + } +} + +void Texture::SetWrapMode (int mode) +{ + if (m_TextureSettings.m_WrapMode != mode) { + m_TextureSettings.m_WrapMode = mode; + ApplySettings(); + SetDirty(); + } +} + +void Texture::SetAnisoLevel (int level) +{ + if (m_TextureSettings.m_Aniso != level) { + m_TextureSettings.m_Aniso = level; + ApplySettings(); + SetDirty(); + } +} + +void Texture::SetMipMapBias (float mipBias) +{ + if (m_TextureSettings.m_MipBias != mipBias) + { + m_TextureSettings.m_MipBias = mipBias; + ApplySettings(); + SetDirty(); + } +} + + +#if UNITY_EDITOR +// Helper function so texture inspector can draw preview wrap mode +void Texture::SetWrapModeNoDirty (int mode) +{ + m_TextureSettings.m_WrapMode = mode; + ApplySettings(); +} + +// Helper function so texture inspector can draw preview aniso level +void Texture::SetAnisoLevelNoDirty (int level) +{ + m_TextureSettings.m_Aniso = level; + ApplySettings(); +} + +// Helper function so texture inspector can draw a specific mip of the texture +void Texture::SetMipMapBiasNoDirty (float mipBias) +{ + m_TextureSettings.m_MipBias = mipBias; + ApplySettings(); +} + +// Helper function for editor so it can draw zoomed textures as point. +void Texture::SetFilterModeNoDirty (int mode) +{ + if (m_TextureSettings.m_FilterMode != mode) { + m_TextureSettings.m_FilterMode = mode; + ApplySettings(); + } +} + +bool Texture::IgnoreMasterTextureLimit () const +{ + return false; +} + +#endif + +int Texture::GetMasterTextureLimit () +{ + return gTextureBaseLevel; +} + +bool Texture::ShouldIgnoreInGarbageDependencyTracking () +{ + return true; +} + +void* Texture::GetNativeTexturePtr() +{ + return GetGfxDevice().GetNativeTexturePointer(m_TexID); +} + +UInt32 Texture::GetNativeTextureID() +{ + return GetGfxDevice().GetNativeTextureID(m_TexID); +} + + +IMPLEMENT_CLASS (Texture) diff --git a/Runtime/Graphics/Texture.h b/Runtime/Graphics/Texture.h new file mode 100644 index 0000000..5babbbb --- /dev/null +++ b/Runtime/Graphics/Texture.h @@ -0,0 +1,165 @@ +#pragma once + +#include "Runtime/BaseClasses/NamedObject.h" +#include "TextureFormat.h" +#include "TextureSettings.h" +#include "Runtime/GfxDevice/GfxDeviceTypes.h" +#include "Runtime/Utilities/dynamic_array.h" +#include "Runtime/Math/ColorSpaceConversion.h" +#include "Runtime/Modules/ExportModules.h" + +#define TRACK_TEXTURE_SIZES (UNITY_EDITOR || ENABLE_PROFILER) + +class ImageReference; +namespace ShaderLab { class TexEnv; } + +class EXPORT_COREMODULE Texture : public NamedObject +{ +protected: + TextureSettings m_TextureSettings; + TextureID m_TexID; + + int m_UsageMode; + + int m_ColorSpace; + + // Used by movie textures so that 0..1 range covers only the + // movie portion. For all other textures this is (1,1). + float m_UVScaleX, m_UVScaleY; + + // Texel size. This is 1/size for all textures. + float m_TexelSizeX, m_TexelSizeY; + + // TexEnvs that use this texture + dynamic_array<ShaderLab::TexEnv*> m_TexEnvUsers; + +public: + REGISTER_DERIVED_ABSTRACT_CLASS (Texture, NamedObject) + + Texture(MemLabelId label, ObjectCreationMode mode); + + virtual bool MainThreadCleanup(); + + virtual void Reset (); + + virtual void CheckConsistency(); + + virtual TextureDimension GetDimension () const = 0; + + // Blits the textures contents into image. + // Will use the closest matching mipmap. + virtual bool ExtractImage (ImageReference* image, int imageIndex = 0) const = 0; + + TextureID GetTextureID() const { return m_TexID; } + + TextureUsageMode GetUsageMode() const { return static_cast<TextureUsageMode>(m_UsageMode); } + void SetUsageMode(TextureUsageMode mode); + + // The Color space the texture is imported for. + void SetStoredColorSpace(TextureColorSpace space); + // Needed to avoid an odd corner case for srgb cube render textures. + // If a render texture has the apply function called a texture id will be created and + // it will cause an error as it will be the wrong dimension when cubemap is set + // This function sets the stored color space without applying it. It will be applied + // when the create on the cubemap is called. + void SetStoredColorSpaceNoDirtyNoApply(TextureColorSpace space); + TextureColorSpace GetStoredColorSpace() const { return static_cast<TextureColorSpace>(m_ColorSpace); } + + // The active color space of the texture + // Depending on if we are in linear or gamma rendering mode, textures will be tagged as sRGB or non-SRGB. + TextureColorSpace GetActiveTextureColorSpace() const { return (GetActiveColorSpace() == kLinearColorSpace) ? GetStoredColorSpace() : kTexColorSpaceLinear; } + + #if ENABLE_PROFILER + virtual int GetStorageMemorySize() const = 0; + #endif + + virtual TextureID GetUnscaledTextureID() const { return m_TexID; } + + // Raw (original) texture size. For NPOT textures these values are NPOT. + virtual int GetDataWidth() const = 0; + virtual int GetDataHeight() const = 0; + + // Size as used by GL. In most cases this matches Data width/height, except for NPOT textures + // where this is scaled up to next power of two. + virtual int GetGLWidth() const { return GetDataWidth(); } + virtual int GetGLHeight() const { return GetDataHeight(); } + + virtual bool HasMipMap () const = 0; + virtual int CountMipmaps () const = 0; + + static void SetMasterTextureLimit (int i, bool reloadTextures = true); + static int GetMasterTextureLimit (); + + static void ReloadAll (bool unload = true, bool load = true, bool forceUnloadAll = false); + + /// this is at the wrong place + enum { kDisableAniso = 0, kEnableAniso = 1, kForceEnableAniso = 2 }; + + // Get/Set the global texture anisotrophy levels + static void SetAnisoLimit (int aniso); + static int GetAnisoLimit (); + + static void SetGlobalAnisoLimits(int forcedMin, int globalMax); + + static Texture* FindTextureByID (TextureID tid) + { + TextureIDMap::iterator it = s_TextureIDMap.find(tid); + return it == s_TextureIDMap.end() ? NULL : it->second; + } + + // Set the filtering mode for this texture. + // TextureFilterMode kTexFilterNearest, kTexFilterBilinear, kTexFilterTrilinear + void SetFilterMode( int mode ); + int GetFilterMode() const { return m_TextureSettings.m_FilterMode; } + + void SetWrapMode (int mode); + int GetWrapMode () const { return m_TextureSettings.m_WrapMode; } + + void SetAnisoLevel (int mode); + int GetAnisoLevel () const { return m_TextureSettings.m_Aniso; } + + float GetMipMapBias () const { return m_TextureSettings.m_MipBias; } + void SetMipMapBias (float bias); + + const TextureSettings& GetSettings() const { return m_TextureSettings; } + TextureSettings& GetSettings() { return m_TextureSettings; } + virtual void ApplySettings(); + + float GetUVScaleX() const { return m_UVScaleX; } + float GetUVScaleY() const { return m_UVScaleY; } + void SetUVScale( float x, float y ) { m_UVScaleX = x; m_UVScaleY = y; NotifyUVScaleChanged(); } + float GetTexelSizeX() const { return m_TexelSizeX; } + float GetTexelSizeY() const { return m_TexelSizeY; } + void SetTexelSize( float x, float y ) { m_TexelSizeX = x; m_TexelSizeY = y; } + + void AddTexEnvUser(ShaderLab::TexEnv* texenv); + void RemoveTexEnvUser(ShaderLab::TexEnv* texenv, size_t index); + + virtual bool ShouldIgnoreInGarbageDependencyTracking (); + + +#if UNITY_EDITOR + void SetAnisoLevelNoDirty (int level); + void SetWrapModeNoDirty (int mode); + void SetMipMapBiasNoDirty (float mipBias); + void SetFilterModeNoDirty (int mode); + virtual bool IgnoreMasterTextureLimit () const; + + virtual TextureFormat GetEditorUITextureFormat () const = 0; +#endif + + void* GetNativeTexturePtr(); + UInt32 GetNativeTextureID(); + +protected: + void NotifyMipBiasChanged(); + void NotifyUVScaleChanged(); + + // Used when changing master texture mip limit or explicitly reloading/unloading + // all resources. + virtual void UnloadFromGfxDevice(bool forceUnloadAll) = 0; + virtual void UploadToGfxDevice() = 0; + + typedef std::map<TextureID, Texture*> TextureIDMap; + static TextureIDMap s_TextureIDMap; +}; diff --git a/Runtime/Graphics/Texture2D.cpp b/Runtime/Graphics/Texture2D.cpp new file mode 100644 index 0000000..691dfd9 --- /dev/null +++ b/Runtime/Graphics/Texture2D.cpp @@ -0,0 +1,1422 @@ +#include "UnityPrefix.h" +#include "Configuration/UnityConfigure.h" +#include "Texture2D.h" +#include "Image.h" +#include "RenderTexture.h" +#include "Runtime/Utilities/BitUtility.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "S3Decompression.h" +#include "Runtime/Camera/RenderManager.h" +#include "Runtime/Serialize/SwapEndianArray.h" +#include "Runtime/Shaders/GraphicsCaps.h" +#include "Runtime/Math/Color.h" +#include "Runtime/GfxDevice/GfxDevice.h" +#include "Runtime/GfxDevice/GfxDeviceConfigure.h" +#include "ImageConversion.h" +#include "Runtime/Misc/Allocator.h" +#include "DXTCompression.h" +#include "Runtime/Serialize/PersistentManager.h" +#include "Runtime/BaseClasses/IsPlaying.h" +#include "Runtime/Profiler/Profiler.h" +#include "Runtime/Profiler/MemoryProfiler.h" +#include "Runtime/Threads/Thread.h" +#include "Runtime/Threads/Mutex.h" +#include "Runtime/Math/ColorSpaceConversion.h" +#include "Runtime/Misc/BuildSettings.h" + +#if UNITY_EDITOR +#include "CubemapTexture.h" +#endif + +#if UNITY_WII +#include "PlatformDependent/wii/WiiUtility.h" +#endif + +#if ENABLE_TEXTUREID_MAP +#include "Runtime/GfxDevice/TextureIdMap.h" +#endif + + +#define USE_IMMEDIATE_INTEGRATION (UNITY_IPHONE || UNITY_ANDROID || UNITY_BB10 || UNITY_PS3 || UNITY_XENON || UNITY_TIZEN) + + +PROFILER_INFORMATION(gIntegrateLoadedImmediately, "Texture.IntegrateLoadedImmediately", kProfilerLoading) +PROFILER_INFORMATION(gAwakeFromLoadTex2D, "Texture.AwakeFromLoad", kProfilerLoading) + +/* + Regular and non-power-of-two textures: + + For regular textures, all TextureRepresentation's are exactly the same; and the data is allocated just + once (all representations point to the same data). + + For NPOT textures: + * m_TexData is what is serialized (non power of two). Mip maps use floor(size/2) convention, + DXT compression is actually next multiple of 4. + * If no mipmaps are specified and NPOTRestricted is supported - act as regular texture + * If mipmaps are specified and NPOTFULL is supported - act as regular texture + * If NPOT textures are not supported: + * m_Tex is scaled up to POT at load time. DXT source gets decompressed into ARGB32. + * m_TexPadded is POT size, but the original is padded by repeating border pixels. GUITexture uses it. +*/ + +bool Texture2D::s_ScreenReadAllowed = true; + +static inline UInt32 GetBytesForOnePixel( TextureFormat format ) +{ + return IsAnyCompressedTextureFormat(format) ? 0 : GetBytesFromTextureFormat(format); +} + +bool IsNPOTTextureAllowed(bool hasMipMap) +{ + if(IS_CONTENT_NEWER_OR_SAME(kUnityVersion4_1_a1)) + return hasMipMap ? gGraphicsCaps.npot == kNPOTFull : gGraphicsCaps.npot >= kNPOTRestricted; + else + return false; +} + +static int GetNextAllowedTextureSize(int size, bool hasMipMap, TextureFormat format) +{ + int multipleMask = GetTextureSizeAllowedMultiple(format) - 1; + size = (size + multipleMask) & ~multipleMask; + if (!IsNPOTTextureAllowed(hasMipMap)) + size = NextPowerOfTwo(size); + return size; +} + + +Texture2D::TextureRepresentation::TextureRepresentation() +: data(NULL) +, width(0) +, height(0) +, format(kTexFormatARGB32) +, imageSize(0) +{ +} + +Texture2D::Texture2D (MemLabelId label, ObjectCreationMode mode) +: Super(label, mode) +, m_InitFlags(0) +{ + #if UNITY_EDITOR + m_IgnoreMasterTextureLimit = false; + #endif + + m_MipMap = false; + m_TextureDimension = kTexDimNone; + +#if UNITY_EDITOR + m_EditorDontWriteTextureData = false; + m_AlphaIsTransparency = false; +#endif + + m_PowerOfTwo = true; + + m_ImageCount = 0; + + m_IsReadable = true; + m_IsUnreloadable = false; + m_ReadAllowed = true; + m_TextureUploaded = false; + m_UnscaledTextureUploaded = false; + + // We use unchecked version since we may not be on the main thread + // This means CreateTextureID() implementation must be thread safe! + m_UnscaledTexID = GetUncheckedGfxDevice().CreateTextureID(); +} + +void Texture2D::Reset () +{ + Super::Reset(); + + m_TextureSettings.Reset (); +} + +//#define LOG_AWAKE(x, arg) printf_console(x, arg); +#define LOG_AWAKE(x, arg) + +typedef std::vector<Texture2D*> TexturesT; +static TexturesT g_TexturesToUploadOnMainThread; +static Mutex g_UploadMutex; + +void Texture2D::AwakeFromLoadThreaded() +{ + Super::AwakeFromLoadThreaded(); + +#if USE_IMMEDIATE_INTEGRATION + LOG_AWAKE("Texture2D: AwakeFromLoadThreaded (%s)\n", GetName()); + g_UploadMutex.Lock(); + g_TexturesToUploadOnMainThread.push_back(this); + g_UploadMutex.Unlock(); + // Pause loading for a while + // this will allow main thread to start integrating previously loaded textures + Thread::Sleep(0.001); +#endif // USE_IMMEDIATE_INTEGRATION +} + +// Helps to avoid memory peak while loading the level on platforms where graphics driver creates an internal copy of texture data +// +// Memory peak arrises in the following scenario: +// 1) [Load thread] all texture assets are loaded +// 2) [Main thread] for every asset (OnAwake) +// a) texture data is uploaded to graphics driver (copy) +// b) texture data is released (in case of NonReadable textures) +// +// Instead we do: +// 1) [Load thread] load 1 texture asset +// 2) [Load thread] stall for a moment and allow Main thread to start uploading +// 3) [Main thread] upload as much texture data as possible +// 4) [Main thread] if significantly behind Load thread, then block Load thread +// 5) continue for all assets +// +// NOTE: Main thread calls IntegrateLoadedImmediately() from PreloadManager::UpdatePreloadingSingleStep() +void Texture2D::IntegrateLoadedImmediately() +{ +#if USE_IMMEDIATE_INTEGRATION + + PROFILER_AUTO(gIntegrateLoadedImmediately, NULL) + + g_UploadMutex.Lock(); + TexturesT toUpload = g_TexturesToUploadOnMainThread; + g_TexturesToUploadOnMainThread.clear(); + + size_t totalTextureSize = 0; + for (size_t q = 0; q < toUpload.size(); ++q) + totalTextureSize += toUpload[q]->m_TexData.imageSize; + + // Evalute texture data ready for immediate integration + // If we have too much data to upload, then block the loading thread! + // Loading thread will stall until all pending data is uploaded. + bool blockLoadingThread = totalTextureSize > (512*1024); + + // Avoid stalling on every texture (if possible) to improve loading speed + if (!blockLoadingThread) + { + g_UploadMutex.Unlock(); + } + else + { + LOG_AWAKE("Texture2D: IntegrateLoadedImmediately() will block loader thread, pending texture data size: %d bytes\n", totalTextureSize); + } + + for (size_t q = 0; q < toUpload.size(); ++q) + { + Texture2D* tex = toUpload[q]; + LOG_AWAKE("Texture2D: Immediately upload texture data (%s)\n", tex->GetName()); + if (tex->m_TexData.data != NULL) + tex->UploadTexture (false); + } + + if (blockLoadingThread) + g_UploadMutex.Unlock(); + +#endif // USE_IMMEDIATE_INTEGRATION +} + +void Texture2D::AwakeFromLoad (AwakeFromLoadMode awakeMode) +{ + SET_ALLOC_OWNER(this); + Super::AwakeFromLoad (awakeMode); + + if( (awakeMode == kDefaultAwakeFromLoad || awakeMode == kInstantiateOrCreateFromCodeAwakeFromLoad) + && m_TexData.data == NULL + ) + { + // actually pretty valid + return; + } + +#if USE_IMMEDIATE_INTEGRATION + // Check if texture data was already uploaded by IntegrateLoadedImmediately() + if ((awakeMode & kDidLoadThreaded) != 0) + { + bool dynamicTexture = m_TexData.imageSize == 0; + Assert(m_TextureUploaded || dynamicTexture); + + if (m_TextureUploaded) + return; + } + LOG_AWAKE("Texture2D: Upload texture data in Awake (%s)\n", GetName()); +#endif // USE_IMMEDIATE_INTEGRATION + + PROFILER_AUTO(gAwakeFromLoadTex2D, this) + UploadTexture (false); +} + +Texture2D::~Texture2D () +{ + DestroyTexture (); + MainThreadCleanup (); +} + +bool Texture2D::MainThreadCleanup () +{ + DeleteGfxTexture (); + + Texture::s_TextureIDMap.erase (m_UnscaledTexID); + + // FreeTextureID() implementation must be thread safe! + GetUncheckedGfxDevice().FreeTextureID(m_UnscaledTexID); + m_UnscaledTexID = TextureID(); + + return Super::MainThreadCleanup (); +} + + +static int SourceMipLevelForBlit( int srcWidth, int srcHeight, int dstWidth, int dstHeight ) +{ + int level = HighestBit( NextPowerOfTwo(srcWidth) ) - HighestBit( NextPowerOfTwo(dstWidth) ); + level = std::max( level, HighestBit( NextPowerOfTwo(srcHeight) ) - HighestBit( NextPowerOfTwo(dstHeight) ) ); + level = std::max( 0, level ); + return level; +} + +// Compressed->compressed extraction. Copies compressed blocks and pads the remainder with +// transparent black. +void Texture2D::ExtractCompressedImageInternal( UInt8* dst, int dstWidth, int dstHeight, int imageIndex ) const +{ + AssertIf( imageIndex < 0 || imageIndex >= m_ImageCount ); + + TextureRepresentation const& rep =m_TexData; + AssertIf( !IsAnyCompressedTextureFormat( rep.format ) ); + if (m_TexData.data == NULL) + { + ErrorString ("Texture data can not be accessed"); + return; + } + + int mipmapLevel = SourceMipLevelForBlit( rep.width, rep.height, dstWidth, dstHeight ); + mipmapLevel = std::min( mipmapLevel, CountDataMipmaps() - 1 ); + int mipmapoffset = CalculateMipMapOffset( rep.width, rep.height, rep.format, mipmapLevel ); + int width = std::max( rep.width >> mipmapLevel, 1 ); + int height = std::max( rep.height >> mipmapLevel, 1 ); + + // pad-copy compressed->compressed + BlitCopyCompressedImage( rep.format, rep.data + imageIndex * rep.imageSize + mipmapoffset, + width, height, dst, dstWidth, dstHeight, true ); +} + + +bool Texture2D::ExtractImageInternal( ImageReference* image, bool scaleToSize, int imageIndex ) const +{ + AssertIf (imageIndex < 0 || imageIndex >= m_ImageCount); + + TextureRepresentation const& rep = m_TexData; + if(rep.data == NULL) + { + ErrorString ("Texture is not accessible."); + return false; + } + + int mipmapLevel = SourceMipLevelForBlit( rep.width, rep.height, image->GetWidth(), image->GetHeight() ); + mipmapLevel = std::min( mipmapLevel, CountDataMipmaps() - 1 ); + int mipmapoffset = CalculateMipMapOffset( rep.width, rep.height, rep.format, mipmapLevel ); + int width = std::max( rep.width >> mipmapLevel, 1 ); + int height = std::max( rep.height >> mipmapLevel, 1 ); + + if( IsAnyCompressedTextureFormat(rep.format) ) + { + // Decompress into upper multiple-of-4 size + int decompWidth = (width + 3) / 4 * 4; + int decompHeight = (height + 3) / 4 * 4; + + Image decompressed( decompWidth, decompHeight, kTexFormatRGBA32 ); + UInt8* compressed = rep.data + imageIndex * rep.imageSize + mipmapoffset; + + if (!DecompressNativeTextureFormatWithMipLevel( rep.format, width, height, mipmapLevel, (UInt32*)compressed, decompWidth, decompHeight, (UInt32*)decompressed.GetImageData() )) + return false; + + // Clip this back to original size + ImageReference clipped = decompressed.ClipImage( 0, 0, width, height ); + + if( scaleToSize ) + { + image->BlitImage( clipped, ImageReference::BLIT_BILINEAR_SCALE ); + } + else + { + AssertIf( width > image->GetWidth() || height > image->GetHeight() ); + image->BlitImage( clipped, ImageReference::BLIT_COPY ); + PadImageBorder( *image, width, height ); + } + return true; + } + else + { + ImageReference source( width, height, width * GetBytesFromTextureFormat(rep.format), + rep.format, rep.data + imageIndex * rep.imageSize + mipmapoffset); + + if( scaleToSize ) + { + image->BlitImage( source, ImageReference::BLIT_BILINEAR_SCALE ); + } + else + { + AssertIf( width > image->GetWidth() || height > image->GetHeight() ); + image->BlitImage( source, ImageReference::BLIT_COPY ); + PadImageBorder( *image, width, height ); + } + return true; + } + + return false; +} +bool Texture2D::ExtractImage (ImageReference* image, int imageIndex) const +{ + return ExtractImageInternal( image, true, imageIndex ); +} + +bool Texture2D::HasMipMap() const { + return m_MipMap; +} + +int Texture2D::CountMipmaps() const +{ + if (m_MipMap) + return CalculateMipMapCount3D( m_glWidth, m_glHeight, 1 ); + else + return 1; +} +int Texture2D::CountDataMipmaps() const +{ + if (m_MipMap) + return CalculateMipMapCount3D( m_TexData.width, m_TexData.height, 1 ); + else + return 1; +} + +UInt8* Texture2D::AllocateTextureData (int imageSize, TextureFormat format, bool initMemory) +{ + // Allocate one more pixel because software bi-linear filtering might require it. + int allocSize = imageSize + GetBytesForOnePixel(format); + void* buffer = UNITY_MALLOC_ALIGNED (GetTextureDataMemoryLabel(), allocSize, 32); + + // In the past the memory manager cleared textures created from script to 0xcd. + // Let's keep doing that even though a color like white makes more sense (case 564961). + if (initMemory && buffer) + memset(buffer, 0xcd, allocSize); + + return static_cast<UInt8*>(buffer); +} + +void Texture2D::DeallocateTextureData (UInt8* tex) +{ + UNITY_FREE (GetTextureDataMemoryLabel(), tex); +} + +void Texture2D::InitTextureInternal (int width, int height, TextureFormat format, int imageSize, UInt8* buffer, int options, int imageCount) +{ + // Cleanup old memory + if ((options & kThreadedInitialize) == 0) + { + DestroyTexture (); + SetDirty (); + } + else + { + AssertIf(m_TexData.data != NULL); + } + + m_TextureDimension = kTexDim2D; + + m_TexData.width = width; + m_TexData.height = height; + m_TexData.format = format; + m_TexData.imageSize = imageSize; + m_TexData.data = buffer; + m_MipMap = options & kMipmapMask; + m_InitFlags = options; + m_ImageCount = imageCount; + UpdatePOTStatus(); + + m_glWidth = GetNextAllowedTextureSize(m_TexData.width, m_MipMap, format); + m_glHeight = GetNextAllowedTextureSize(m_TexData.height, m_MipMap, format); + SetTexelSize( 1.0f/m_glWidth, 1.0f/m_glHeight ); +} + +bool Texture2D::InitTexture (int width, int height, TextureFormat format, int options, int imageCount, intptr_t nativeTex) +{ + SET_ALLOC_OWNER(this); + if (width < 0 || width > 10000 || height < 0 || height > 10000) + { + ErrorStringObject ("Texture has out of range width / height", this); + return false; + } + + if (!IsValidTextureFormat(format)) + { + ErrorStringObject ("TextureFormat is invalid!", this); + return false; + } + + int imageSize; + if( options & kMipmapMask) + imageSize = CalculateImageMipMapSize( width, height, format ); + else + imageSize = CalculateImageSize( width, height, format ); + + unsigned int tlen = imageSize * imageCount; + // probably an multiplication overflow + if (imageSize != 0 && imageCount != tlen / imageSize) + return false; + // probably an addition overflow + if (tlen + GetBytesForOnePixel(format) < tlen) + return false; + + bool allocData = ENABLE_TEXTUREID_MAP == 0 || nativeTex == 0; + +#if ENABLE_TEXTUREID_MAP + if(nativeTex) + TextureIdMap::UpdateTexture(m_TexID, GetGfxDevice().CreateExternalTextureFromNative(nativeTex)); +#endif + + UInt8* buffer = allocData ? AllocateTextureData(imageSize * imageCount, format, true) : 0; + InitTextureInternal (width, height, format, imageSize, buffer, options, imageCount); + + return true; +} + +void Texture2D::RebuildMipMap () +{ + TextureRepresentation& rep = m_TexData; + + if( rep.data == NULL || !m_MipMap ) + return; + if( IsAnyCompressedTextureFormat(rep.format) ) + { + ErrorString ("Rebuilding mipmaps of compressed textures is not supported"); + return; + } + + for( int i=0;i<m_ImageCount;i++ ) + CreateMipMap (rep.data + rep.imageSize * i, rep.width, rep.height, 1, rep.format); +} + + +#if UNITY_EDITOR +void Texture2D::SetImage (const ImageReference& image, int flags) +{ + SET_ALLOC_OWNER(this); + InitTexture( image.GetWidth (), image.GetHeight (), image.GetFormat (), flags ); + + int bytesPerPixel = GetBytesFromTextureFormat (m_TexData.format); + + ImageReference dst( m_TexData.width, m_TexData.height, bytesPerPixel * m_TexData.width, m_TexData.format, m_TexData.data ); + if (image.GetImageData () != NULL) + dst.BlitImage (image); + + RebuildMipMap (); + + UploadTexture (true); + SetDirty (); +} +#endif + + + +bool Texture2D::GetImageReferenceInternal (ImageReference* image, int frame, int miplevel) const +{ + TextureRepresentation const& rep = m_TexData; + + if( rep.data == NULL || IsAnyCompressedTextureFormat(rep.format) ) + return false; + + AssertIf (frame >= m_ImageCount); + AssertIf (miplevel >= CountDataMipmaps ()); + + UInt8* base = rep.data + frame * rep.imageSize; + base += CalculateMipMapOffset( rep.width, rep.height, rep.format, miplevel ); + int width = std::max (rep.width >> miplevel, 1); + int height = std::max (rep.height >> miplevel, 1); + + *image = ImageReference( width, height, GetRowBytesFromWidthAndFormat(width, rep.format), rep.format, base ); + return true; +} + +bool Texture2D::GetWriteImageReference (ImageReference* image, int frame, int miplevel) +{ + return GetImageReferenceInternal (image, frame, miplevel); +} + + +void Texture2D::ApplySettings() +{ + TextureDimension texdim = GetDimension(); + m_TextureSettings.Apply( GetTextureID(), texdim, HasMipMap(), GetActiveTextureColorSpace()); + if( m_UnscaledTextureUploaded ) + m_TextureSettings.Apply( GetUnscaledTextureID(), texdim, HasMipMap(), GetActiveTextureColorSpace() ); + + NotifyMipBiasChanged(); +} + +void Texture2D::UpdatePOTStatus() +{ + m_PowerOfTwo = IsPowerOfTwo(m_TexData.width) && IsPowerOfTwo(m_TexData.height); + + // force clamp if we will keep it npot + if(!m_PowerOfTwo && !m_MipMap && gGraphicsCaps.npot == kNPOTRestricted) + m_TextureSettings.m_WrapMode = kTexWrapClamp; +} + + +void Texture2D::UploadTexture (bool dontUseSubImage) +{ + if (m_TexData.data == NULL) + { + ErrorString("No Texture memory available to upload"); + return; + } + if (m_TexData.width == 0 || m_TexData.height == 0) + { + return; + } + + TextureRepresentation scaled = m_TexData; + TextureRepresentation padded = m_TexData; + + InitTextureRepresentations(&scaled, &padded); + + int mipCount = CountMipmaps(); + + int masterTextureLimit = Texture::GetMasterTextureLimit(); + #if UNITY_EDITOR + if (m_IgnoreMasterTextureLimit) + masterTextureLimit = 0; + #endif + + // Master texture limit must be clamped to mipCount-1 or otherwise GfxDevice won't upload anything + masterTextureLimit = min(mipCount-1, masterTextureLimit); + + TextureUsageMode usageMode = GetUsageMode(); + + // upload regular texture + { + TextureRepresentation& rep = scaled; + UInt8* srcData = rep.data; + UInt32 uploadFlags = (dontUseSubImage || !m_TextureUploaded) ? GfxDevice::kUploadTextureDontUseSubImage : GfxDevice::kUploadTextureDefault; + if (m_InitFlags & kOSDrawingCompatible) + uploadFlags |= GfxDevice::kUploadTextureOSDrawingCompatible; + GetGfxDevice().UploadTexture2D( GetTextureID(), GetDimension(), srcData, rep.imageSize, rep.width, rep.height, rep.format, mipCount, uploadFlags, masterTextureLimit, usageMode, GetActiveTextureColorSpace() ); + Texture::s_TextureIDMap.insert (std::make_pair(GetTextureID(),this)); + m_TextureSettings.Apply( GetTextureID(), GetDimension(), m_MipMap, GetActiveTextureColorSpace()); + m_TextureUploaded = true; + } + + // upload unscaled one if NPOT and not supported + if( m_TexData.width != m_glWidth || m_TexData.height != m_glHeight ) + { + TextureRepresentation& rep = padded; + UInt8* srcData = rep.data; + + UInt32 uploadFlags = (dontUseSubImage || !m_UnscaledTextureUploaded) ? GfxDevice::kUploadTextureDontUseSubImage : GfxDevice::kUploadTextureDefault; + if (m_InitFlags & kOSDrawingCompatible) + uploadFlags |= GfxDevice::kUploadTextureOSDrawingCompatible; + + m_UnscaledTextureUploaded = true; // must be before Upload in order for GetUnscaledGLTextureName() to return the right value + TextureID tid = GetUnscaledTextureID(); + GetGfxDevice().UploadTexture2D( tid, GetDimension(), srcData, rep.imageSize, rep.width, rep.height, rep.format, mipCount, uploadFlags, masterTextureLimit, usageMode, GetActiveTextureColorSpace() ); + Texture::s_TextureIDMap.insert (std::make_pair(tid,this)); + m_TextureSettings.Apply( tid, GetDimension(), m_MipMap, GetActiveTextureColorSpace() ); + } + +#if UNITY_XENON && !MASTER_BUILD + GetGfxDevice().SetTextureName( GetTextureID(), GetName() ); +#endif + +#if UNITY_EDITOR + DestroyTextureRepresentations(&scaled, &padded, false); +#elif UNITY_WII + // On Wii texture data is being directly referenced from m_TexData.data, so don't delete it after uploading + DestroyTextureRepresentations(&scaled, &padded, false); +#else + DestroyTextureRepresentations(&scaled, &padded, !m_IsReadable); + if(!m_IsReadable) + m_TexData.imageSize = 0; +#endif +} + +void Texture2D::DestroyTextureRepresentation( TextureRepresentation* rep ) +{ + if( rep ) + { + if( rep->data == m_TexData.data ) + rep->data = NULL; + + DeallocateTextureData (rep->data); + rep->data = NULL; + } +} + +void Texture2D::DestroyTextureRepresentations( TextureRepresentation* scaled, TextureRepresentation* padded, bool freeSourceImage ) +{ + DestroyTextureRepresentation(padded); + DestroyTextureRepresentation(scaled); + + if( freeSourceImage ) + { + DeallocateTextureData (m_TexData.data); + m_TexData.data = NULL; + } +} + +void Texture2D::DeleteGfxTexture () +{ + if (m_TextureUploaded) + { + ASSERT_RUNNING_ON_MAIN_THREAD + GetGfxDevice().DeleteTexture( GetTextureID() ); + m_TextureUploaded = false; + } + if (m_UnscaledTextureUploaded) + { + ASSERT_RUNNING_ON_MAIN_THREAD + GetGfxDevice().DeleteTexture( GetUnscaledTextureID() ); + m_UnscaledTextureUploaded = false; + } +} + + +void Texture2D::DestroyTexture () +{ + DestroyTextureRepresentations (0,0,true); + + DeleteGfxTexture (); +} + + +void Texture2D::UnloadFromGfxDevice(bool forceUnloadAll) +{ + if (m_IsUnreloadable && !forceUnloadAll) + return; + DeleteGfxTexture (); +} + +void Texture2D::UploadToGfxDevice() +{ + if (m_IsUnreloadable) + return; + + GfxDevice& device = GetGfxDevice(); + + // We need to load the texture data from disk. Awake FromLoad will be called during ReloadFromDisk. + bool needReloadFromDisk = (m_TexData.data == NULL && !m_IsReadable); + if (needReloadFromDisk) + { + TextureSettings settings = m_TextureSettings; + + GetPersistentManager().ReloadFromDisk(this); + + m_TextureSettings = settings; + ApplySettings(); + } + // Just upload the texture data we already have in memory + else + { + UploadTexture (true); + } +} + + +#if UNITY_EDITOR +struct TemporaryTextureSerializationRevert +{ + Texture2D* texture; + Texture2D::TextureRepresentation texData; + int imageCount; + bool isReadable; + + TemporaryTextureSerializationRevert (Texture2D& tex2D, bool doRevert) + { + texture = NULL; + + if (doRevert) + { + texture = &tex2D; + + texData = texture->m_TexData; + imageCount = texture->m_ImageCount; + isReadable = texture->m_IsReadable; + } + } + ~TemporaryTextureSerializationRevert () + { + if (texture != NULL) + { + texture->m_TexData = texData; + texture->m_ImageCount = imageCount; + texture->m_IsReadable = isReadable; + } + } +}; +#endif + +template<class TransferFunction> +void Texture2D::Transfer (TransferFunction& transfer) +{ + Super::Transfer (transfer); + +#if UNITY_EDITOR + + // Store serialization state temporarliy and revert it when exiting function + TemporaryTextureSerializationRevert revert(*this, !transfer.IsReading ()); + + // When writing dynamic font textures we don't want to write any texture data to disk + if (m_EditorDontWriteTextureData && !transfer.IsReading ()) + { + m_ImageCount = m_TexData.imageSize = m_TexData.height = m_TexData.width = 0; + m_TexData.data = NULL; + } + + // readable flag gets adjusted by forced readable flag + if (transfer.IsBuildingTargetPlatform(kBuildAnyPlayerData)) + m_IsReadable |= transfer.GetBuildUsage().forceTextureReadable; + + TRANSFER_EDITOR_ONLY_HIDDEN(m_AlphaIsTransparency); + transfer.Align(); +#endif + + // In case we're converting the texture data to another format, we also properly write out the format ID. + transfer.Transfer( m_TexData.width, "m_Width", kNotEditableMask ); + transfer.Transfer( m_TexData.height, "m_Height", kNotEditableMask ); + transfer.Transfer( m_TexData.imageSize, "m_CompleteImageSize", kNotEditableMask ); + transfer.Transfer( m_TexData.format, "m_TextureFormat", kHideInEditorMask ); + transfer.Transfer( m_MipMap, "m_MipMap", kNotEditableMask ); + + transfer.Transfer( m_IsReadable, "m_IsReadable"); + transfer.Transfer( m_ReadAllowed, "m_ReadAllowed", kNotEditableMask ); + transfer.Align(); + transfer.Transfer( m_ImageCount, "m_ImageCount", kNotEditableMask ); + transfer.Transfer( m_TextureDimension, "m_TextureDimension", kHideInEditorMask ); + transfer.Transfer( m_TextureSettings, "m_TextureSettings"); + transfer.Transfer( m_UsageMode, "m_LightmapFormat"); + transfer.Transfer( m_ColorSpace, "m_ColorSpace"); + + unsigned imageSizeXImageCount = m_ImageCount * m_TexData.imageSize; + + if (!transfer.IsWritingGameReleaseData ()) + transfer.TransferTypeless (&imageSizeXImageCount, "image data", kHideInEditorMask); + + if (transfer.IsReading ()) + { + DestroyTexture (); + Assert(GetMemoryLabel().label != kMemTextureCacheId); + m_TexData.data = AllocateTextureData(imageSizeXImageCount, m_TexData.format); + m_glWidth = GetNextAllowedTextureSize(m_TexData.width, m_MipMap, TextureFormat(m_TexData.format)); + m_glHeight = GetNextAllowedTextureSize(m_TexData.height, m_MipMap, TextureFormat(m_TexData.format)); + SetTexelSize( 1.0f/m_glWidth, 1.0f/m_glHeight ); + } + + // write tex image + if (transfer.IsWriting()) + { + const bool gpuBE = UNITY_EDITOR ? transfer.IsBuildingTargetPlatform(kBuildXBOX360) : false; + + UInt8* convertedData = NULL; + if( transfer.ConvertEndianess() ) + { + convertedData = AllocateTextureData(imageSizeXImageCount, m_TexData.format); + ConvertTextureEndianessWrite(m_TexData.format, m_TexData.data, convertedData, imageSizeXImageCount, gpuBE); + } + + if (transfer.IsWritingGameReleaseData ()) + transfer.TransferTypeless (&imageSizeXImageCount, "image data", kHideInEditorMask); + transfer.TransferTypelessData (imageSizeXImageCount, convertedData ? convertedData : m_TexData.data ); + + DeallocateTextureData(convertedData); + } + else + { + if( transfer.ConvertEndianess() ) + { + transfer.TransferTypelessData (imageSizeXImageCount, m_TexData.data); + ConvertTextureEndianessRead (m_TexData.format, m_TexData.data, imageSizeXImageCount); + } + else + { + transfer.TransferTypelessData (imageSizeXImageCount, m_TexData.data); + } + } + + AssertIf ( m_TexData.imageSize != 0 && m_TexData.data == NULL ); + + if( transfer.IsReading() ) + UpdatePOTStatus(); +} + +void ConvertTextureEndianessWrite (int format, UInt8* src, UInt8* dst, int size, bool bBigEndianGPU) +{ + memcpy (dst, src, size); + if (format == kTexFormatARGBFloat) + SwapEndianArray (dst, 4, size / 4); + else if (format == kTexFormatRGB565 || format == kTexFormatARGB4444 || format == kTexFormatRGBA4444) + SwapEndianArray (dst, 2, size / 2); + else if (bBigEndianGPU && (format == kTexFormatDXT1 || format == kTexFormatDXT3 || format == kTexFormatDXT5)) + SwapEndianArray (dst, 2, size / 2); + +} + +void ConvertTextureEndianessRead (int format, UInt8* dst, int size) +{ + if (format == kTexFormatARGBFloat) + SwapEndianArray (dst, 4, size / 4); + else if (format == kTexFormatRGB565 || format == kTexFormatARGB4444 || format == kTexFormatRGBA4444) + SwapEndianArray (dst, 2, size / 2); +} + + +void Texture2D::UpdateImageData () +{ + RebuildMipMap (); + UploadTexture (false); + SetDirty (); +} + +void Texture2D::UpdateImageDataDontTouchMipmap () +{ + UploadTexture (false); + SetDirty (); +} + + +TextureID Texture2D::GetUnscaledTextureID() const +{ + return m_UnscaledTextureUploaded ? m_UnscaledTexID : m_TexID; +} +int Texture2D::GetDataWidth() const +{ + return m_TexData.width; +} +int Texture2D::GetDataHeight() const +{ + return m_TexData.height; +} + + +void Texture2D::InitTextureRepresentation( TextureRepresentation* rep, int format, const char* tag ) +{ + Assert(rep); + + rep->width = GetNextAllowedTextureSize(m_TexData.width, m_MipMap, TextureFormat(format)); + rep->height = GetNextAllowedTextureSize(m_TexData.height, m_MipMap, TextureFormat(format)); + rep->format = format; + + if( m_MipMap ) + rep->imageSize = CalculateImageMipMapSize( rep->width, rep->height, rep->format ); + else + rep->imageSize = CalculateImageSize( rep->width, rep->height, rep->format ); + + rep->data = AllocateTextureData(rep->imageSize * m_ImageCount, rep->format); +} + +void Texture2D::ExtractMipLevel( TextureRepresentation* dst, int frame, int mipLevel, bool checkCompression, bool scaleToSize ) +{ + if (dst->width == 0 || dst->height == 0) + return; + + UInt8* data = dst->data + frame * dst->imageSize + CalculateMipMapOffset(dst->width, dst->height, dst->format, mipLevel ); + + + int width = std::max( dst->width >> mipLevel, 1 ); + int height = std::max( dst->height >> mipLevel, 1 ); + + if( checkCompression && IsAnyCompressedTextureFormat(dst->format) ) + { + ExtractCompressedImageInternal( data, width, height, frame ); + } + else + { + ImageReference ref( width, height, width*GetBytesFromTextureFormat(dst->format), dst->format, data ); + ExtractImageInternal( &ref, scaleToSize, frame ); + } +} + + +void Texture2D::InitTextureRepresentations(Texture2D::TextureRepresentation* scaled, Texture2D::TextureRepresentation* padded) +{ + Assert(scaled); + Assert(padded); + + if( m_TextureDimension == kTexDimDeprecated1D ) + m_TextureDimension = kTexDim2D; + + int multipleMask = GetTextureSizeAllowedMultiple(TextureFormat(m_TexData.format)) - 1; + bool isAllowedMultiple = (m_TexData.width & multipleMask) == 0 && (m_TexData.height & multipleMask) == 0; + if (isAllowedMultiple && (m_PowerOfTwo || IsNPOTTextureAllowed(m_MipMap))) + { + *scaled = *padded = m_TexData; + SetTexelSize( 1.0f / m_TexData.width, 1.0f / m_TexData.height ); + return; + } + + DebugAssertIf( m_PowerOfTwo ); + DebugAssertIf( !m_TexData.data ); + + InitTextureRepresentation(scaled, IsAnyCompressedTextureFormat(m_TexData.format) ? kTexFormatRGBA32 : m_TexData.format, "tex.scaled"); + InitTextureRepresentation(padded, m_TexData.format, "tex.padded"); + + int mipcount = CountMipmaps(); + for( int frame = 0; frame < m_ImageCount; ++frame ) + { + for( int mip = 0; mip < mipcount; ++mip ) + { + ExtractMipLevel(scaled, frame, mip, false, true); + ExtractMipLevel(padded, frame, mip, true, false); + } + } +} + +int Texture2D::GetRuntimeMemorySize() const +{ +#if ENABLE_MEM_PROFILER + return Super::GetRuntimeMemorySize() + + GetMemoryProfiler()->GetRelatedIDMemorySize(m_TexID.m_ID) + + (m_UnscaledTextureUploaded?GetMemoryProfiler()->GetRelatedIDMemorySize(GetUnscaledTextureID().m_ID):0); +#endif + return sizeof(Texture2D); +} + + +IMPLEMENT_CLASS (Texture2D) +IMPLEMENT_OBJECT_SERIALIZE (Texture2D) +INSTANTIATE_TEMPLATE_TRANSFER (Texture2D) + +bool Texture2D::CheckHasPixelData () const +{ + if (m_TexData.data != NULL) + return true; + + if (!m_IsReadable) + { + ErrorString(Format("Texture '%s' is not readable, the texture memory can not be accessed from scripts. You can make the texture readable in the Texture Import Settings.", GetName())); + } + else + { + ErrorString(Format("Texture '%s' has no data", GetName())); + } + return false; +} + + +void Texture2D::SetPixel (int frame, int x, int y, const ColorRGBAf& c) +{ + if (!CheckHasPixelData()) + return; + + if (frame > m_ImageCount) { + ErrorString (Format ("SetPixel called on an undefined image (valid values are 0 - %d", m_ImageCount - 1)); + return; + } + + ImageReference image; + if (GetWriteImageReference(&image, frame, 0)) + { + SetImagePixel (image, x, y, static_cast<TextureWrapMode>(GetSettings().m_WrapMode), c); + } + else + { + if( IsAnyCompressedTextureFormat(m_TexData.format) ) + { + ErrorString(kUnsupportedSetPixelOpFormatMessage); + } + else + { + ErrorString("Unable to retrieve image reference"); + } + } +} + +void Texture2D::SetPixels( int x, int y, int width, int height, int pixelCount, const ColorRGBAf* pixels, int miplevel, int frame ) +{ + if (width == 0 || height == 0) + return; // nothing to do + + if (!CheckHasPixelData()) + return; + + int mipcount = CountMipmaps(); + if (miplevel < 0 || miplevel >= mipcount) + { + ErrorString ("Invalid mip level"); + return; + } + + UInt8* data = m_TexData.data + frame * m_TexData.imageSize; + data += CalculateMipMapOffset (m_TexData.width, m_TexData.height, m_TexData.format, miplevel); + int dataWidth = std::max (m_TexData.width >> miplevel, 1); + int dataHeight = std::max (m_TexData.height >> miplevel, 1); + + SetImagePixelBlock (data, dataWidth, dataHeight, m_TexData.format, x, y, width, height, pixelCount, pixels); +} + + + +ColorRGBAf Texture2D::GetPixel (int frame,int x, int y) const +{ + if (!CheckHasPixelData()) + return ColorRGBAf(1.0F,1.0F,1.0F,1.0F); + + if (frame > m_ImageCount) { + ErrorString (Format ("GetPixel called on an undefined image (valid values are 0 - %d", m_ImageCount - 1)); + return ColorRGBAf(1.0F,1.0F,1.0F,1.0F); + } + + return GetImagePixel (m_TexData.data + frame*m_TexData.imageSize, m_TexData.width, m_TexData.height, m_TexData.format, static_cast<TextureWrapMode>(GetSettings().m_WrapMode), x, y); +} + +ColorRGBAf Texture2D::GetPixelBilinear (int frame, float u, float v) const +{ + if (!CheckHasPixelData()) + return ColorRGBAf(1.0F,1.0F,1.0F,1.0F); + if (frame > m_ImageCount) { + ErrorString (Format ("GetPixelBilinear called on an undefined image (valid values are 0 - %d", m_ImageCount - 1)); + return ColorRGBAf(1.0F,1.0F,1.0F,1.0F); + } + return GetImagePixelBilinear (m_TexData.data + frame*m_TexData.imageSize, m_TexData.width, m_TexData.height, m_TexData.format, static_cast<TextureWrapMode>(GetSettings().m_WrapMode), u, v); +} + +bool Texture2D::GetPixels( int x, int y, int width, int height, int miplevel, ColorRGBAf* colors, int frame ) const +{ + if (width == 0 || height == 0) + return true; // nothing to do + + if (!CheckHasPixelData()) + return false; + + int mipcount = CountMipmaps(); + if (miplevel < 0 || miplevel >= mipcount) + { + ErrorString ("Invalid mip level"); + return false; + } + + UInt8* data = m_TexData.data + frame * m_TexData.imageSize; + data += CalculateMipMapOffset (m_TexData.width, m_TexData.height, m_TexData.format, miplevel); + int dataWidth = std::max (m_TexData.width >> miplevel, 1); + int dataHeight = std::max (m_TexData.height >> miplevel, 1); + + return GetImagePixelBlock (data, dataWidth, dataHeight, m_TexData.format, x, y, width, height, colors); +} + +bool Texture2D::GetPixels32( int miplevel, ColorRGBA32* colors ) const +{ + AssertIf( miplevel < 0 || miplevel >= CountDataMipmaps() ); + + ImageReference texImage; + if( !GetImageReferenceInternal(&texImage, 0, miplevel) ) + { + if (m_TexData.data != NULL && IsAnyCompressedTextureFormat(m_TexData.format)) + { + UInt8* data = m_TexData.data + CalculateMipMapOffset( m_TexData.width, m_TexData.height, m_TexData.format, miplevel ); + const int minSize = GetMinimumTextureMipSizeForFormat(m_TexData.format); + // the decompress size is at least minSize x minSize + int width = std::max (m_TexData.width >> miplevel, minSize); + int height = std::max (m_TexData.height >> miplevel, minSize); + // also, it should be a multiple of minSize + if (width % minSize == 0 && height % minSize == 0) + { + DecompressNativeTextureFormatWithMipLevel( m_TexData.format, width, height,miplevel, reinterpret_cast<const UInt32*>(data), width, height, reinterpret_cast<UInt32*>(colors) ); + return true; + } + else + { + // make it an upper multiple of minSize + int decompWidth = (width + minSize - 1) / minSize * minSize; + int decompHeight = (height + minSize - 1) / minSize * minSize; + Image decompressed( decompWidth, decompHeight, kTexFormatRGBA32 ); + DecompressNativeTextureFormatWithMipLevel( m_TexData.format, width, height,miplevel, reinterpret_cast<const UInt32*>(data), decompWidth, decompHeight, (UInt32*)decompressed.GetImageData() ); + // clip it back to the original size + ImageReference clipped = decompressed.ClipImage( 0, 0, width, height ); + // blit it over to the colors array + ImageReference resultImage( width, height, GetRowBytesFromWidthAndFormat(width,kTexFormatRGBA32), kTexFormatRGBA32, colors ); + resultImage.BlitImage( clipped, ImageReference::BLIT_COPY ); + return true; + } + } + AssertString( "Invalid texture" ); + return false; + } + + int texWidth = texImage.GetWidth(); + int texHeight = texImage.GetHeight(); + + ImageReference resultImage( texWidth, texHeight, GetRowBytesFromWidthAndFormat(texWidth,kTexFormatRGBA32), kTexFormatRGBA32, colors ); + resultImage.BlitImage( texImage, ImageReference::BLIT_COPY ); + + return true; +} + + +void Texture2D::SetPixels32( int miplevel, const ColorRGBA32* pixels, const int pixelCount ) +{ + AssertIf( miplevel < 0 || miplevel >= CountMipmaps() ); + + ImageReference texImage; + if( !GetWriteImageReference(&texImage, 0, miplevel) ) + { + AssertString( "Invalid texture format" ); + return; + } + + int texWidth = texImage.GetWidth(); + int texHeight = texImage.GetHeight(); + + if(texWidth * texHeight != pixelCount) + { + AssertString( "SetPixels32 called with invalid number if pixels in the array" ); + return; + } + + ImageReference inputImage( texWidth, texHeight, GetRowBytesFromWidthAndFormat(texWidth,kTexFormatRGBA32), kTexFormatRGBA32, (void*)pixels ); + texImage.BlitImage( inputImage, ImageReference::BLIT_COPY ); +} + +static const char* kUnsupportedColorPixelFormatMessage = "Unsupported image format - the texture needs to be ARGB32 or RGB24"; + +static bool IsAllowedToReadPixels () +{ +#if UNITY_EDITOR + // from Player.h + bool IsInsidePlayerLoop (); + // Editor needs to read frame buffer outside player loop + if( !IsInsidePlayerLoop() ) + return true; +#elif WEBPLUG + // Allow old web content for compatibility + if( !IS_CONTENT_NEWER_OR_SAME(kUnityVersion4_0_a1) ) + return true; +#endif + return GetGfxDevice().IsInsideFrame() || RenderTexture::GetActive(); +} + +// TODO: Check dimensions, make everything work. +void Texture2D::ReadPixels (int frame, int left, int bottom, int width, int height, int destX, int destY, bool flipped, bool computeMipMap) +{ + //Prevent out of bounds reads (case 562067) + if (destX < 0 || destY < 0 || destX >= GetDataWidth() || destY >= GetDataHeight()) + { + ErrorString("Trying to read pixels out of bounds"); + return; + } + if (width < 0 || height < 0) + { + ErrorString("Negative read pixels rectangle width|height"); + return; + } + + if( !IsAllowedToReadPixels() ) + { + ErrorString ("ReadPixels was called to read pixels from system frame buffer, while not inside drawing frame."); + // Our tests rely on calling ReadImage() during Update() so we allow it anyway... + } + + if( frame >= m_ImageCount ) + { + ErrorString (Format ("ReadPixels called on undefined image %d (valid values are 0 - %d", frame, m_ImageCount - 1)); + return; + } + + GfxDeviceRenderer renderer = GetGfxDevice().GetRenderer(); + bool isGLES = (renderer == kGfxRendererOpenGLES20Mobile || renderer == kGfxRendererOpenGLES30); + int texFormat = GetTextureFormat(); + bool validFormat = texFormat == kTexFormatARGB32 || + texFormat == kTexFormatRGB24 || + texFormat == (kTexFormatRGBA32 && isGLES) || + texFormat == (kTexFormatRGB565 && isGLES); + if( !validFormat ) + { + ErrorString(kUnsupportedColorPixelFormatMessage); + return; + } + + ImageReference image; + if( !GetWriteImageReference(&image, frame, 0) ) + { + ErrorString("Unable to retrieve image reference"); + return; + } + + if (RenderTexture::GetActive() == NULL) + { + Rectf rc = GetRenderManager().GetWindowRect(); + left += rc.x; + bottom += rc.y; + } + + if (left < 0) { + width += left; + left = 0; + } + if (bottom < 0) { + height += bottom; + bottom = 0; + } + if (destX + width > GetDataWidth()) + width = GetDataWidth() - destX; + if (destY + height > GetDataHeight()) + height = GetDataHeight() - destY; + + GetGfxDevice().ReadbackImage( image, left, bottom, width, height, destX, destY ); + + if (flipped) + { + ImageReference subImage = image.ClipImage (destX, destY, width, height); + subImage.FlipImageY(); + } + + if( computeMipMap && m_MipMap ) + RebuildMipMap(); +} + + +#if ENABLE_PNG_JPG +bool Texture2D::EncodeToPNG( dynamic_array<UInt8>& outBuffer ) +{ + if( IsAnyCompressedTextureFormat(GetTextureFormat()) ) + { + ErrorString(kUnsupportedSetPixelOpFormatMessage); + return false; + } + ImageReference image; + if( !GetWriteImageReference( &image, 0, 0 ) ) + { + ErrorString( "Unable to retrieve image reference" ); + return false; + } + if( !ConvertImageToPNGBuffer( image, outBuffer ) ) + { + ErrorString( "Failed to encode to PNG" ); + return false; + } + + return true; +} +#endif + + + +bool Texture2D::ResizeWithFormat (int width, int height, TextureFormat format, int flags) +{ + if (!m_IsReadable) + { + ErrorString ("Texture is not readable."); + return false; + } + + if( IsAnyCompressedTextureFormat(format) ) + { + ErrorStringObject ("Can't resize to a compressed texture format", this); + return false; + } + + return InitTexture(width, height, format, flags); +} + + +void Texture2D::Compress (bool dither) +{ + if (!m_IsReadable) + { + ErrorString(Format("Texture '%s' is not readable, Compress will not work. You can make the texture readable in the Texture Import Settings.", GetName())); + return; + } + + SET_ALLOC_OWNER(this); + // If hardware does not support DXT, then nothing to do. + if( !gGraphicsCaps.hasS3TCCompression ) + return; + + #if !GFX_SUPPORTS_DXT_COMPRESSION + Assert(!"GraphicsCaps.hasS3TCCompression is invalid"); + return; + #else + + TextureFormat format = GetTextureFormat(); + // If already compressed, then nothing to do. + if( IsAnyCompressedTextureFormat(format) ) + return; + + // Copy out old data into RGBA32 format. + bool mipMaps = HasMipMap(); + int width = GetDataWidth(); + int height = GetDataHeight(); + int rgbaByteSize = mipMaps ? + CalculateImageMipMapSize( width, height, kTexFormatRGBA32 ) : + CalculateImageSize( width, height, kTexFormatRGBA32 ); + UInt8* rgbaData = new UInt8[rgbaByteSize]; + int mipCount = CountDataMipmaps(); + for( int mip = 0; mip < mipCount; ++mip ) + { + UInt8* rgbaMipData = rgbaData + CalculateMipMapOffset(width, height, kTexFormatRGBA32, mip); + int mipWidth = std::max( width >> mip, 1 ); + int mipHeight = std::max( height >> mip, 1 ); + ImageReference mipDst( mipWidth, mipHeight, mipWidth * 4, kTexFormatRGBA32, rgbaMipData ); + ExtractImageInternal( &mipDst, false, 0 ); + } + + // Reformat texture into DXT format + bool hasAlpha = HasAlphaTextureFormat(format); + TextureFormat compressedFormat = hasAlpha ? kTexFormatDXT5 : kTexFormatDXT1; + InitTexture( width, height, compressedFormat, mipMaps ? kMipmapMask : kNoMipmap ); + + // DXT compress RGBA data + for( int mip = 0; mip < mipCount; ++mip ) + { + const UInt8* srcMipData = rgbaData + CalculateMipMapOffset(width, height, kTexFormatRGBA32, mip); + UInt8* dstMipData = GetRawImageData() + CalculateMipMapOffset(width, height, compressedFormat, mip); + int mipWidth = std::max( width >> mip, 1 ); + int mipHeight = std::max( height >> mip, 1 ); + FastCompressImage( mipWidth, mipHeight, srcMipData, dstMipData, hasAlpha, dither ); + } + + delete[] rgbaData; + + UpdateImageDataDontTouchMipmap(); + #endif //GFX_SUPPORTS_DXT_COMPRESSION +} + +#if UNITY_EDITOR +void Texture2D::WarnInstantiateDisallowed () +{ + if (!m_IsReadable) + { + ErrorStringObject(Format("Instantiating a non-readable '%s' texture is not allowed! Please mark the texture readable in the inspector or don't instantiate it.", GetName()), this); + } +} + +bool Texture2D::IgnoreMasterTextureLimit () const +{ + return m_IgnoreMasterTextureLimit; +} + +void Texture2D::SetIgnoreMasterTextureLimit (bool ignore) +{ + m_IgnoreMasterTextureLimit = ignore; +} + +bool Texture2D::GetAlphaIsTransparency() const +{ + return m_AlphaIsTransparency; +} + +void Texture2D::SetAlphaIsTransparency(bool is) +{ + m_AlphaIsTransparency = is; +} +#endif // UNITY_EDITOR + + +void Texture2D::Apply(bool updateMipmaps, bool makeNoLongerReadable) +{ + if( makeNoLongerReadable ) + { + SetIsReadable(false); + SetIsUnreloadable(true); + } + + if( IsAnyCompressedTextureFormat(GetTextureFormat()) ) + updateMipmaps = false; + + if (updateMipmaps) UpdateImageData(); + else UpdateImageDataDontTouchMipmap(); +} + + diff --git a/Runtime/Graphics/Texture2D.h b/Runtime/Graphics/Texture2D.h new file mode 100644 index 0000000..e64a035 --- /dev/null +++ b/Runtime/Graphics/Texture2D.h @@ -0,0 +1,208 @@ +#pragma once + +#include "Texture.h" +#include "Configuration/UnityConfigure.h" +#include "Runtime/Modules/ExportModules.h" + +class ColorRGBAf; +class ColorRGBA32; + + +class EXPORT_COREMODULE Texture2D: public Texture +{ +public: + // Should be called by MAIN thread in order to upload texture data immediately after texture asset was loaded + // On some platforms helps to avoid memory peak during level load + static void IntegrateLoadedImmediately(); + +protected: + + // See comments at Texture2D.cpp + struct TextureRepresentation { + TextureRepresentation(); + + UInt8* data; // data for all image frames + int width; + int height; + int format; + int imageSize; // size in bytes of one image frames, including mip levels + }; + TextureRepresentation m_TexData; // original data + TextureID m_UnscaledTexID; + + static bool s_ScreenReadAllowed; + +protected: + int m_ImageCount; + int m_TextureDimension; + + int m_glWidth; + int m_glHeight; + + int m_InitFlags; + bool m_MipMap; + bool m_PowerOfTwo; + bool m_TextureUploaded; + bool m_UnscaledTextureUploaded; + bool m_IsReadable; + bool m_ReadAllowed; + bool m_IsUnreloadable; + + #if UNITY_EDITOR + bool m_EditorDontWriteTextureData; + bool m_IgnoreMasterTextureLimit; + bool m_AlphaIsTransparency; + #endif + +protected: + void DestroyTexture (); + void DeleteGfxTexture (); + + virtual void UploadTexture (bool dontUseSubImage); + virtual void UnloadFromGfxDevice(bool forceUnloadAll); + virtual void UploadToGfxDevice(); + + +private: + + void DestroyTextureRepresentation( TextureRepresentation* rep ); + void DestroyTextureRepresentations( TextureRepresentation* scaled, TextureRepresentation* padded, bool freeSourceImage=true ); + + void InitTextureRepresentation( TextureRepresentation* rep, int format, const char* tag ); + void InitTextureRepresentations( TextureRepresentation* scaled, TextureRepresentation* padded ); + + void UpdatePOTStatus(); + + + void ExtractMipLevel( TextureRepresentation* dst, int frame, int mipLevel, bool checkCompression, bool scaleToSize ); + bool ExtractImageInternal( ImageReference* image, bool scaleToSize, int imageIndex ) const; + void ExtractCompressedImageInternal( UInt8* dst, int dstWidth, int dstHeight, int imageIndex ) const; + + bool GetImageReferenceInternal (ImageReference* image, int frame, int miplevel) const; + + bool CheckHasPixelData () const; + + MemLabelId GetTextureDataMemoryLabel() const { return ( GetMemoryLabel().label == kMemTextureCacheId ? GetMemoryLabel() : MemLabelId(kMemTextureId, GetMemoryLabel().GetRootHeader()) ); } +public: + REGISTER_DERIVED_CLASS (Texture2D, Texture) + DECLARE_OBJECT_SERIALIZE (Texture2D) + + Texture2D (MemLabelId label, ObjectCreationMode mode); + // ~Texture2D (); declared-by-macro + + virtual bool MainThreadCleanup (); + + virtual void Reset (); + virtual void AwakeFromLoadThreaded (); + virtual void AwakeFromLoad (AwakeFromLoadMode awakeMode); + + virtual TextureDimension GetDimension () const { return static_cast<TextureDimension>(m_TextureDimension); } + + virtual int GetGLWidth() const { return m_glWidth; } + virtual int GetGLHeight() const { return m_glHeight; } + virtual void ApplySettings(); + + virtual int GetDataWidth() const; + virtual int GetDataHeight() const; + + virtual int GetRuntimeMemorySize() const; + #if ENABLE_PROFILER || UNITY_EDITOR + virtual int GetStorageMemorySize() const { return m_TexData.imageSize*m_ImageCount; } + #endif + + virtual TextureID GetUnscaledTextureID() const; + int CountDataMipmaps () const; + bool IsNonPowerOfTwo() const { return !m_PowerOfTwo; } + + enum { + kNoMipmap = 0, + kMipmapMask = 1 << 0, + kThreadedInitialize = 1 << 2, + kOSDrawingCompatible = 1 << 3, + }; + virtual bool InitTexture (int width, int height, TextureFormat format, int flags=kMipmapMask, int imageCount=1, intptr_t nativeTex=0); + + void InitTextureInternal (int width, int height, TextureFormat format, int imageSize, UInt8* buffer, int options, int imageCount); + UInt8* AllocateTextureData (int imageSize, TextureFormat format, bool initMemory = false); + void DeallocateTextureData (UInt8* memory); + + virtual bool HasMipMap () const; + + void SetIsReadable (bool readable) { m_IsReadable = readable; } + bool GetIsReadable () const { return m_IsReadable; } + void SetIsUnreloadable (bool value) { m_IsUnreloadable = value; } + bool GetIsUploaded () const { return m_TextureUploaded; } + + #if UNITY_EDITOR + void SetEditorDontWriteTextureData (bool value) { m_EditorDontWriteTextureData = value; } + // directly load from an image, used in editor for gizmos/icons + void SetImage (const ImageReference& image, int flags = kMipmapMask); + virtual void WarnInstantiateDisallowed (); + virtual bool IgnoreMasterTextureLimit () const; + void SetIgnoreMasterTextureLimit (bool ignore); + + bool GetAlphaIsTransparency() const; + void SetAlphaIsTransparency(bool is); + + virtual TextureFormat GetEditorUITextureFormat () const { return GetTextureFormat(); } + + #endif + + virtual void UpdateImageData (); + virtual void UpdateImageDataDontTouchMipmap (); + + // Returns the original (may be NPOT) data + UInt8 *GetRawImageData (int frame = 0) { return m_TexData.data + frame * m_TexData.imageSize; } + int GetRawImageDataSize () const { return m_TexData.imageSize; } + + bool GetWriteImageReference (ImageReference* image, int frame, int miplevel); + + int GetImageCount () const { return m_ImageCount; } + + virtual bool ExtractImage (ImageReference* image, int imageIndex = 0) const; + + bool ResizeWithFormat (int width, int height, TextureFormat format, int flags); + bool Resize (int width, int height) { return ResizeWithFormat (width, height, GetTextureFormat(), HasMipMap() ? kMipmapMask : kNoMipmap); } + + int GetTextureFormat () const { return m_TexData.format; } + + void Compress (bool dither); + + virtual void RebuildMipMap (); + + virtual int CountMipmaps () const; + + ColorRGBAf GetPixelBilinear (int image, float u, float v) const; + ColorRGBAf GetPixel (int image, int x, int y) const; + void SetPixel (int image, int x, int y, const ColorRGBAf& c); + + // Read pixels. Set reversed when reading into a cubemap + void ReadPixels (int frame, int left, int bottom, int width, int height, int destX, int destY, bool reversed, bool computeMipMap); + + bool GetPixels (int x, int y, int width, int height, int mipLevel, ColorRGBAf* data, int frame = 0) const; + void SetPixels( int x, int y, int width, int height, int pixelCount, const ColorRGBAf* pixels, int miplevel, int frame = 0 ); + + + // always whole mip level, into/from 32 bit RGBA colors + // GetPixels32 also supports getting pixels from DXT textures. + // For DXT textures the output width/height must have minimum of 4. + bool GetPixels32( int mipLevel, ColorRGBA32* data ) const; + void SetPixels32( int mipLevel, const ColorRGBA32* pixels, const int pixelCount ); + + // Encodes to PNG bytes + bool EncodeToPNG( dynamic_array<UInt8>& outBuffer ); + + // Is reading the data from this texture allowed by webplayer security + void SetReadAllowed (bool allowed) { m_ReadAllowed = allowed; if (!allowed) s_ScreenReadAllowed=false;} + bool GetReadAllowed () const { return m_ReadAllowed; } + + void Apply(bool updateMipmaps, bool makeNoLongerReadable); + + static bool GetScreenReadAllowed () { return s_ScreenReadAllowed; } + + + friend struct TemporaryTextureSerializationRevert; +}; + +void ConvertTextureEndianessWrite (int format, UInt8* src, UInt8* dst, int size, bool bBigEndianGPU); +void ConvertTextureEndianessRead (int format, UInt8* src, int size); diff --git a/Runtime/Graphics/Texture3D.cpp b/Runtime/Graphics/Texture3D.cpp new file mode 100644 index 0000000..0c2f6c3 --- /dev/null +++ b/Runtime/Graphics/Texture3D.cpp @@ -0,0 +1,322 @@ +#include "UnityPrefix.h" +#include "Texture3D.h" +#include "Image.h" +#include "Runtime/Utilities/BitUtility.h" +#include "Runtime/GfxDevice/GfxDevice.h" +#include "Runtime/Misc/BuildSettings.h" +#include "Runtime/Shaders/GraphicsCaps.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" + + +static UInt32 CalculateMipOffset3D (int mipCount, int width, int height, int depth, TextureFormat format) +{ + UInt32 imageSize = 0; + const int bpp = GetBytesFromTextureFormat(format); + for (int mip = 0; mip < mipCount; ++mip) + { + int mipWidth = std::max(width >> mip,1); + int mipHeight = std::max(height >> mip,1); + int mipDepth = std::max(depth >> mip,1); + imageSize += bpp * mipWidth * mipHeight * mipDepth; + } + return imageSize; +} + + +Texture3D::Texture3D (MemLabelId label, ObjectCreationMode mode) +: Super(label, mode) +, m_Width(0) +, m_Height(0) +, m_Depth(0) +, m_Format(kTexFormatARGB32) +, m_Data(NULL) +, m_DataSize(0) +, m_MipMap(false) +, m_TextureUploaded(false) +{ +} + +Texture3D::~Texture3D () +{ + DestroyTexture(); +} + +bool Texture3D::MainThreadCleanup () +{ + DeleteGfxTexture(); + return Super::MainThreadCleanup(); +} + + + +void Texture3D::AwakeFromLoad (AwakeFromLoadMode awakeMode) +{ + Super::AwakeFromLoad (awakeMode); + + if ((awakeMode == kDefaultAwakeFromLoad || awakeMode == kInstantiateOrCreateFromCodeAwakeFromLoad) && m_Data == NULL) + return; + + UploadTexture (false); +} + + +void Texture3D::Reset () +{ + Super::Reset(); + m_TextureSettings.Reset(); +} + + +bool Texture3D::InitTexture (int width, int height, int depth, TextureFormat format, bool mipMaps) +{ + if (!IsPowerOfTwo(width) || !IsPowerOfTwo(height) || !IsPowerOfTwo(depth)) + { + ErrorStringObject ("Texture3D has non-power of two size", this); + return false; + } + if (format >= kTexFormatDXT1 || !IsValidTextureFormat(format)) + { + ErrorStringObject ("Invalid texture format for Texture3D", this); + return false; + } + + const int kMax3DTextureSize = 1024; + if (width < 0 || width > kMax3DTextureSize || height < 0 || height > kMax3DTextureSize || depth < 0 || depth > kMax3DTextureSize) + { + ErrorStringObject ("Texture3D has out of range width / height / depth", this); + return false; + } + + m_Width = width; + m_Height = height; + m_Depth = depth; + m_Format = format; + m_MipMap = mipMaps; + + const int mipCount = CountMipmaps(); + UInt32 imageSize = CalculateMipOffset3D (mipCount, width, height, depth, format); + + UInt8* buffer = AllocateTextureData(imageSize, m_Format, true); + if (!buffer) + return false; + + // Cleanup any old memory + DestroyTexture (); + + m_Data = buffer; + m_DataSize = imageSize; + + SetTexelSize (1.0f/m_Width, 1.0f/m_Height); + + SetDirty (); + return true; +} + +UInt8* Texture3D::AllocateTextureData (int imageSize, TextureFormat format, bool initMemory) +{ + // Allocate one more pixel because software bi-linear filtering might require it. + int allocSize = imageSize + GetBytesFromTextureFormat(format); + void* buffer = UNITY_MALLOC_ALIGNED (kMemTexture, allocSize, 32); + + // In the past the memory manager cleared textures created from script to 0xcd. + // Let's keep doing that even though a color like white makes more sense (case 564961). + if (initMemory && buffer) + memset(buffer, 0xcd, allocSize); + + return static_cast<UInt8*>(buffer); +} + +void Texture3D::DeleteGfxTexture() +{ + if (m_TextureUploaded) + { + ASSERT_RUNNING_ON_MAIN_THREAD + GetGfxDevice().DeleteTexture(GetTextureID()); + m_TextureUploaded = false; + } +} + + +void Texture3D::DestroyTexture() +{ + UNITY_FREE (kMemTexture, m_Data); + m_Data = NULL; + m_DataSize = 0; + + DeleteGfxTexture(); +} + +void Texture3D::UploadTexture (bool dontUseSubImage) +{ + if (!gGraphicsCaps.has3DTexture) + return; + + const BuildSettings* buildSettings = GetBuildSettingsPtr(); + if (buildSettings && !buildSettings->hasPROVersion) + return; + + Assert (GetImageDataPointer() != NULL); + + UInt32 uploadFlags = (dontUseSubImage || !m_TextureUploaded) ? GfxDevice::kUploadTextureDontUseSubImage : GfxDevice::kUploadTextureDefault; + GetGfxDevice().UploadTexture3D (GetTextureID(), GetImageDataPointer(), GetImageDataSize(), m_Width, m_Height, m_Depth, m_Format, CountMipmaps(), uploadFlags); + Texture::s_TextureIDMap.insert (std::make_pair(GetTextureID(),this)); + ApplySettings(); +#if UNITY_XENON && !MASTER_BUILD + GetGfxDevice().SetTextureName( GetTextureID(), GetName() ); +#endif + m_TextureUploaded = true; +} + +void Texture3D::UpdateImageData (bool rebuildMipMaps) +{ + if (rebuildMipMaps) + RebuildMipMap (); + UploadTexture (false); + SetDirty (); +} + + +void Texture3D::RebuildMipMap () +{ + if (!m_MipMap || m_Data == NULL) + return; + if (IsAnyCompressedTextureFormat(m_Format)) + { + ErrorStringObject ("Rebuilding mipmaps of compressed textures is not supported", this); + return; + } + + CreateMipMap (m_Data, m_Width, m_Height, m_Depth, m_Format); +} + + +bool Texture3D::ExtractImage (ImageReference* image, int imageIndex) const +{ + if (!m_Data) + return false; + + ImageReference source (m_Width, m_Height, m_Width * GetBytesFromTextureFormat(m_Format), m_Format, m_Data); + image->BlitImage (source, ImageReference::BLIT_BILINEAR_SCALE); + return true; +} + + + +int Texture3D::CountMipmaps () const +{ + if (m_MipMap) + return CalculateMipMapCount3D (m_Width, m_Height, m_Depth); + else + return 1; +} + + +void Texture3D::UnloadFromGfxDevice(bool forceUnloadAll) +{ + DeleteGfxTexture (); +} + +void Texture3D::UploadToGfxDevice() +{ + //@TODO: once texture3D gets the option to unload system memory copy, + // then we need to do needReloadFromDisk dance similar to Texture2D. + + UploadTexture (true); +} + + +void Texture3D::SetPixels (int pixelCount, const ColorRGBAf* pixels, int miplevel) +{ + if (pixelCount == 0 || pixels == NULL) + return; + + if (!m_Data) + { + ErrorStringObject("Texture has no data", this); + return; + } + + int mipcount = CountMipmaps(); + if (miplevel < 0 || miplevel >= mipcount) + { + ErrorStringObject ("Invalid mip level", this); + return; + } + + + UInt8* data = m_Data + CalculateMipOffset3D (miplevel, m_Width, m_Height, m_Depth, m_Format); + const int mipWidth = std::max(m_Width >> miplevel,1); + const int mipHeight = std::max(m_Height >> miplevel,1); + const int mipDepth = std::max(m_Depth >> miplevel,1); + + SetImagePixelBlock (data, mipWidth, mipHeight*mipDepth, m_Format, 0, 0, mipWidth, mipHeight*mipDepth, pixelCount, pixels); +} + + +bool Texture3D::GetPixels (ColorRGBAf* dest, int miplevel) const +{ + if (!dest) + return true; // nothing to do + + if (!m_Data) + { + ErrorStringObject("Texture has no data", this); + return false; + } + + int mipcount = CountMipmaps(); + if (miplevel < 0 || miplevel >= mipcount) + { + ErrorStringObject ("Invalid mip level", this); + return false; + } + + UInt8* data = m_Data + CalculateMipOffset3D (miplevel, m_Width, m_Height, m_Depth, m_Format); + const int mipWidth = std::max(m_Width >> miplevel,1); + const int mipHeight = std::max(m_Height >> miplevel,1); + const int mipDepth = std::max(m_Depth >> miplevel,1); + + return GetImagePixelBlock (data, mipWidth, mipHeight*mipDepth, m_Format, 0, 0, mipWidth, mipHeight*mipDepth, dest); +} + + + +template<class TransferFunction> +void Texture3D::Transfer (TransferFunction& transfer) +{ + Super::Transfer (transfer); + + transfer.Transfer (m_Width, "m_Width", kNotEditableMask); + transfer.Transfer (m_Height, "m_Height", kNotEditableMask); + transfer.Transfer (m_Depth, "m_Depth", kNotEditableMask); + transfer.Transfer (m_Format, "m_Format", kNotEditableMask); + transfer.Transfer (m_MipMap, "m_MipMap", kNotEditableMask); + transfer.Align(); + transfer.Transfer (m_DataSize, "m_DataSize", kNotEditableMask); + transfer.Transfer (m_TextureSettings, "m_TextureSettings"); + + unsigned dataSize = m_DataSize; + transfer.TransferTypeless (&dataSize, "image data", kHideInEditorMask); + + if (transfer.IsReading ()) + { + DestroyTexture (); + Assert(GetMemoryLabel().label != kMemTextureCacheId); + + m_DataSize = dataSize; + m_Data = AllocateTextureData(dataSize, m_Format, false); + + SetTexelSize (1.0f/m_Width, 1.0f/m_Height); + } + + // texture data + //@TODO: format / endianess conversions + transfer.TransferTypelessData (dataSize, m_Data); + + Assert (m_DataSize == 0 || m_Data != NULL); +} + + + +IMPLEMENT_CLASS (Texture3D) +IMPLEMENT_OBJECT_SERIALIZE (Texture3D) diff --git a/Runtime/Graphics/Texture3D.h b/Runtime/Graphics/Texture3D.h new file mode 100644 index 0000000..8618e9b --- /dev/null +++ b/Runtime/Graphics/Texture3D.h @@ -0,0 +1,67 @@ +#pragma once + +#include "Texture.h" + + +class Texture3D : public Texture +{ +public: + REGISTER_DERIVED_CLASS (Texture3D, Texture) + DECLARE_OBJECT_SERIALIZE (Texture3D) + + Texture3D (MemLabelId label, ObjectCreationMode mode); + + virtual bool MainThreadCleanup (); + + + virtual void Reset (); + virtual void AwakeFromLoad (AwakeFromLoadMode awakeMode); + + void UploadTexture (bool dontUseSubImage); + bool InitTexture (int width, int height, int depth, TextureFormat format, bool mipMaps); + void UpdateImageData (bool rebuildMipMaps); + + UInt8* GetImageDataPointer() { return m_Data; } + UInt32 GetImageDataSize() const { return m_DataSize; } + int GetDepth() const { return m_Depth; } + TextureFormat GetTextureFormat() const { return m_Format; } + + bool GetPixels (ColorRGBAf* dest, int miplevel) const; + void SetPixels (int pixelCount, const ColorRGBAf* pixels, int miplevel); + + // Texture + virtual TextureDimension GetDimension () const { return kTexDim3D; } + virtual bool ExtractImage (ImageReference* image, int imageIndex = 0) const; + virtual int GetDataWidth() const { return m_Width; } + virtual int GetDataHeight() const { return m_Height; } + virtual bool HasMipMap () const { return m_MipMap; } + virtual int CountMipmaps() const; + #if ENABLE_PROFILER + virtual int GetStorageMemorySize() const { return m_DataSize; } + #endif + #if UNITY_EDITOR + virtual TextureFormat GetEditorUITextureFormat () const { return GetTextureFormat(); } + #endif + virtual int GetRuntimeMemorySize() const { return m_DataSize; } + +protected: + // Texture + virtual void UnloadFromGfxDevice(bool forceUnloadAll); + virtual void UploadToGfxDevice(); + +private: + UInt8* AllocateTextureData(int imageSize, TextureFormat format, bool initMemory); + void DestroyTexture(); + void RebuildMipMap (); + void DeleteGfxTexture(); + +private: + int m_Width; + int m_Height; + int m_Depth; + TextureFormat m_Format; + UInt8* m_Data; + UInt32 m_DataSize; + bool m_MipMap; + bool m_TextureUploaded; +}; diff --git a/Runtime/Graphics/TextureFormat.cpp b/Runtime/Graphics/TextureFormat.cpp new file mode 100644 index 0000000..27d60fa --- /dev/null +++ b/Runtime/Graphics/TextureFormat.cpp @@ -0,0 +1,248 @@ +#include "UnityPrefix.h" +#include "TextureFormat.h" +#include "Runtime/Utilities/LogAssert.h" + +// NOTE: match indices in kTexFormat* enum! + +const static int kTextureByteTable[kTexFormatTotalCount] = +{ + 0, + 1, // kTexFormatAlpha8 + 2, // kTexFormatARGB4444 + 3, // kTexFormatRGB24 + 4, // kTexFormatRGBA32 + 4, // kTexFormatARGB32 + 16, // kTexFormatARGBFloat + 2, // kTexFormatRGB565 + 3, // kTexFormatBGR24 + 2, // kTexFormatAlphaLum16 + 0, // kTexFormatDXT1 (Depends on width, height, depth) + 0, // kTexFormatDXT3 (Depends on width, height, depth) + 0, // kTexFormatDXT5 (Depends on width, height, depth) + 2, // kTexFormatRGBA4444 +}; + +UInt32 GetBytesFromTextureFormat (TextureFormat inFormat) +{ + AssertMsg (inFormat < kTexFormatDXT1 || inFormat == kTexFormatBGRA32 || inFormat == kTexFormatRGBA4444, "Invalid texture format: %d", (int)inFormat); + return (inFormat == kTexFormatBGRA32) ? 4 : kTextureByteTable[inFormat]; +} + +UInt32 GetMaxBytesPerPixel (TextureFormat inFormat) +{ + Assert (GetBytesFromTextureFormat (inFormat) <= kTextureByteTable[kTexFormatARGBFloat]); + return kTextureByteTable[kTexFormatARGBFloat]; +} + +int GetRowBytesFromWidthAndFormat (int width, TextureFormat inFormat) +{ + AssertMsg (inFormat < kTexFormatDXT1 || inFormat == kTexFormatBGRA32 || inFormat == kTexFormatRGBA4444, "Invalid texture format: %d", (int)inFormat); + return GetBytesFromTextureFormat (inFormat) * width; +} + +bool IsValidTextureFormat (TextureFormat format) +{ + if ((format >= kTexFormatAlpha8 && format <= kTexFormatRGBA4444) || + IsCompressedPVRTCTextureFormat(format) || + IsCompressedETCTextureFormat(format) || + IsCompressedATCTextureFormat(format) || + IsCompressedFlashATFTextureFormat (format) || + IsCompressedETC2TextureFormat(format) || + IsCompressedASTCTextureFormat(format) || + IsCompressedEACTextureFormat(format) || + format == kTexFormatBGRA32) + return true; + else + return false; +} + +int GetTextureSizeAllowedMultiple( TextureFormat format ) +{ + if (IsCompressedDXTTextureFormat(format) || IsCompressedATCTextureFormat(format) || IsCompressedETCTextureFormat(format) + || IsCompressedETC2TextureFormat(format) || IsCompressedEACTextureFormat(format)) + return 4; + else + return 1; +} + +int GetMinimumTextureMipSizeForFormat( TextureFormat format ) +{ + if (format == kTexFormatPVRTC_RGBA2 || format == kTexFormatPVRTC_RGB2) + return 16; + else if (format == kTexFormatPVRTC_RGBA4 || format == kTexFormatPVRTC_RGB4) + return 8; + else if (IsCompressedETCTextureFormat(format) || IsCompressedETC2TextureFormat(format) || IsCompressedEACTextureFormat(format)) + return 4; + else if (IsCompressedATCTextureFormat(format)) + return 4; + else if (IsCompressedASTCTextureFormat(format)) + return 1; + else if (IsCompressedFlashATFTextureFormat(format)) + return 1; + else if (IsCompressedDXTTextureFormat(format)) + return 4; + else + return 1; +} + +bool IsAlphaOnlyTextureFormat( TextureFormat format ) +{ + return format == kTexFormatAlpha8; +} + + +TextureFormat ConvertToAlphaTextureFormat (TextureFormat format) +{ + if (format == kTexFormatRGB24 || format == kTexFormatBGR24) + return kTexFormatARGB32; + else if (format == kTexFormatRGB565) + return kTexFormatARGB4444; + else if (format == kTexFormatDXT1) + return kTexFormatDXT5; + else if (format == kTexFormatPVRTC_RGB2) + return kTexFormatPVRTC_RGBA2; + else if (format == kTexFormatPVRTC_RGB4) + return kTexFormatPVRTC_RGBA4; + else if (format == kTexFormatETC_RGB4) + return kTexFormatARGB4444; + else if (format == kTexFormatATC_RGB4) + return kTexFormatATC_RGBA8; + else if (IsCompressedFlashATFTextureFormat(format)) + return kTexFormatFlashATF_RGBA_JPG; + else if (format == kTexFormatETC2_RGB) + return kTexFormatETC2_RGBA8; + else + return format; +} + +bool HasAlphaTextureFormat( TextureFormat format ) +{ + return format == kTexFormatAlpha8 || format == kTexFormatARGB4444 || format == kTexFormatRGBA4444 || format == kTexFormatRGBA32 || format == kTexFormatARGB32 + || format == kTexFormatARGBFloat || format == kTexFormatAlphaLum16 || format == kTexFormatDXT5 || format == kTexFormatDXT3 + || format == kTexFormatPVRTC_RGBA2 || format == kTexFormatPVRTC_RGBA4 || format == kTexFormatATC_RGBA8 || format == kTexFormatBGRA32 || format == kTexFormatFlashATF_RGBA_JPG + || format == kTexFormatETC2_RGBA1 || format == kTexFormatETC2_RGBA8 || (format >= kTexFormatASTC_RGBA_4x4 && format <= kTexFormatASTC_RGBA_12x12); +} + +bool IsDepthRTFormat( RenderTextureFormat format ) +{ + return format == kRTFormatDepth || format == kRTFormatShadowMap; +} + +bool IsHalfRTFormat( RenderTextureFormat format ) +{ + return format == kRTFormatARGBHalf || format == kRTFormatRGHalf || format == kRTFormatRHalf || IsDepthRTFormat(format); +} + +const char* GetCompressionTypeString (TextureFormat format) +{ + // Shortcut here, no sense in typing all block sizes in switchcase below + if(IsCompressedASTCTextureFormat(format)) + return "ASTC"; + + switch (format) + { + case kTexFormatDXT1: return "DXT1"; + case kTexFormatDXT3: return "DXT3"; + case kTexFormatDXT5: return "DXT5"; + case kTexFormatETC_RGB4: return "ETC1"; + case kTexFormatETC2_RGB: + case kTexFormatETC2_RGBA1: + case kTexFormatETC2_RGBA8: return "ETC2"; + case kTexFormatEAC_R: + case kTexFormatEAC_R_SIGNED: + case kTexFormatEAC_RG: + case kTexFormatEAC_RG_SIGNED: return "EAC"; + case kTexFormatPVRTC_RGB2: + case kTexFormatPVRTC_RGBA2: + case kTexFormatPVRTC_RGB4: + case kTexFormatPVRTC_RGBA4: return "PVRTC"; + case kTexFormatATC_RGB4: + case kTexFormatATC_RGBA8: return "ATC"; + case kTexFormatFlashATF_RGB_DXT1: return "DXT1"; + case kTexFormatFlashATF_RGBA_JPG: return "JPG"; + case kTexFormatFlashATF_RGB_JPG: return "JPG"; + default: + return "Uncompressed"; + } +} + +const char* GetTextureFormatString (TextureFormat format) +{ + switch (format) + { + case kTexFormatAlpha8: return "Alpha 8"; + case kTexFormatARGB4444: return "ARGB 16 bit"; + case kTexFormatRGBA4444: return "RGBA 16 bit"; + case kTexFormatRGB24: return "RGB 24 bit"; + case kTexFormatRGBA32: return "RGBA 32 bit"; + case kTexFormatARGB32: return "ARGB 32 bit"; + case kTexFormatARGBFloat: return "ARGB float"; + case kTexFormatRGB565: return "RGB 16 bit"; + case kTexFormatBGR24: return "BGR 24 bit"; + case kTexFormatAlphaLum16: return "Alpha 16 bit"; + case kTexFormatDXT1: return "RGB Compressed DXT1"; + case kTexFormatDXT3: return "RGBA Compressed DXT3"; + case kTexFormatDXT5: return "RGBA Compressed DXT5"; + + // gles + case kTexFormatPVRTC_RGB2: return "RGB Compressed PVRTC 2 bits"; + case kTexFormatPVRTC_RGBA2: return "RGBA Compressed PVRTC 2 bits"; + case kTexFormatPVRTC_RGB4: return "RGB Compressed PVRTC 4 bits"; + case kTexFormatPVRTC_RGBA4: return "RGBA Compressed PVRTC 4 bits"; + case kTexFormatETC_RGB4: return "RGB Compressed ETC 4 bits"; + case kTexFormatATC_RGB4: return "RGB Compressed ATC 4 bits"; + case kTexFormatATC_RGBA8: return "RGBA Compressed ATC 8 bits"; + + case kTexFormatETC2_RGB: return "RGB Compressed ETC2 4 bits"; + case kTexFormatETC2_RGBA1: return "RGB + 1-bit Alpha Compressed ETC2 4 bits"; + case kTexFormatETC2_RGBA8: return "RGBA Compressed ETC2 8 bits"; + case kTexFormatEAC_R: return "11-bit R Compressed EAC 4 bit"; + case kTexFormatEAC_R_SIGNED: return "11-bit signed R Compressed EAC 4 bit"; + case kTexFormatEAC_RG: return "11-bit RG Compressed EAC 8 bit"; + case kTexFormatEAC_RG_SIGNED: return "11-bit signed RG Compressed EAC 8 bit"; + +#define STR_(x) #x +#define STR(x) STR_(x) +#define DO_ASTC(bx,by) case kTexFormatASTC_RGB_##bx##x##by : return "RGB Compressed ASTC " STR(bx) "x" STR(by) " block"; case kTexFormatASTC_RGBA_##bx##x##by : return "RGBA Compressed ASTC " STR(bx) "x" STR(by) " block" + + DO_ASTC(4, 4); + DO_ASTC(5, 5); + DO_ASTC(6, 6); + DO_ASTC(8, 8); + DO_ASTC(10, 10); + DO_ASTC(12, 12); + +#undef DO_ASTC +#undef STR +#undef STR_ + + // Flash + case kTexFormatFlashATF_RGB_DXT1: return "RGB Compressed DXT1"; + case kTexFormatFlashATF_RGBA_JPG: return "RGBA JPG Compressed"; + case kTexFormatFlashATF_RGB_JPG: return "RGB JPG Compressed"; + + case kTexFormatBGRA32: return "BGRA 32 bit"; + + default: + return "Unsupported"; + } +} + +const char* GetTextureColorSpaceString (TextureColorSpace colorSpace) +{ + switch (colorSpace) + { + case kTexColorSpaceLinear: return "Linear"; + case kTexColorSpaceSRGB: return "sRGB"; + case kTexColorSpaceSRGBXenon: return "sRGB (Xenon)"; + default: return "Unsupported"; + } +} + +TextureColorSpace ColorSpaceToTextureColorSpace(BuildTargetPlatform platform, ColorSpace colorSpace) +{ + if (colorSpace == kGammaColorSpace) + return (platform == kBuildXBOX360) ? kTexColorSpaceSRGBXenon : kTexColorSpaceSRGB; + else + return kTexColorSpaceLinear; +} diff --git a/Runtime/Graphics/TextureFormat.h b/Runtime/Graphics/TextureFormat.h new file mode 100644 index 0000000..5d6eed0 --- /dev/null +++ b/Runtime/Graphics/TextureFormat.h @@ -0,0 +1,96 @@ +#pragma once + +#include "Runtime/GfxDevice/GfxDeviceTypes.h" +#include "Runtime/Math/ColorSpaceConversion.h" + +/* Important note about endianess. + Endianess needs to be swapped for the following formats: + kTexFormatARGBFloat, kTexFormatRGB565, kTexFormatARGB4444, (assuming for this too: kTexFormatRGBA4444) +*/ + +UInt32 GetBytesFromTextureFormat( TextureFormat inFormat ); +UInt32 GetMaxBytesPerPixel( TextureFormat inFormat ); +int GetRowBytesFromWidthAndFormat( int width, TextureFormat format ); +bool IsValidTextureFormat (TextureFormat format); + + +inline bool IsCompressedDXTTextureFormat( TextureFormat format ) +{ + return format >= kTexFormatDXT1 && format <= kTexFormatDXT5; +} + +inline bool IsCompressedPVRTCTextureFormat( TextureFormat format ) +{ + return format >= kTexFormatPVRTC_RGB2 && format <= kTexFormatPVRTC_RGBA4; +} + +inline bool IsCompressedETCTextureFormat( TextureFormat format ) +{ + return format == kTexFormatETC_RGB4; +} + +inline bool IsCompressedEACTextureFormat( TextureFormat format ) +{ + return format >= kTexFormatEAC_R && format <= kTexFormatEAC_RG_SIGNED; +} + +inline bool IsCompressedETC2TextureFormat( TextureFormat format ) +{ + return format >= kTexFormatETC2_RGB && format <= kTexFormatETC2_RGBA8; +} + +inline bool IsCompressedATCTextureFormat( TextureFormat format ) +{ + return format == kTexFormatATC_RGB4 || format == kTexFormatATC_RGBA8; +} +inline bool IsCompressedFlashATFTextureFormat( TextureFormat format ) +{ + return format == kTexFormatFlashATF_RGB_DXT1 || format == kTexFormatFlashATF_RGBA_JPG || format == kTexFormatFlashATF_RGB_JPG; +} +inline bool Is16BitTextureFormat(TextureFormat format ) +{ + return format == kTexFormatARGB4444 || format == kTexFormatRGBA4444 || format == kTexFormatRGB565; +} + +inline bool IsCompressedASTCTextureFormat( TextureFormat format) +{ + return format >= kTexFormatASTC_RGB_4x4 && format <= kTexFormatASTC_RGBA_12x12; +} + +inline bool IsTextureFormatSupportedOnFlash(TextureFormat format ) +{ + return IsCompressedFlashATFTextureFormat(format) || + format == kTexFormatARGB32 || + format == kTexFormatRGBA32 || + format == kTexFormatRGB24 || + format == kTexFormatAlpha8; +} + +inline bool IsAnyCompressedTextureFormat( TextureFormat format ) +{ + return IsCompressedDXTTextureFormat(format) || IsCompressedPVRTCTextureFormat(format) + || IsCompressedETCTextureFormat(format) || IsCompressedATCTextureFormat(format) + || IsCompressedFlashATFTextureFormat (format) || IsCompressedEACTextureFormat(format) + || IsCompressedETC2TextureFormat(format) || IsCompressedASTCTextureFormat(format); +} + +bool IsAlphaOnlyTextureFormat( TextureFormat format ); + +int GetTextureSizeAllowedMultiple( TextureFormat format ); +int GetMinimumTextureMipSizeForFormat( TextureFormat format ); +bool IsAlphaOnlyTextureFormat( TextureFormat format ); + +TextureFormat ConvertToAlphaTextureFormat (TextureFormat format); + +bool HasAlphaTextureFormat( TextureFormat format ); + +bool IsDepthRTFormat( RenderTextureFormat format ); +bool IsHalfRTFormat( RenderTextureFormat format ); + +const char* GetCompressionTypeString (TextureFormat format); +const char* GetTextureFormatString (TextureFormat format); +const char* GetTextureColorSpaceString (TextureColorSpace colorSpace); + +TextureColorSpace ColorSpaceToTextureColorSpace(BuildTargetPlatform platform, ColorSpace colorSpace); + +std::pair<int,int> RoundTextureDimensionsToBlocks (TextureFormat fmt, int w, int h); diff --git a/Runtime/Graphics/TextureGenerator.h b/Runtime/Graphics/TextureGenerator.h new file mode 100644 index 0000000..13e9dac --- /dev/null +++ b/Runtime/Graphics/TextureGenerator.h @@ -0,0 +1,114 @@ +#ifndef TEXTUREGENERATOR_H +#define TEXTUREGENERATOR_H + +#include "TextureFormat.h" +#include "Texture3D.h" +#include "Texture.h" +#include "Runtime/Math/Vector3.h" +#include "CubemapTexture.h" + +template<typename T, class ColorFunctor> +void GenerateTexture (Texture2D *tex, const ColorFunctor &fun) { + AssertIf (!tex); + int maxX = tex->GetGLWidth (), maxY = tex->GetGLHeight(); + T *data = (T*)tex->GetRawImageData(); + int bpp = GetBytesFromTextureFormat (tex->GetTextureFormat()) / sizeof(T); + for (int y = 0; y < maxY; y++) + for (int x = 0; x < maxX; x++) { + fun (tex, data, x, y, maxX, maxY); + data+=bpp; + } +} + +template<typename T, class ColorFunctor> +void Generate3DTexture (Texture3D *tex, const ColorFunctor &fun) { + AssertIf (!tex); + int maxX = tex->GetGLWidth (), maxY = tex->GetGLHeight(), maxZ = tex->GetDepth(); + T *data = (T*)tex->GetImageDataPointer(); + int bpp = GetBytesFromTextureFormat (tex->GetTextureFormat()) / sizeof(T); + for (int z = 0; z < maxZ; z++) { + for (int y = 0; y < maxY; y++) { + for (int x = 0; x < maxX; x++) { + fun (data, x, y, z, maxX, maxY, maxZ); + data += bpp; + } + } + } +} + +template<typename T, class ColorFunctor> +void GenerateCubeTexture (Cubemap *cubemap, const ColorFunctor &fun) { + AssertIf (!cubemap); + for (int direction = 0; direction < 6; direction++) { + const int kCubeXRemap[6] = { 2, 2, 0, 0, 0, 0 }; + const int kCubeYRemap[6] = { 1, 1, 2, 2, 1, 1 }; + const int kCubeZRemap[6] = { 0, 0, 1, 1, 2, 2 }; + const float kCubeXSign[6] = { -1.0F, 1.0F, 1.0F, 1.0F, 1.0F, -1.0F }; + const float kCubeYSign[6] = { -1.0F, -1.0F, 1.0F, -1.0F, -1.0F, -1.0F }; + const float kCubeZSign[6] = { 1.0F, -1.0F, 1.0F, -1.0F, 1.0F, -1.0F }; + + int width = cubemap->GetGLWidth (); + int height = cubemap->GetGLHeight (); + + // do the sign scale according to the cubemap specs then flip y sign + Vector3f signScale = Vector3f (kCubeXSign[direction], kCubeYSign[direction], kCubeZSign[direction]); + int byteSize = GetBytesFromTextureFormat (cubemap->GetTextureFormat ()); + + Vector2f invSize (1.0F / (float)width, 1.0F / (float)height); + UInt8* dest = cubemap->GetRawImageData (direction); + for (int y=0;y<height;y++) + { + for (int x=0;x<width;x++) + { + Vector2f uv = Scale (Vector2f (x, y), invSize) * 2.0F - Vector2f (1.0F, 1.0F); + Vector3f uvDir = Vector3f (uv.x, uv.y, 1.0F); + + uvDir.Scale (signScale); + Vector3f worldDir; + // Rotate the uv to the world direction using a table lookup + worldDir[kCubeXRemap[direction]] = uvDir[0]; + worldDir[kCubeYRemap[direction]] = uvDir[1]; + worldDir[kCubeZRemap[direction]] = uvDir[2]; + + fun ((T*)dest, worldDir); + dest += byteSize; + } + } + } +} + +template<typename T, typename ColorFunctor> +Texture2D *BuildTexture (int width, int height, TextureFormat format, const ColorFunctor &fun, bool mipmaps = false) { + Texture2D* tex = CreateObjectFromCode<Texture2D>(); + tex->SetHideFlags(Object::kHideAndDontSave); + tex->InitTexture (width, height, format, mipmaps ? Texture2D::kMipmapMask : Texture2D::kNoMipmap, 1); + tex->GetSettings().m_Aniso = 0; // disable aniso + GenerateTexture<T> (tex, fun); + mipmaps ? tex->UpdateImageData() : tex->UpdateImageDataDontTouchMipmap(); + return tex; +} + +template<typename T, class ColorFunctor> +Texture3D *Build3DTexture (int width, int height, int depth, TextureFormat format, const ColorFunctor &fun) { + Texture3D *tex = CreateObjectFromCode<Texture3D>(); + tex->SetHideFlags(Object::kHideAndDontSave); + tex->InitTexture (width, height, depth, format, false); + Generate3DTexture<T> (tex, fun); + tex->UpdateImageData(false); + return tex; +} + +template<typename T, class ColorFunctor> +Cubemap *BuildCubeTexture (int size, TextureFormat format, const ColorFunctor &fun) { + Cubemap *tex = CreateObjectFromCode<Cubemap>(); + tex->SetHideFlags(Object::kHideAndDontSave); + tex->InitTexture (size, size, format, 0, 6); + GenerateCubeTexture<T> (tex, fun); + tex->UpdateImageDataDontTouchMipmap(); + tex->GetSettings ().m_WrapMode = kTexWrapClamp; + tex->ApplySettings (); + return tex; +} + + +#endif diff --git a/Runtime/Graphics/TextureSettings.cpp b/Runtime/Graphics/TextureSettings.cpp new file mode 100644 index 0000000..a11b565 --- /dev/null +++ b/Runtime/Graphics/TextureSettings.cpp @@ -0,0 +1,64 @@ +#include "UnityPrefix.h" +#include "TextureSettings.h" +#include "Runtime/Utilities/Utility.h" +#include "Runtime/GfxDevice/GfxDevice.h" + +static int gUserMinAniso = 1; +static int gUserMaxAniso = 16; + + +void TextureSettings::SetAnisoLimits (int minAniso, int maxAniso) +{ + gUserMinAniso = minAniso; + gUserMaxAniso = maxAniso; + ErrorIf (gUserMinAniso < 1); + ErrorIf (gUserMaxAniso > 16); +} + +void TextureSettings::GetAnisoLimits (int& minAniso, int& maxAniso) +{ + minAniso = gUserMinAniso; + maxAniso = gUserMaxAniso; +} + + +void TextureSettings::Reset () +{ + m_FilterMode = kTexFilterBilinear; + m_Aniso = 1; + m_MipBias = 0.0f; + m_WrapMode = 0; +} + +void TextureSettings::CheckConsistency() +{ + m_FilterMode = clamp<int> (m_FilterMode, 0, kTexFilterCount-1); + m_WrapMode = clamp<int> (m_WrapMode, 0, kTexWrapCount-1); +} + + +void TextureSettings::Apply (TextureID texture, TextureDimension texDim, bool hasMipMap, TextureColorSpace colorSpace) const +{ + GfxDevice& device = GetGfxDevice(); + + int aniso; + // Never use anisotropic on textures where we certainly don't want it, + // and on Point filtered textures. + if (m_Aniso == 0 || m_FilterMode == kTexFilterNearest) + aniso = 1; + else + aniso = clamp (m_Aniso, gUserMinAniso, gUserMaxAniso); + + device.SetTextureParams (texture, texDim, (TextureFilterMode)m_FilterMode, + (TextureWrapMode)m_WrapMode, aniso, hasMipMap, colorSpace); +} + +#if UNITY_EDITOR +void TextureSettings::Invalidate () +{ + m_FilterMode = -1; + m_Aniso = -1; + m_MipBias = -1.0f; + m_WrapMode = -1; +} +#endif diff --git a/Runtime/Graphics/TextureSettings.h b/Runtime/Graphics/TextureSettings.h new file mode 100644 index 0000000..3082a5a --- /dev/null +++ b/Runtime/Graphics/TextureSettings.h @@ -0,0 +1,42 @@ +#pragma once + +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "Runtime/GfxDevice/GfxDeviceTypes.h" + + +// Texture filter, anisotropy, wrap mode settings. +struct TextureSettings +{ + DECLARE_SERIALIZE_NO_PPTR (GLTextureSettings) // keep the name GLTextureSettings here, it's for serialized stuff! + + int m_FilterMode; ///< enum { Nearest, Bilinear, Trilinear } Texture filter mode + int m_Aniso; ///< Anisotropy factor (1 = None, 0 = Always disabled) + float m_MipBias; ///< Bias used for LOD-selection (0 = none) + int m_WrapMode; ///< enum {Repeat, Clamp} Texture wrapping mode. + + TextureSettings () { Reset (); } + + // Set default values + void Reset(); + + void CheckConsistency(); + + #if UNITY_EDITOR + // Set all numbers to -1, marking them as invalid + void Invalidate(); + #endif + + void Apply (TextureID texture, TextureDimension texDim, bool hasMipMap, TextureColorSpace colorSpace) const; + + static void SetAnisoLimits (int minAniso, int maxAniso); + static void GetAnisoLimits (int& minAniso, int& maxAniso); +}; + +template<class TransferFunc> +void TextureSettings::Transfer (TransferFunc& transfer) +{ + TRANSFER (m_FilterMode); + TRANSFER (m_Aniso); + TRANSFER (m_MipBias); + TRANSFER (m_WrapMode); +} diff --git a/Runtime/Graphics/Transform.cpp b/Runtime/Graphics/Transform.cpp new file mode 100644 index 0000000..eaa9bdb --- /dev/null +++ b/Runtime/Graphics/Transform.cpp @@ -0,0 +1,1695 @@ +#include "UnityPrefix.h" +#include "Transform.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "Runtime/BaseClasses/SupportedMessageOptimization.h" +#include "Runtime/Serialize/SerializationMetaFlags.h" +#include "Runtime/Utilities/Word.h" +#include "Runtime/BaseClasses/IsPlaying.h" +#include "Runtime/Utilities/RecursionLimit.h" +#include "Runtime/Utilities/Prefetch.h" +#include "Runtime/Utilities/ValidateArgs.h" +#include "Runtime/Misc/BuildSettings.h" + +#if UNITY_EDITOR +#include "Editor/Src/SceneInspector.h" +static Transform::HierarchyChangedCallback* gHierarchyChangedCallback = NULL; +static Transform::HierarchyChangedCallbackSetParent* gHierarchyChangedSetParentCallback = NULL; +#endif + +static Transform* FindActiveTransformWithPathImpl (const char* path, GameObject& go, bool needsToBeRoot); + +// parentTransform * (translation * rotation * scale) + +#if DEBUGMODE +#define ASSERT_ROTATION if (!CompareApproximately (SqrMagnitude (m_LocalRotation), 1.0F)) { AssertStringObject(Format("transform.rotation of '%s' is no longer valid, due to a bad input value. Input rotation is %f, %f, %f, %f.", GetName(), q.x, q.y, q.z, q.w), this); } +#else +#define ASSERT_ROTATION +#endif + +#pragma message ("Move this away") +inline float InverseSafe (float f) +{ + if (Abs (f) > Vector3f::epsilon) + return 1.0F / f; + else + return 0.0F; + +} + +inline Vector3f InverseSafe (const Vector3f& v) +{ + return Vector3f (InverseSafe (v.x), InverseSafe (v.y), InverseSafe (v.z)); +} + +static float MakeNiceAbs(float f) +{ + union + { + float f; + UInt32 n; + } u; + u.f = f; + + int exponent = ((u.n & 0x7F800000) >> 23) - 127; + + if(exponent >= 24) + return f; + + if(exponent <= -24) + return 0; + + UInt32 v = (UInt32)f; + float fraction = f - v; + + if(fraction < 0.00001f) + return v; + + if(fraction > 0.99999f) + return v + 1; + + return f; +} + +static float MakeNice(float f) +{ + if(f >= 0) + return MakeNiceAbs(f); + else + return -MakeNiceAbs(-f); +} + +Vector3f MakeNice(const Vector3f& v) +{ + return Vector3f(MakeNice(v[0]), MakeNice(v[1]), MakeNice(v[2])); +} + + + + +Transform::Transform (MemLabelId label, ObjectCreationMode mode) +: Super(label, mode) +//, m_Children (Transform::TransformComList::allocator_type(*baseAllocator)) +#if UNITY_EDITOR +, m_VisibleRootValid(false) +#endif +, m_CachedTransformType(kNoScaleTransform) +, m_HasCachedTransformMatrix(false) +, m_HasChanged(false) +{ + m_InternalTransformType = kNoScaleTransform; + m_SupportsTransformChanged = 0; + +#if UNITY_EDITOR + // Create hint that will make euler angles at editor time match those in play mode + // + m_LocalEulerAnglesHint.x = 179.999f; + m_LocalEulerAnglesHint.y = 179.999f; + m_LocalEulerAnglesHint.z = 179.999f; + #endif +} + +void Transform::Reset () +{ + Super::Reset (); + m_LocalRotation = Quaternionf::identity (); + + m_LocalPosition = Vector3f::zero; + m_LocalScale = Vector3f::one; + +#if UNITY_EDITOR + m_LocalEulerAnglesHint.x = 0; + m_LocalEulerAnglesHint.y = 0; + m_LocalEulerAnglesHint.z = 0; +#endif + + RecalculateTransformType (); + + + m_HasCachedTransformMatrix = false; + m_HasChanged = true; + + if (GetGameObjectPtr()) + SendTransformChanged (kPositionChanged | kRotationChanged | kScaleChanged | kParentingChanged); + +#if ENABLE_EDITOR_HIERARCHY_ORDERING + m_Order = 0; +#endif +} + +Transform::~Transform () +{ +#if UNITY_EDITOR + if (m_VisibleRootValid) + GetSceneTracker().GetVisibleRootTransforms().erase(m_VisibleRootIterator); +#endif +} + +Transform::iterator Transform::Find( const Transform* child ) +{ + iterator it, itEnd = end(); + for( it = begin(); it != itEnd; ++it ) + { + if( *it == child ) + return it; + } + return itEnd; +} + +void Transform::RemoveFromParent () +{ +// Assert(!IsPersistent()); + + // If it has an father, remove this from fathers children + Transform* father = GetParent (); + if (father != NULL) + { +// Assert(!father->IsPersistent()); + + TransformComList& children = father->m_Children; + // Fastpath try back of children array + if (!children.empty() != 0 && &*children.back() == this) + { + children.pop_back(); + } + // Find this in fathers children list + else + { + iterator i = father->Find(this); + if (i != children.end ()) + children.erase (i); + } + + father->SetDirty (); + } +} + +void Transform::ClearChildrenParentPointer () +{ + for (int i=0;i<m_Children.size();i++) + { + Transform* cur = m_Children[i]; + + AssertIf(cur == NULL || cur->GetParent() != this); + if (cur && cur->GetParent() == this) + cur->m_Father = NULL; + } +} + +void Transform::ClearChild (Transform* child) +{ + iterator found = Find(child); + if (found != m_Children.end()) + m_Children.erase(found); +} + +Matrix3x3f Transform::GetWorldScale () const +{ + Matrix3x3f invRotation; + QuaternionToMatrix (Inverse (GetRotation ()), invRotation); + Matrix3x3f scaleAndRotation = GetWorldRotationAndScale (); + return invRotation * scaleAndRotation; +} + +Vector3f Transform::GetWorldScaleLossy () const +{ + Matrix3x3f rot = GetWorldScale (); + return Vector3f (rot.Get (0, 0), rot.Get (1, 1), rot.Get (2, 2)); +} + + +Matrix3x3f Transform::GetWorldRotationAndScale () const +{ + Matrix3x3f scale; + scale.SetScale (m_LocalScale); + + Matrix3x3f rotation; + QuaternionToMatrix (m_LocalRotation, rotation); + + Transform* parent = GetParent (); + if (parent) + { + ///@TODO optimize: Special case multiplication + Matrix3x3f parentTransform = parent->GetWorldRotationAndScale (); + return parentTransform * rotation * scale; + } + else + { + return rotation * scale; + } +} + +bool Transform::SetParent (Transform* newFather, SetParentOption options) +{ + if (GetGameObject().IsDestroying() || (newFather && newFather->GetGameObject().IsDestroying())) + return false; + + if (IS_CONTENT_NEWER_OR_SAME (kUnityVersion4_0_a1)) + { + if ( (GetParent () && GetParent ()->GetGameObject().IsActivating()) || (newFather && newFather->GetGameObject().IsActivating())) + { + ErrorStringObject ("Cannot change GameObject hierarchy while activating or deactivating the parent.", this); + return false; + } + } + + // Make sure that the new father is not a child of this transform. + Transform* cur; + for (cur=newFather;cur != NULL;cur = cur->m_Father) + { + if (this == cur) + return false; + } + + if ((options & kAllowParentingFromPrefab) == 0) + { + if (IsPrefabParent() || (newFather && newFather->IsPrefabParent())) + { + ErrorString("Setting the parent of a transform which resides in a prefab is disabled to prevent data corruption."); + return false; + } + } + +// if (IsPersistent() || (newFather && newFather->IsPersistent())) +// { +// ErrorString("Setting the parent of a transform on an asset is not allowed!"); +// return false; +// } + + // Save the old position in worldspace + Vector3f worldPosition = GetPosition (); + Quaternionf worldRotation = GetRotation (); + Matrix3x3f worldScale = GetWorldRotationAndScale (); + + // If it already has an father, remove this from fathers children + Transform* father = GetParent (); + if (father != NULL) + { + // Find this in fathers children list + iterator i = father->Find( this ); + AssertIf (i == father->end ()); + father->m_Children.erase (i); + father->SetDirty (); + } + + #if UNITY_EDITOR + if (newFather) + { + ///@TODO: This can use binary search because we should be able to assume that the children array is already sorted + iterator i = newFather->begin(); + iterator end = newFather->end(); + + for (;i!=end;i++) + { + #if ENABLE_EDITOR_HIERARCHY_ORDERING + if (CompareDepths(this, (*i))) + #else + if (SemiNumericCompare(GetName(), (**i).GetName()) < 0) + #endif + { + newFather->m_Children.insert (i, this); + break; + } + } + if (i == end) + newFather->m_Children.push_back (this); + + newFather->SetDirty (); + } + #else + if (newFather) + { + newFather->m_Children.push_back (this); + newFather->SetDirty (); + } + #endif + + m_Father = newFather; + + if (!(options & kDisableTransformMessage)) + { + if (options & kWorldPositionStays) + { + // Restore old position so they stay at the same position in worldspace + SetRotationSafe (worldRotation); + SetPosition (worldPosition); + SetWorldRotationAndScale ( worldScale ); + SendTransformChanged (kParentingChanged); + } + else + SendTransformChanged (kPositionChanged | kRotationChanged | kScaleChanged | kParentingChanged); + } + + #if UNITY_EDITOR + if (gHierarchyChangedSetParentCallback) + gHierarchyChangedSetParentCallback (this, father, newFather); + #endif + SetDirty (); + SetCacheDirty(); + + return true; +} + + +#if UNITY_EDITOR + +#if ENABLE_EDITOR_HIERARCHY_ORDERING +bool Transform::CompareVisibleRoots::operator() (const VisibleRootKey& lhs, const VisibleRootKey& rhs) const +{ + bool orderCompare = lhs.first != rhs.first; + if (orderCompare) + return lhs.first < rhs.first; + else + { + VisibleRootSecondaryKey lhsSecondaryKey = lhs.second; + VisibleRootSecondaryKey rhsSecondaryKey = rhs.second; + int compare = SemiNumericCompare(lhsSecondaryKey.first, rhsSecondaryKey.first); + if (compare != 0) + return compare < 0; + return lhsSecondaryKey.second < rhsSecondaryKey.second; + } +} +#else +bool Transform::CompareVisibleRoots::operator() (const VisibleRootKey& lhs, const VisibleRootKey& rhs) const +{ + int compare = SemiNumericCompare(lhs.first, rhs.first); + if (compare != 0) + return compare < 0; + return lhs.second < rhs.second; +} +#endif + +// Cannot be called Repeat as there is already another function with that name (which does currently not work for negative numbers) +inline float RepeatWorking (float t, float length) +{ + return (t - (floor (t / length) * length)); +} + +void Transform::SyncLocalEulerAnglesHint () +{ + if (IsWorldPlaying()) + return; + + Vector3f newEuler = QuaternionToEuler(m_LocalRotation) * Rad2Deg(1); + + newEuler.x = RepeatWorking(newEuler.x - m_LocalEulerAnglesHint.x + 180.0F, 360.0F) + m_LocalEulerAnglesHint.x - 180.0F; + newEuler.y = RepeatWorking(newEuler.y - m_LocalEulerAnglesHint.y + 180.0F, 360.0F) + m_LocalEulerAnglesHint.y - 180.0F; + newEuler.z = RepeatWorking(newEuler.z - m_LocalEulerAnglesHint.z + 180.0F, 360.0F) + m_LocalEulerAnglesHint.z - 180.0F; + + m_LocalEulerAnglesHint = MakeNice(newEuler); +} +#endif + +void Transform::SetLocalEulerAngles (const Vector3f& eulerAngles) +{ + ABORT_INVALID_VECTOR3 (eulerAngles, localEulerAngles, transform) + + SetLocalRotationSafe (EulerToQuaternion (eulerAngles * Deg2Rad (1))); + + #if UNITY_EDITOR + if (!IsWorldPlaying()) + { + m_LocalEulerAnglesHint = MakeNice(eulerAngles); + } + #endif +} + +Vector3f Transform::GetLocalEulerAngles () +{ + #if UNITY_EDITOR + if (!IsWorldPlaying()) + { + if ( !CompareApproximately (EulerToQuaternion (m_LocalEulerAnglesHint * Deg2Rad(1)), m_LocalRotation) ) + SyncLocalEulerAnglesHint (); + + return m_LocalEulerAnglesHint; + } + else + { + Quaternionf rotation = NormalizeSafe (m_LocalRotation); + return QuaternionToEuler (rotation) * Rad2Deg (1); + } + #else + Quaternionf rotation = NormalizeSafe (m_LocalRotation); + return QuaternionToEuler (rotation) * Rad2Deg (1); + #endif +} + + +void Transform::SetLocalPosition (const Vector3f& inTranslation) +{ + ABORT_INVALID_VECTOR3 (inTranslation, localPosition, transform); + m_LocalPosition = inTranslation; + SetDirty (); + SendTransformChanged (kPositionChanged); +} + +void Transform::SetLocalRotation (const Quaternionf& q) +{ + ABORT_INVALID_QUATERNION (q, localRotation, transform); + m_LocalRotation = q; + +#if UNITY_EDITOR + SyncLocalEulerAnglesHint (); +#endif + ASSERT_ROTATION + + SetDirty (); + SendTransformChanged (kRotationChanged); +} + +void Transform::SetLocalRotationSafe (const Quaternionf& q) +{ + SetLocalRotation (NormalizeSafe(q)); +} + +void Transform::SetRotation (const Quaternionf& q) +{ + Transform* father = GetParent (); + if (father != NULL) + SetLocalRotation (Inverse (father->GetRotation ()) * q); + else + SetLocalRotation (q); +} + +void Transform::SetRotationSafe(const Quaternionf& q) +{ + ABORT_INVALID_QUATERNION (q, rotation, transform); + Transform* father = GetParent (); + if (father != NULL) + SetLocalRotation (NormalizeSafe (Inverse (father->GetRotation ()) * q)); + else + SetLocalRotation (NormalizeSafe (q)); +} + +void Transform::SetPosition (const Vector3f& p) +{ + ABORT_INVALID_VECTOR3 (p, position, transform); + + Vector3f newPosition = p; + Transform* father = GetParent (); + if (father != NULL) + newPosition = father->InverseTransformPoint (newPosition); + + SetLocalPosition (newPosition); +} + +void Transform::SetPositionWithLocalOffset (const Vector3f& p, const Vector3f& localOffset) +{ + ABORT_INVALID_VECTOR3 (p, positionWithLocalOffset, transform); + ABORT_INVALID_VECTOR3 (localOffset, positionWithLocalOffset, transform); + Vector3f newPosition = p - TransformPoint (localOffset) + GetPosition (); + SetPosition (newPosition); +} + +Vector3f Transform::TransformPointWithLocalOffset (const Vector3f& p, const Vector3f& localOffset) const +{ + return p - TransformPoint (localOffset) + GetPosition (); +} + +void Transform::SetWorldRotationAndScale (const Matrix3x3f& scale) +{ + m_LocalScale = Vector3f::one; + + Matrix3x3f inverseRS = GetWorldRotationAndScale (); + inverseRS.Invert (); + + inverseRS = inverseRS * scale; + + m_LocalScale.x = inverseRS.Get (0, 0); + m_LocalScale.y = inverseRS.Get (1, 1); + m_LocalScale.z = inverseRS.Get (2, 2); + + RecalculateTransformType (); + SetDirty (); + SendTransformChanged (kScaleChanged | kRotationChanged | kPositionChanged); +} + +void Transform::SetLocalScale (const Vector3f& scale) +{ + ABORT_INVALID_VECTOR3 (scale, localScale, transform); + m_LocalScale = scale; + RecalculateTransformType (); + SetDirty (); + SendTransformChanged (kScaleChanged | kRotationChanged | kPositionChanged); +} + +template<bool Safe, bool Notify> +void Transform::SetPositionAndRotationInternal ( const Vector3f& p, const Quaternionf& q ) +{ + ABORT_INVALID_VECTOR3 (p, position, transform); + ABORT_INVALID_QUATERNION (q, rotation, transform); + Transform* father = GetParent (); + if (father != NULL) + { + m_LocalPosition = father->InverseTransformPoint (p); + if ( Safe ) + m_LocalRotation = NormalizeSafe (Inverse (father->GetRotation ()) * q); + else + m_LocalRotation = Inverse (father->GetRotation ()) * q; + } + else + { + m_LocalPosition = p; + if ( Safe ) + m_LocalRotation = NormalizeSafe (q); + else + m_LocalRotation = q; + } +#if UNITY_EDITOR + SyncLocalEulerAnglesHint (); +#endif + + ASSERT_ROTATION + if ( Notify ) + { + SetDirty (); + SendTransformChanged ( kPositionChanged | kRotationChanged ); + } +} + +void Transform::SetPositionAndRotationWithoutNotification (const Vector3f& p, const Quaternionf& q) +{ + SetPositionAndRotationInternal<false, false>( p, q ); +} + +void Transform::SetPositionAndRotationSafeWithoutNotification (const Vector3f& p, const Quaternionf& q) +{ + SetPositionAndRotationInternal<true, false>( p, q ); +} + +void Transform::SetPositionAndRotation (const Vector3f& p, const Quaternionf& q) +{ + SetPositionAndRotationInternal<false, true>( p, q ); +} + +void Transform::SetPositionAndRotationSafe (const Vector3f& p, const Quaternionf& q) +{ + SetPositionAndRotationInternal<true, true>( p, q ); +} + +void Transform::SetPositionWithoutNotification (const Vector3f& p) +{ + ABORT_INVALID_VECTOR3 (p, position, transform); + Transform* father = GetParent (); + if (father != NULL) + m_LocalPosition = father->InverseTransformPoint (p); + else + m_LocalPosition = p; +} + +void Transform::SetRotationWithoutNotification (const Quaternionf& q) +{ + Transform* father = GetParent (); + if (father != NULL) + m_LocalRotation = Inverse (father->GetRotation ()) * q; + else + m_LocalRotation = q; +} + +void Transform::SetLocalPositionAndRotation (const Vector3f& p, const Quaternionf& q) +{ + ABORT_INVALID_VECTOR3 (p, localPosition, transform); + ABORT_INVALID_QUATERNION (q, localRotation, transform); + m_LocalPosition = p; + m_LocalRotation = q; + + #if UNITY_EDITOR + SyncLocalEulerAnglesHint (); + #endif + + ASSERT_ROTATION + + SetDirty (); + SendTransformChanged (kPositionChanged | kRotationChanged); +} +#if UNITY_EDITOR +void Transform::RegisterHierarchyChangedCallback (HierarchyChangedCallback* callback) +{ + gHierarchyChangedCallback = callback; +} + +void Transform::RegisterHierarchyChangedSetParentCallback (HierarchyChangedCallbackSetParent* callback) +{ + gHierarchyChangedSetParentCallback = callback; +} +#endif + +UInt32 Transform::CalculateSupportedMessages () +{ + if (GetGameObject().WillHandleMessage(kTransformChanged)) + return kSupportsTransformChanged; + else + return 0; +} + +void Transform::MakeEditorValuesLookNice() +{ + m_LocalScale = MakeNice(m_LocalScale); + m_LocalPosition = MakeNice(m_LocalPosition); +} + +void Transform::SupportedMessagesDidChange (int mask) +{ + Super::SupportedMessagesDidChange(mask); + m_SupportsTransformChanged = mask & kSupportsTransformChanged; +} + +void Transform::SendTransformChanged (int changeMask) +{ + bool parentingChanged = changeMask & kParentingChanged; + + // Fastpath if we don't support any TransformChanged callbacks on this game object + if (!m_SupportsTransformChanged && !parentingChanged) + { + m_HasCachedTransformMatrix = false; + m_HasChanged = true; + + TransformComList::iterator i; + TransformComList::iterator end = m_Children.end (); + for (i = m_Children.begin ();i != end;i++) + { + Transform* child = *i; + child->SendTransformChanged (changeMask | kPositionChanged); + } + + return; + } + + m_HasCachedTransformMatrix = false; + m_HasChanged = true; + + // NOTE: SetCacheDirty() doesn't need to be called here since SendTransformChanged traverses the transform hierarchy anyway + + GameObject& go = GetGameObject (); + if (m_SupportsTransformChanged) + { + MessageData data; + data.SetData (changeMask, ClassID (int)); + go.SendMessageAny (kTransformChanged, data); + } + + if (parentingChanged) + go.TransformParentHasChanged (); + + TransformComList::iterator i; + TransformComList::iterator end = m_Children.end (); + for (i = m_Children.begin ();i != end;i++) + { + Transform* child = *i; + child->SendTransformChanged (changeMask | kPositionChanged); + } +} + +void Transform::SetCacheDirty() +{ + m_HasCachedTransformMatrix = false; + m_HasChanged = true; + + TransformComList::iterator end = m_Children.end (); + for (TransformComList::iterator i = m_Children.begin (); i != end; ++i) + (*i)->SetCacheDirty(); +} + +void Transform::BroadcastMessageAny(const MessageIdentifier& messageID, MessageData& data) +{ + GameObject* go = GetGameObjectPtr (); + if (go) + go->SendMessageAny (messageID, data); + + TransformComList::iterator i; + for (i = m_Children.begin ();i != m_Children.end ();i++) + (**i).BroadcastMessageAny (messageID, data); +} + +void Transform::RotateAroundLocal (const Vector3f& localAxis, float rad) +{ + AssertIf (!CompareApproximately (Magnitude (localAxis), 1.0F)); + + Quaternionf q = AxisAngleToQuaternion (localAxis, rad); + m_LocalRotation = Normalize (q * m_LocalRotation); + #if UNITY_EDITOR + SyncLocalEulerAnglesHint (); + #endif + + SetDirty (); + SendTransformChanged (kRotationChanged); +} + +void Transform::RotateAroundLocalSafe (const Vector3f& localAxis, float rad) +{ + if (SqrMagnitude (localAxis) > Vector3f::epsilon) + RotateAroundLocal (Normalize (localAxis), rad); +} + +void Transform::RotateAround (const Vector3f& worldAxis, float rad) +{ + AssertIf (!CompareApproximately (Magnitude (worldAxis), 1.0F)); + + Vector3f localAxis = InverseTransformDirection(worldAxis); + + Quaternionf q = AxisAngleToQuaternion (localAxis, rad); + m_LocalRotation = Normalize (m_LocalRotation * q); + #if UNITY_EDITOR + SyncLocalEulerAnglesHint (); + #endif + + SetDirty (); + SendTransformChanged (kRotationChanged); +} + +void Transform::RotateAroundSafe (const Vector3f& worldAxis, float rad) +{ + Vector3f localAxis = InverseTransformDirection(worldAxis); + if (SqrMagnitude (localAxis) > Vector3f::epsilon) + { + localAxis = Normalize (localAxis); + Quaternionf q = AxisAngleToQuaternion (localAxis, rad); + m_LocalRotation = Normalize (m_LocalRotation * q); + #if UNITY_EDITOR + SyncLocalEulerAnglesHint (); + #endif + + SetDirty (); + SendTransformChanged (kRotationChanged); + } +} + +Vector3f Transform::GetPosition () const +{ + Vector3f worldPos = m_LocalPosition; + Transform* cur = GetParent (); + while (cur) + { + worldPos.Scale (cur->m_LocalScale); + worldPos = RotateVectorByQuat (cur->m_LocalRotation, worldPos); + worldPos += cur->m_LocalPosition; + + cur = cur->GetParent (); + } + + return worldPos; +} + +Quaternionf Transform::GetRotation ()const +{ + Quaternionf worldRot = m_LocalRotation; + Transform* cur = GetParent (); + while (cur) + { + worldRot = cur->m_LocalRotation * worldRot; + cur = cur->GetParent (); + } + + return worldRot; +} + + +void Transform::GetPositionAndRotation (Vector3f& pos, Quaternionf& q) const +{ + Vector3f worldPos = m_LocalPosition; + Quaternionf worldRot = m_LocalRotation; + Transform* cur = GetParent (); + while (cur) + { + worldPos.Scale (cur->m_LocalScale); + worldPos = RotateVectorByQuat (cur->m_LocalRotation, worldPos); + worldPos += cur->m_LocalPosition; + + worldRot = cur->m_LocalRotation * worldRot; + + cur = cur->GetParent (); + } + + pos = worldPos; + q = worldRot; +} + +static TransformType DetectActualNegativeScale (int type, const Transform* transform) +{ + type &= ~kOddNegativeScaleTransform; + + // Calculate if we need to flip the back facing when rendering + // We need to use backface rendering if one or three scale axes are negative (odd count) + // In this case we enable kOddNegativeScaleTransform flag + + Transform* cur = (Transform *)transform; + while (cur) + { + TransformType parentType = (TransformType)cur->m_InternalTransformType; + // kOddNegativeScaleTransform is XOR against parent (odd+odd=even, odd+even=odd, even+even=even), other bits are OR + type = ((type | parentType) & ~kOddNegativeScaleTransform) | ((type ^ parentType) & kOddNegativeScaleTransform); + cur = cur->GetParent (); + } + + return (TransformType)type; +} + +static TransformType UpdateTransformType (TransformType type, const Transform* transform) +{ + if ((type & kOddNegativeScaleTransform) != 0) + type = DetectActualNegativeScale (type, transform); + + // kNonUniformScaleTransform 'overwrites' kUniformScaleTransform + if ((type & kNonUniformScaleTransform) != 0) + type &= ~kUniformScaleTransform; + + Assert ((type & (kNonUniformScaleTransform|kUniformScaleTransform)) != (kNonUniformScaleTransform|kUniformScaleTransform)); + return type; +} + + +TransformType Transform::GetPositionAndRotationWithTransformType (Vector3f& worldPos, Quaternionf& worldRot) const +{ + TransformType type = (TransformType)m_InternalTransformType; + + worldPos = m_LocalPosition; + worldRot = m_LocalRotation; + Transform* cur = GetParent (); + while (cur) + { + TransformType parentType = (TransformType)cur->m_InternalTransformType; + // kOddNegativeScaleTransform is XOR against parent (odd+odd=even, odd+even=odd, even+even=even), other bits are OR + type = ((type | parentType) & ~kOddNegativeScaleTransform) | ((type ^ parentType) & kOddNegativeScaleTransform); + + worldPos.Scale (cur->m_LocalScale); + worldPos = RotateVectorByQuat (cur->m_LocalRotation, worldPos); + worldPos += cur->m_LocalPosition; + + worldRot = cur->m_LocalRotation * worldRot; + + cur = cur->GetParent (); + } + + // kNonUniformScaleTransform 'overwrites' kUniformScaleTransform + if ((type & kNonUniformScaleTransform) != 0) + type &= ~kUniformScaleTransform; + Assert ((type & (kNonUniformScaleTransform|kUniformScaleTransform)) != (kNonUniformScaleTransform|kUniformScaleTransform)); + + return type; +} + +TransformType Transform::CalculateTransformMatrix (Matrix4x4f& transform) const +{ + //@TODO: Does this give any performance gain?? + Prefetch(m_CachedTransformMatrix.GetPtr()); + if (m_HasCachedTransformMatrix) + { + CopyMatrix(m_CachedTransformMatrix.GetPtr(), transform.GetPtr()); + return (TransformType)m_CachedTransformType; + } + + const Transform* transforms[32]; + int transformCount = 1; + TransformType type = (TransformType)0; + Matrix4x4f temp; + + { + // collect all transform that need CachedTransformMatrix update + transforms[0] = this; + Transform* parent = NULL; + for (parent = GetParent(); parent != NULL && !parent->m_HasCachedTransformMatrix; parent = parent->GetParent()) + { + transforms[transformCount++] = parent; + // reached maximum of transforms that we can calculate - fallback to old method + if (transformCount == 31) + { + parent = parent->GetParent(); + if (parent) + { + type = parent->CalculateTransformMatrixIterative(temp); + Assert(parent->m_HasCachedTransformMatrix); + } + break; + } + } + + // storing parent of last transform (can be null), the transform itself won't be updated + transforms[transformCount] = parent; + Assert(transformCount <= 31); + } + + // iterate transforms from lowest parent + for (int i = transformCount - 1; i >= 0; --i) + { + const Transform* t = transforms[i]; + const Transform* parent = transforms[i + 1]; + if (parent) + { + Assert(parent->m_HasCachedTransformMatrix); + // Build the local transform into temp + type |= t->CalculateLocalTransformMatrix(temp); + type |= (TransformType)parent->m_CachedTransformType; + MultiplyMatrices4x4(&parent->m_CachedTransformMatrix, &temp, &t->m_CachedTransformMatrix); + } + else + { + // Build the local transform into m_CachedTransformMatrix + type |= t->CalculateLocalTransformMatrix(t->m_CachedTransformMatrix); + } + // store cached transform + t->m_CachedTransformType = UpdateTransformType(type, t); + t->m_HasCachedTransformMatrix = true; + } + + Assert(m_HasCachedTransformMatrix); + CopyMatrix(m_CachedTransformMatrix.GetPtr(), transform.GetPtr()); + return (TransformType)m_CachedTransformType; +} + + +// This method doesn't cache all transforms - just the last one, but can calculate +// more than 32 transforms. CalculateTransformMatrix caches all results +TransformType Transform::CalculateTransformMatrixIterative (Matrix4x4f& transform) const +{ + if (m_HasCachedTransformMatrix) + { + CopyMatrix(m_CachedTransformMatrix.GetPtr(), transform.GetPtr()); + return (TransformType)m_CachedTransformType; + } + + // Build the local transform + TransformType type = CalculateLocalTransformMatrix(transform); + + Transform* parent = GetParent (); + Matrix4x4f temp; + while (parent != NULL) + { + if (parent->m_HasCachedTransformMatrix) + { + type |= (TransformType)parent->m_CachedTransformType; + MultiplyMatrices4x4 (&parent->m_CachedTransformMatrix, &transform, &temp); + // no need to iterate further - we got world transform + parent = NULL; + } + else + { + Matrix4x4f parentTransform; + type |= parent->CalculateLocalTransformMatrix(parentTransform); + MultiplyMatrices4x4 (&parentTransform, &transform, &temp); + parent = parent->GetParent (); + } + CopyMatrix (temp.GetPtr(), transform.GetPtr()); + } + + CopyMatrix(transform.GetPtr(), m_CachedTransformMatrix.GetPtr()) ; + m_CachedTransformType = UpdateTransformType(type, this); + m_HasCachedTransformMatrix = true; + return type; +} + + +TransformType Transform::CalculateLocalTransformMatrix(Matrix4x4f& matrix) const +{ + TransformType type = (TransformType)m_InternalTransformType; + if (type == kNoScaleTransform) + matrix.SetTR(m_LocalPosition, m_LocalRotation); + else + matrix.SetTRS(m_LocalPosition, m_LocalRotation, m_LocalScale); + return type; +} + + +TransformType Transform::CalculateTransformMatrixDisableNonUniformScale (Matrix4x4f& transform, Matrix4x4f& scaleOnly, float& uniformScale) const +{ + // Use CalculateTransformMatrix in order to take advantage of the cached transform. + // Only non-uniform scaled meshes need to take the slower code path. +/* TransformType cachedTransformType = CalculateTransformMatrix (transform); + if (!IsNonUniformScaleTransform(cachedTransformType)) + { + uniformScale = Magnitude(transform.GetAxisX()); + scaleOnly.SetIdentity(); + return cachedTransformType; + } +*/ + + // Build the local transform! + TransformType type = (TransformType)m_InternalTransformType; + uniformScale = m_LocalScale.x; + if (type == kNoScaleTransform) + transform.SetTR (m_LocalPosition, m_LocalRotation); + else + transform.SetTRS (m_LocalPosition, m_LocalRotation, m_LocalScale); + + // @TBD: cache parent transform and reuse it across CalculateTransformXXX funcs + Transform* parent = GetParent (); + Matrix4x4f temp; + while (parent != NULL) + { + Matrix4x4f parentTransform; + + TransformType parentType = (TransformType)parent->m_InternalTransformType; + // kOddNegativeScaleTransform is XOR against parent (odd+odd=even, odd+even=odd, even+even=even), other bits are OR + type = ((type | parentType) & ~kOddNegativeScaleTransform) | ((type ^ parentType) & kOddNegativeScaleTransform); + + if (parentType == kNoScaleTransform) + parentTransform.SetTR (parent->m_LocalPosition, parent->m_LocalRotation); + else + parentTransform.SetTRS (parent->m_LocalPosition, parent->m_LocalRotation, parent->m_LocalScale); + uniformScale *= parent->m_LocalScale.x; + + MultiplyMatrices4x4 (&parentTransform, &transform, &temp); + CopyMatrix (temp.GetPtr(), transform.GetPtr()); + parent = parent->GetParent (); + } + + // kNonUniformScaleTransform 'overwrites' kUniformScaleTransform + if ((type & kNonUniformScaleTransform) != 0) + type &= ~kUniformScaleTransform; + DebugAssert ((type & (kNonUniformScaleTransform|kUniformScaleTransform)) != (kNonUniformScaleTransform|kUniformScaleTransform)); + + // uniform or no scale + if (!IsNonUniformScaleTransform(type)) + { + scaleOnly.SetIdentity(); + return type; + } + // non-uniform scale + else + { + // Calculate scaleOnlyMatrix (In order to scale the mesh with the non-uniform scale) + Matrix4x4f worldToLocalMatrixNoScale; + GetWorldToLocalMatrixNoScale (worldToLocalMatrixNoScale); + MultiplyMatrices4x4(&worldToLocalMatrixNoScale, &transform, &scaleOnly); + scaleOnly.Get (0,3) = 0.0F; + scaleOnly.Get (1,3) = 0.0F; + scaleOnly.Get (2,3) = 0.0F; + + // Calculate the matrix without any scale applied + GetLocalToWorldMatrixNoScale (transform); + uniformScale = 1.0F; + + return type; + } +} + + +TransformType Transform::CalculateTransformMatrixDisableScale (Matrix4x4f& matrix) const +{ + Vector3f worldPos; + Quaternionf worldRot; + TransformType type = GetPositionAndRotationWithTransformType(worldPos, worldRot); + + matrix.SetTR (worldPos, worldRot); + return type; +} + +TransformType Transform::CalculateTransformMatrixScaleDelta (Matrix4x4f& m) const +{ + Matrix4x4f scaledMatrix; + TransformType type = CalculateTransformMatrix (scaledMatrix); + // Affine matrix? + if ((type & (kUniformScaleTransform | kNonUniformScaleTransform)) == 0) + { + m.SetIdentity (); + return type; + } + else + { + Matrix4x4f tmp; + GetWorldToLocalMatrixNoScale (tmp); + MultiplyMatrices4x4(&tmp, &scaledMatrix, &m); + m.Get (0,3) = 0.0F; + m.Get (1,3) = 0.0F; + m.Get (2,3) = 0.0F; + return type; + } +} + +Matrix4x4f Transform::GetWorldToLocalMatrixNoScale () const +{ + Matrix4x4f m; + GetWorldToLocalMatrixNoScale(m); + return m; +} + +const Matrix4x4f& Transform::GetWorldToLocalMatrixNoScale (Matrix4x4f& m) const +{ + Vector3f pos; + Quaternionf rot; + GetPositionAndRotation(pos, rot); + m.SetTRInverse (pos, rot); + return m; +} + +Matrix4x4f Transform::GetLocalToWorldMatrixNoScale () const +{ + Matrix4x4f m; + GetLocalToWorldMatrixNoScale(m); + return m; +} + +const Matrix4x4f& Transform::GetLocalToWorldMatrixNoScale (Matrix4x4f& m) const +{ + Quaternionf rot; Vector3f pos; + GetPositionAndRotation(pos, rot); + m.SetTR (pos, rot); + return m; +} + +Matrix4x4f Transform::GetWorldToLocalMatrix () const +{ + Matrix4x4f m, temp; + m.SetTRInverse (m_LocalPosition, m_LocalRotation); + if (m_InternalTransformType != kNoScaleTransform) + { + Matrix4x4f scale; + scale.SetScale (InverseSafe (m_LocalScale)); + MultiplyMatrices4x4 (&scale, &m, &temp); + CopyMatrix (temp.GetPtr(), m.GetPtr()); + } + + Transform* father = GetParent (); + if (father != NULL) + { + Matrix4x4f parentMat = father->GetWorldToLocalMatrix(); + MultiplyMatrices4x4 (&m, &parentMat, &temp); + CopyMatrix (temp.GetPtr(), m.GetPtr()); + } + + return m; +} + + +Matrix4x4f Transform::GetLocalToWorldMatrix () const +{ + Matrix4x4f m; + CalculateTransformMatrix (m); + return m; +} + +Vector3f Transform::TransformDirection (const Vector3f& inDirection) const +{ + return RotateVectorByQuat (GetRotation (), inDirection); +} + +Vector3f Transform::InverseTransformDirection (const Vector3f& inDirection) const +{ + return RotateVectorByQuat (Inverse(GetRotation()), inDirection); +} + +Vector3f Transform::InverseTransformPoint (const Vector3f& inPosition) const +{ + Vector3f newPosition, localPosition; + Transform* father = GetParent (); + if (father) + localPosition = father->InverseTransformPoint (inPosition); + else + localPosition = inPosition; + + localPosition -= m_LocalPosition; + newPosition = RotateVectorByQuat (Inverse(m_LocalRotation), localPosition); + if (m_InternalTransformType != kNoScaleTransform) + newPosition.Scale (InverseSafe (m_LocalScale)); + + return newPosition; +} + +Vector3f Transform::TransformPoint (const Vector3f& inPoint) const +{ + Vector3f worldPos = inPoint; + + const Transform* cur = this; + while (cur) + { + worldPos.Scale (cur->m_LocalScale); + worldPos = RotateVectorByQuat (cur->m_LocalRotation, worldPos); + worldPos += cur->m_LocalPosition; + + cur = cur->GetParent (); + } + return worldPos; +} + +template<class TransferFunction> inline +void Transform::Transfer (TransferFunction& transfer) +{ + Super::Transfer (transfer); + TRANSFER_SIMPLE (m_LocalRotation); + TRANSFER_SIMPLE (m_LocalPosition); + TRANSFER_SIMPLE (m_LocalScale); + + //TRANSFER_EDITOR_ONLY_HIDDEN (m_LocalEulerAnglesHint); + + // This needs to be here since eg. Mesh collider queries the recalculate transform type. + // and awakefromload might not have been called already. + if (transfer.IsReading()) + RecalculateTransformType (); + + // When cloning objects for prefabs and instantiate, we don't use serialization to duplicate the hierarchy, + // we duplicate the hierarchy directly + if (SerializePrefabIgnoreProperties(transfer)) + { + transfer.Transfer (m_Children, "m_Children", kHideInEditorMask | kStrongPPtrMask | kIgnoreWithInspectorUndoMask); + transfer.Transfer (m_Father, "m_Father", kHideInEditorMask | kIgnoreWithInspectorUndoMask); + } + +#if ENABLE_EDITOR_HIERARCHY_ORDERING + TRANSFER_EDITOR_ONLY_HIDDEN(m_Order); +#endif +} + +void Transform::RecalculateTransformType () +{ + // #pragma message ("Compare approximately is bad due to epsilon changing with the size of the value") + if (CompareApproximately (m_LocalScale.x, m_LocalScale.y, 0.0001F) && CompareApproximately (m_LocalScale.y, m_LocalScale.z, 0.0001F)) + { + if (CompareApproximately( m_LocalScale.x, 1.0F, 0.0001F )) + { + m_InternalTransformType = kNoScaleTransform; + } + else + { + m_InternalTransformType = kUniformScaleTransform; + if (m_LocalScale.x < 0.0F) + { + m_InternalTransformType = kOddNegativeScaleTransform | kNonUniformScaleTransform; + } + } + } + else + { + m_InternalTransformType = kNonUniformScaleTransform; + + int hasOddNegativeScale = m_LocalScale.x * m_LocalScale.y * m_LocalScale.z < 0.0F ? 1 : 0; + m_InternalTransformType |= (TransformType)(hasOddNegativeScale * kOddNegativeScaleTransform); + } +} + +void Transform::AwakeFromLoad (AwakeFromLoadMode awakeMode) +{ + Super::AwakeFromLoad (awakeMode); + SetCacheDirty(); + + // Only call SendTransformChanged if it was really changed eg. + // by a propepertyeditor or datatemplate propagation but not if it was loaded from disk + + if ((awakeMode & kDidLoadFromDisk) == 0) + { + // This is for all kinds of non-serialization Awakes. + // eg. animation. Because Transfer already recalculates + RecalculateTransformType (); + + SendTransformChanged (kPositionChanged | kRotationChanged | kScaleChanged); + } + + #if UNITY_EDITOR + if (gHierarchyChangedCallback) + gHierarchyChangedCallback (this); + #endif +} + +inline void MakeValidFloat (float* f) +{ + if (!IsFinite (*f)) + *f = 0.0F; +} + +void Transform::CheckConsistency () +{ + Super::CheckConsistency (); + MakeValidFloat (&m_LocalRotation.x); + MakeValidFloat (&m_LocalRotation.y); + MakeValidFloat (&m_LocalRotation.z); + MakeValidFloat (&m_LocalRotation.w); + MakeValidFloat (&m_LocalPosition.x); + MakeValidFloat (&m_LocalPosition.y); + MakeValidFloat (&m_LocalPosition.z); + MakeValidFloat (&m_LocalScale.x); + MakeValidFloat (&m_LocalScale.y); + MakeValidFloat (&m_LocalScale.z); + m_LocalRotation = NormalizeSafe (m_LocalRotation); + #if UNITY_EDITOR + SyncLocalEulerAnglesHint (); + #endif + + // Check if father has this as child + Transform* father = m_Father; + if (father) + { + if ( father->Find(this) == father->m_Children.end ()) + father->m_Children.push_back (this); + } + + // Check if all children are available and have this as father. Also make + // sure that any of our children is on the list exactly once. + for (int i=0;i<m_Children.size ();i++) + { + Transform* child = m_Children[i]; + Assert(child != this); + + if (child == NULL) + { + ErrorStringObject ("CheckConsistency: Transform child can't be loaded", this); + iterator it = m_Children.begin () + i; + m_Children.erase (it); + i--; + continue; + } + + Transform* parent = child->m_Father; + + #if UNITY_EDITOR + // We only try to fix the parent pointer in the Editor as we don't want to risk breaking existing players. + if (parent == NULL) + { + child->m_Father = this; + ErrorStringObject ("CheckConsistency: Restored Transform child parent pointer from NULL", child); + continue; + } + #endif + + if (parent != this) + { + iterator it = m_Children.begin () + i; + m_Children.erase (it); + i--; + ErrorStringObject ("CheckConsistency: Transform child has another parent", child); + continue; + } + + // Look for and remove multiple occurrences. + bool occursMultipleTimes = false; + for (int j = i + 1; j < m_Children.size ();) + { + Transform* otherChild = m_Children[j]; + if (otherChild == child) + { + occursMultipleTimes = true; + m_Children.erase (m_Children.begin () + j); + } + else + ++j; + } + if (occursMultipleTimes) + { + ErrorStringObject ("CheckConsistency: Transform child is linked multiple times to parent; removed extraneous links from parent", child); + } + } +} +#if ENABLE_EDITOR_HIERARCHY_ORDERING +void Transform::SetOrder(SInt32 order) +{ + m_Order = order; + SetDirty(); + + if (gHierarchyChangedCallback) + gHierarchyChangedCallback (this); + + SendTransformChanged (kParentingChanged); +} + +int Transform::CompareDepths(Transform* lhs, Transform* rhs) + { + bool orderCompare = lhs->GetOrder() != rhs->GetOrder(); + if (orderCompare) + return lhs->GetOrder() < rhs->GetOrder(); + else + { + const char* lhsName = lhs ? lhs->GetName() : ""; + const char* rhsName = rhs ? rhs->GetName() : ""; + return SemiNumericCompare(lhsName, rhsName) < 0; + } + } + +void Transform::OrderChildrenRecursively() +{ + std::sort(m_Children.begin(), m_Children.end(), Transform::CompareDepths); + + for (int i = 0; i < m_Children.size(); ++i) + { + Transform* childTrans = m_Children[i]; + childTrans->OrderChildrenRecursively(); + } +} +#endif + +IMPLEMENT_OBJECT_SERIALIZE (Transform) +IMPLEMENT_CLASS (Transform) + +static inline int FindSeperator (const char* in) +{ + const char* c = in; + while (*c != '/' && *c != '\0') + c++; + return c - in; +} + +Transform* FindRelativeTransformWithPath (Transform& transform, const char* path) +{ + LIMIT_RECURSION (2000, NULL); + + if (path[0] == '\0') + return &transform; + + int seperator = FindSeperator (path); + + if (path[0] == '/') + return FindActiveTransformWithPath (path); + else if (path[0] == '.' && path[1] == '.') + { + Transform* parent = transform.GetParent(); + if (path[2] == '/') + { + if (parent) + return FindRelativeTransformWithPath (*parent, path + 3); + else + return NULL; + } + else if (path[2] == '\0') + return parent; + } + + for (Transform::iterator i=transform.begin ();i != transform.end ();i++) + { + Transform& child = **i; + + const char* name = child.GetName(); + + // Early out if size is not the same + if (strlen(name) != seperator) + continue; + + // continue if the name isn't the same + const char* n = name; + int j; + for (j=0;j<seperator;j++,n++) + if (path[j] != *n) + break; + if (j != seperator) + continue; + + // We found the transform we were searching for + if (path[seperator] == '\0') + return &child; + // Recursively find in the children + else + { + Transform* result = FindRelativeTransformWithPath (child, path + seperator + 1); + if (result != NULL) + return result; + } + } + return NULL; +} + +string CalculateTransformPath (const Transform& transform, const Transform* to) +{ + string path; + const Transform* cur = &transform; + while (cur != to && cur != NULL) + { + if (!path.empty ()) + path = cur->GetName () + ('/' + path); + else + path = cur->GetName (); + cur = cur->GetParent (); + } + return path; +} + +void AppendTransformPath (string& path, const char* appendName) +{ + if (path.empty()) + path = appendName; + else + { + path += '/'; + path += appendName; + } +} + +void AppendTransformPath (UnityStr& path, const char* appendName) +{ + if (path.empty()) + path = appendName; + else + { + path += '/'; + path += appendName; + } +} + + + +Transform* FindActiveTransformWithPath (const char* path) +{ + bool needsToBeRoot = path[0] == '/'; + if (path[0] == '/') + path++; + + if (path[0] == 0) + return NULL; + + GameObjectList::iterator i; + + GameObjectList& tagged = GetGameObjectManager().m_TaggedNodes; + for (i=tagged.begin();i != tagged.end();i++) + { + Transform* transform = FindActiveTransformWithPathImpl(path, **i, needsToBeRoot); + if (transform) + return transform; + } + + GameObjectList& active = GetGameObjectManager().m_ActiveNodes; + for (i=active.begin();i != active.end();i++) + { + Transform* transform = FindActiveTransformWithPathImpl(path, **i, needsToBeRoot); + if (transform) + return transform; + } + + return NULL; +/* + + for (int i=0;i<gos.size ();i++) + { + if (!gos[i]->IsActive ()) + continue; + + const string& name = gos[i]->GetName (); + if (name.find (path, 0, name.size ()) == 0) + { + path += name.size (); + if (path[0] == '/') + path ++; + + Transform* transform = gos[i]->QueryComponent (Transform); + if (transform) + { + if (needsToBeRoot && transform->GetParent()) + continue; + + if (path[0] == 0) + return transform; + transform = FindRelativeTransformWithPath (*transform, path); + if (transform) + return transform; + } + } + } + + return NULL; +*/ +} + +static Transform* FindActiveTransformWithPathImpl (const char* path, GameObject& go, bool needsToBeRoot) +{ + AssertIf(!go.IsActive()); + + const char* name = go.GetName (); + size_t size = strlen(name); + + if (strncmp(name, path, size) == 0) + { + path += size; + if (path[0] == '/') + path ++; + + Transform* transform = go.QueryComponent (Transform); + if (transform) + { + if (needsToBeRoot && transform->GetParent()) + return NULL; + + if (path[0] == 0 && transform->IsActive()) + return transform; + + transform = FindRelativeTransformWithPath (*transform, path); + if (transform && transform->IsActive()) + return transform; + } + } + return NULL; +} + +Transform& Transform::GetRoot () +{ + Transform* cur = this; + Transform* curParent = NULL; + while ((curParent = cur->GetParent ()) != NULL) + cur = curParent; + + return *cur; +} + +bool IsChildOrSameTransform(Transform& transform, Transform& inParent) +{ + Transform* child = &transform; + while (child) + { + if (child == &inParent) + return true; + child = child->GetParent(); + } + return false; +} + +#if ENABLE_EDITOR_HIERARCHY_ORDERING +void Transform::GetSortedChildList (Transform::TransformComList& sortedChildren) const +{ + sortedChildren.assign(m_Children.begin(), m_Children.end()); + std::sort(sortedChildren.begin(), sortedChildren.end(), Transform::CompareDepths); +} +#endif + +void Transform::SetLocalTRS (const Vector3f& pos, const Quaternionf& rot, const Vector3f& scale) +{ + ABORT_INVALID_VECTOR3 (pos, localPosition, transform); + ABORT_INVALID_QUATERNION (rot, localRotation, transform); + m_LocalRotation = NormalizeSafe(rot); + m_LocalPosition = pos; + m_LocalScale = scale; + #if UNITY_EDITOR + SyncLocalEulerAnglesHint (); + #endif + RecalculateTransformType(); + SendTransformChanged(kPositionChanged | kRotationChanged | kScaleChanged); +} + +int GetTransformDepth(Transform& transform) +{ + Transform* parent = transform.GetParent(); + int depth = 0; + while (parent) + { + depth++; + parent = parent->GetParent(); + } + return depth; +} + +Transform* FindTransformWithName(Transform* root, const char* name) +{ + if (strcmp(root->GetName(), name) == 0) + return root; + else + { + for (int i = 0; i < root->GetChildrenCount(); i++) + { + Transform* ret = FindTransformWithName(&(root->GetChild(i)), name); + if (ret) + return ret; + } + return NULL; + } +} diff --git a/Runtime/Graphics/Transform.h b/Runtime/Graphics/Transform.h new file mode 100644 index 0000000..2e1cf66 --- /dev/null +++ b/Runtime/Graphics/Transform.h @@ -0,0 +1,327 @@ +#ifndef TRANSFORM_H +#define TRANSFORM_H + +#include "Runtime/Math/Quaternion.h" +#include "Runtime/BaseClasses/GameObject.h" +#include "Runtime/Utilities/dynamic_array.h" +#include "Runtime/Utilities/EnumFlags.h" +#include "Runtime/Modules/ExportModules.h" +using std::vector; //TODO:refactor + +/** Transformcomponent stores rotation and position of objects + * A Transformcomponent can also have child Transformcomponent's, + * which then rotate and translate relative to their father. + + * Local space is the space which is relative to its father while world space is an absolute space + * Local and Worldspace is the same if the transformcomponent does not have a father. + * The localspace functions are faster than the worldspace functions + */ + +class EXPORT_COREMODULE Transform : public Unity::Component +{ + public: + + typedef dynamic_array<ImmediatePtr<Transform> > TransformComList; + typedef TransformComList::iterator iterator; + + typedef void TransformChangedCallback (Transform* t); + typedef void HierarchyChangedCallback (Transform* t); + typedef void HierarchyChangedCallbackSetParent (Transform* obj, Transform* oldParent, Transform* newParent); + +private: + Quaternionf m_LocalRotation; + Vector3f m_LocalPosition; + Vector3f m_LocalScale; + + mutable Matrix4x4f m_CachedTransformMatrix; + mutable UInt8 m_CachedTransformType; + mutable UInt8 m_HasCachedTransformMatrix; + mutable UInt8 m_HasChanged; + +public: + // This tracks kNoScaleTransform, kUniformScaleTransform and kNonUniformScaleTransform, kHasNegativeScale + UInt8 m_InternalTransformType; + UInt8 m_SupportsTransformChanged; + + TransformComList m_Children; + ImmediatePtr<Transform> m_Father; + + #if UNITY_EDITOR + + #if ENABLE_EDITOR_HIERARCHY_ORDERING + typedef std::pair<std::string, int> VisibleRootSecondaryKey; + typedef std::pair<SInt32, VisibleRootSecondaryKey> VisibleRootKey; + #else + typedef std::pair<std::string, int> VisibleRootKey; + #endif + + struct CompareVisibleRoots + { + bool operator() (const VisibleRootKey& lhs, const VisibleRootKey& rhs) const; + }; + typedef std::map<VisibleRootKey, Transform*, CompareVisibleRoots> VisibleRootMap; + VisibleRootMap::iterator m_VisibleRootIterator; + bool m_VisibleRootValid; + Vector3f m_LocalEulerAnglesHint; + #endif + + #if ENABLE_EDITOR_HIERARCHY_ORDERING + SInt32 m_Order; + #endif + +public: + + REGISTER_DERIVED_CLASS (Transform, Component) + DECLARE_OBJECT_SERIALIZE (Transform) + + Transform (MemLabelId label, ObjectCreationMode mode); + // virtual ~Transform (); declared-by-macro + + virtual void AwakeFromLoad (AwakeFromLoadMode awakeMode); + virtual void CheckConsistency (); + virtual void SupportedMessagesDidChange (int mask); + + // Tag class as sealed, this makes QueryComponent faster. + static bool IsSealedClass () { return true; } + + /// Returns a ptr to the father transformcomponent (NULL if no father) + Transform *GetParent () const { return m_Father; } + /// Returns a reference to the root transform (top most transform with no parent) + Transform& GetRoot (); + + /// Finds given transform + iterator Find( const Transform* child ); + + /// access to the children + int GetChildrenCount ()const { return m_Children.size (); } + Transform &GetChild (int i) const { Assert (i < (int)m_Children.size()); return *m_Children[i]; } + iterator begin () { return m_Children.begin (); } + iterator end () { return m_Children.end (); } + + /// Sets the father to p(if p is invalid the Transformcomponent will have no father) + /// Returns false if the father could not be set + /// This happens only if you are trying to set the father to one of its direct/indirect children. + enum SetParentOption { kWorldPositionStays = 1 << 0, kLocalPositionStays = 1 << 1, kAllowParentingFromPrefab = 1 << 2, kDisableTransformMessage = 1 << 3 }; + bool SetParent (Transform * parent, SetParentOption options = kWorldPositionStays); + + /// Sets the rotation in local space + void SetLocalRotation (const Quaternionf& rotation); + /// Sets the Rotation in world space + void SetRotation (const Quaternionf& rotation); + /// Sets the local euler angles + void SetLocalEulerAngles (const Vector3f& eulerAngles); + + /// Sets the position in local space + /// (If the object has no father, localspace is basically the same as world space) + void SetLocalPosition (const Vector3f& inTranslation); + /// Sets the position in world space + void SetPosition (const Vector3f& position); + + /// Sets the position - a local space offset will be scaled, rotated and subtracted from the position. + /// Used to set the position For CharacterController and NavMeshAgent that have baseOffset / center. + /// For retreiving the position with a local offset use: TransformPoint (localOffset) + void SetPositionWithLocalOffset (const Vector3f& p, const Vector3f& localOffset); + + /// Transforms local space position to world space - while applying an offset which is scaled and rotated accordingly. + Vector3f TransformPointWithLocalOffset (const Vector3f& p, const Vector3f& localOffset) const; + + /// Same as above but normalizes the quaternion + void SetLocalRotationSafe (const Quaternionf& rotation); + void SetRotationSafe (const Quaternionf& rotation); + + /// Sets the scale in local space + void SetLocalScale (const Vector3f& scale); + /// Sets the scale from a rotation * scale + /// The transform can not hold the full scale if it contains skew + void SetWorldRotationAndScale (const Matrix3x3f& worldRotationAndScale); + + /// Sets the world position and rotation + void SetPositionAndRotation (const Vector3f& position, const Quaternionf& rotation); + void SetLocalPositionAndRotation (const Vector3f& position, const Quaternionf& rotation); + + /// Return matrix that converts a point from World To Local space + Matrix4x4f GetWorldToLocalMatrix () const; + /// Return matrix that converts a point from Local To World space + Matrix4x4f GetLocalToWorldMatrix () const; + + Matrix4x4f GetWorldToLocalMatrixNoScale () const; + const Matrix4x4f& GetWorldToLocalMatrixNoScale (Matrix4x4f& m) const; + Matrix4x4f GetLocalToWorldMatrixNoScale () const; + const Matrix4x4f& GetLocalToWorldMatrixNoScale (Matrix4x4f& m) const; + + TransformType CalculateTransformMatrix (Matrix4x4f& matrix) const; + + TransformType CalculateTransformMatrixScaleDelta (Matrix4x4f& matrix) const; + + TransformType CalculateTransformMatrixDisableNonUniformScale (Matrix4x4f& matrix, Matrix4x4f& scaleOnly, float& scale) const; + TransformType CalculateTransformMatrixDisableScale (Matrix4x4f& matrix) const; + + /// Whether the transform has changed since the last time this flag was set to 'false'. + bool GetChangedFlag () { return m_HasChanged; } + /// Sets the flag indicating whether the transform has changed. Most commonly used to simply set it to 'false'. + void SetChangedFlag (bool val) { m_HasChanged = val; } + + /// Gets the rotation from local to world space + Quaternionf GetRotation () const; + /// Gets the local rotation + Quaternionf GetLocalRotation () const { return m_LocalRotation; } + /// Gets the local euler angles (in the editor it is first ensures that they are in sync with the local rotation quaternion) + Vector3f GetLocalEulerAngles (); + + /// Gets the local position relative to the father + Vector3f GetLocalPosition () const {return m_LocalPosition;} + /// Gets the position in world space + Vector3f GetPosition () const; + + /// Returns the local scale + Vector3f GetLocalScale () const { return m_LocalScale; } + /// Returns the world rotation and scale. + /// (It is impossible to return a Vector3 because the scale might be skewed) + Matrix3x3f GetWorldRotationAndScale () const; + + Matrix3x3f GetWorldScale () const; + + Vector3f GetWorldScaleLossy () const; + + /// Rotates the transform around axis by rad + void RotateAroundLocal (const Vector3f& localAxis, float rad); + /// Same, but normalizes the axis for you + void RotateAroundLocalSafe (const Vector3f& localAxis, float rad); + /// Rotates the transform around axis by rad + void RotateAround (const Vector3f& worldAxis, float rad); + /// Same, but normalizes the axis for you + void RotateAroundSafe (const Vector3f& worldAxis, float rad); + + + /// transforms a point from localspace to worldspace + Vector3f TransformPoint (const Vector3f& inPoint) const; + /// Transforms a direction from localspace to worldspace + /// (Ignores scale) + Vector3f TransformDirection (const Vector3f& inDirection) const; + + /// Transforms a point from worldspace to localspace + Vector3f InverseTransformPoint (const Vector3f& inDirection) const; + /// Transforms a direction from worldspace to localspace + /// (Ignores scale) + Vector3f InverseTransformDirection (const Vector3f& inDirection) const; + + #if UNITY_EDITOR + /// Register a function which is called whenever a transformcomponent is changed + static void RegisterHierarchyChangedCallback (HierarchyChangedCallback* callback); + static void RegisterHierarchyChangedSetParentCallback (HierarchyChangedCallbackSetParent* callback); + + VisibleRootMap::iterator* GetVisibleRootIterator () { return m_VisibleRootValid ? &m_VisibleRootIterator : NULL; } + void SetVisibleRootIterator (const VisibleRootMap::iterator& it) { m_VisibleRootIterator = it; m_VisibleRootValid = true; } + void ClearVisibleRootIterator () { m_VisibleRootValid = false; } + #endif // End UNITY_EDITOR + + #if ENABLE_EDITOR_HIERARCHY_ORDERING + static int CompareDepths(Transform* lhs, Transform* rhs); + + SInt32 GetOrder () const { return m_Order; } + void SetOrder (SInt32 order); + void OrderChildrenRecursively(); + void GetSortedChildList (TransformComList& sortedChildren) const; + #endif + + TransformComList& GetChildrenInternal () { return m_Children; } + const TransformComList& GetChildrenInternal () const { return m_Children; } + ImmediatePtr<Transform>& GetParentPtrInternal () { return m_Father; } + + /// Reset position&rotation + void Reset (); + + /// Sets the world position and rotation without sending out a TransformChanged message to the gameobject and without setting dirty + /// (Not sending TransformChanged will result in the Renderer + /// not updating the AABB to reflect the transform change) + void SetPositionAndRotationWithoutNotification (const Vector3f& position, const Quaternionf& q); + void SetPositionWithoutNotification (const Vector3f& position); + void SetRotationWithoutNotification (const Quaternionf& q); + void SetPositionAndRotationSafeWithoutNotification (const Vector3f& p, const Quaternionf& q); + + void SetPositionAndRotationSafe (const Vector3f& p, const Quaternionf& q); + + void GetPositionAndRotation (Vector3f& pos, Quaternionf& q)const; + TransformType GetPositionAndRotationWithTransformType (Vector3f& worldPos, Quaternionf& worldRot) const; + + /// You seldomly want to call this function yourself. + /// Sends the transform changed message to itself and all children. + /// A bitmask specifies which components have changed! + /// kParentingChanged is also called when SetOrder(SInt32) is called. (Editor Only) + enum { kPositionChanged = 1 << 0, kRotationChanged = 1 << 1, kScaleChanged = 1 << 3, kAnimatePhysics = 1 << 4, kParentingChanged = 1 << 5}; + void SendTransformChanged (int mask); + + /// private but used by datatemplates + void RemoveFromParent (); + void ClearChildrenParentPointer (); + void ClearChild (Transform* child); + + /// Broadcasts a message to this and all child transforms + void BroadcastMessageAny(const MessageIdentifier& message, MessageData& data); + + void SetLocalTRS (const Vector3f& pos, const Quaternionf& rot, const Vector3f& scale); + + inline void SetLocalRotationWithoutNotification (const Quaternionf& rotation) + { + m_LocalRotation = rotation; + #if UNITY_EDITOR + SyncLocalEulerAnglesHint (); + #endif + } + + inline void SetLocalPositionWithoutNotification (const Vector3f& inTranslation) + { + m_LocalPosition = inTranslation; + } + + inline void SetLocalScaleWithoutNotification (const Vector3f& scale) + { + m_LocalScale = scale; + RecalculateTransformType (); + } + + // For use only by the animation system + void RecalculateTransformType (); + + UInt32 CalculateSupportedMessages (); + + void MakeEditorValuesLookNice(); + +private: + + friend class AnimationBinder; + friend void SampleAnimation (GameObject& go, class AnimationClip& clip, float inTime, int wrapMode, float deltaTime); + + TransformType CalculateLocalTransformMatrix(Matrix4x4f& matrix) const; + TransformType CalculateTransformMatrixIterative (Matrix4x4f& matrix) const; + + void SetCacheDirty(); + + template<bool Safe, bool Notify> + void SetPositionAndRotationInternal( const Vector3f& position, const Quaternionf& rotation ); + + #if UNITY_EDITOR + /// Makes the local euler angles be in sync with the quaternion rotation + void SyncLocalEulerAnglesHint (); + #endif +}; + +ENUM_FLAGS(Transform::SetParentOption); + +Transform* FindRelativeTransformWithPath (Transform& transform, const char* path); + +Transform* FindActiveTransformWithPath (const char* path); + +Transform* FindTransformWithName (Transform* root, const char* name); + +std::string CalculateTransformPath (const Transform& transform, const Transform* to); +void AppendTransformPath (string& path, const char* appendName); +void AppendTransformPath (UnityStr& path, const char* appendName); + +/// Is transform a child of parent? Or is the transform the same. +bool IsChildOrSameTransform(Transform& transform, Transform& parent); + +EXPORT_COREMODULE int GetTransformDepth(Transform& transform); + + +#endif diff --git a/Runtime/Graphics/TransformTests.cpp b/Runtime/Graphics/TransformTests.cpp new file mode 100644 index 0000000..7d6e870 --- /dev/null +++ b/Runtime/Graphics/TransformTests.cpp @@ -0,0 +1,102 @@ +#include "UnityPrefix.h" + +#if ENABLE_UNIT_TESTS + +#include "External/UnitTest++/src/UnitTest++.h" +#include "Transform.h" +#include "Runtime/Misc/GameObjectUtility.h" +#include "Runtime/Testing/TestFixtures.h" +#include "Runtime/Testing/Testing.h" +#include "Editor/Src/Prefabs/Prefab.h" + + +class Prefab; + +SUITE (TransformTests) +{ + class TransformFixture : TestFixtureBase + { + protected: + Transform* MakeTransform () + { + Transform* tf = NewTestObject<Transform> (); + GameObject* go = NewTestObject<GameObject> (); + go->AddComponentInternal (tf); + return tf; + } + + Prefab* MakePrefabParent () + { + Prefab* prefab = NewTestObject<Prefab> (); + prefab->m_IsPrefabParent = true; + return prefab; + } + }; + + TEST_FIXTURE (TransformFixture, Has_Null_Parrent_By_Default) + { + Transform* transform = MakeTransform (); + CHECK_EQUAL ((Transform*)0, transform->GetParent ()); + } + + TEST_FIXTURE (TransformFixture, SetParent_Returns_True_If_New_Parent_Is_Null) + { + Transform* transform = MakeTransform (); + CHECK_EQUAL (true, transform->SetParent (0)); + } + + TEST_FIXTURE (TransformFixture, SetParent_Returns_False_When_GameObject_Is_Being_Destroyed) + { + Transform* transform = MakeTransform (); + Transform* parent = MakeTransform (); + transform->GetGameObject().WillDestroyGameObject (); + CHECK_EQUAL (false, transform->SetParent (parent)); + } + + TEST_FIXTURE (TransformFixture, SetParent_Returns_False_When_GameObject_Of_New_Parent_Is_Being_Destroyed) + { + Transform* transform = MakeTransform (); + Transform* parent = MakeTransform (); + parent->GetGameObject().WillDestroyGameObject (); + CHECK_EQUAL (false, transform->SetParent (parent)); + } + + TEST_FIXTURE (TransformFixture, SetParent_Returns_False_If_New_Parent_Is_A_Child) + { + Transform* parent = MakeTransform (); + Transform* child = MakeTransform (); + child->SetParent (parent); + CHECK_EQUAL (false, parent->SetParent (child)); + } + + #if UNITY_EDITOR + TEST_FIXTURE (TransformFixture, SetParent_With_Disable_Parenting_From_Prefab_Returns_False_If_Tranform_Resides_In_Prefab_EditorOnly) + { + Transform* transform = MakeTransform (); + transform->m_Prefab = PPtr<Prefab> (MakePrefabParent ()); + + EXPECT (Error, "Setting the parent of a transform which resides in a prefab is disabled"); + CHECK_EQUAL (false, transform->SetParent (MakeTransform(), Transform::kWorldPositionStays)); + } + #endif + + TEST (ChildPosition_Test) + { + GameObject& go = CreateGameObject ("parent", "Transform", NULL); + go.GetComponent (Transform).SetLocalScale (Vector3f (1.0F, 1.0F, 0.1F)); + + GameObject& child = CreateGameObject ("child", "Transform", NULL); + + child.GetComponent (Transform).SetParent (&go.GetComponent (Transform), Transform::kLocalPositionStays); + + child.GetComponent (Transform).SetLocalPosition (Vector3f (0.0F, 0.0F, 10.0F)); + child.GetComponent (Transform).SetLocalScale (Vector3f (1.0F, 1.0F, 1.0F)); + child.GetComponent (Transform).SetLocalRotation (Quaternionf::identity ()); + + CHECK ( CompareApproximately (child.GetComponent (Transform).GetPosition (), Vector3f (0.0F,0.0F,1.0F)) ); + + DestroyObjectHighLevel (&go); + } +} + +#endif // ENABLE_UNIT_TESTS diff --git a/Runtime/Graphics/TriStripper.cpp b/Runtime/Graphics/TriStripper.cpp new file mode 100644 index 0000000..89d6c1b --- /dev/null +++ b/Runtime/Graphics/TriStripper.cpp @@ -0,0 +1,112 @@ +#include "UnityPrefix.h" +#include "External/Tristripper/Striper.h" +#include "TriStripper.h" +#include "Runtime/Filters/Mesh/Mesh.h" +#include "Runtime/Utilities/vector_utility.h" +#include "Runtime/Utilities/LogAssert.h" + +using namespace std; + + +bool Stripify (const UInt32* faces, int count, UNITY_TEMP_VECTOR(UInt32)& strip) +{ + Assert (faces != NULL || !count); + strip.clear (); + + // Fail on empty input. + if (!count) + return false; + + STRIPERCREATE stripinput; + stripinput.DFaces = const_cast<UInt32*> (faces); + stripinput.NbFaces = count / 3; + + STRIPERRESULT stripresult; + Striper striper; + if (striper.Init (stripinput) && striper.Compute (stripresult) && stripresult.NbStrips == 1) + { + int stripSize = stripresult.StripLengths[0]; + UInt32* stripData = reinterpret_cast<UInt32*> (stripresult.StripRuns); + reserve_trimmed (strip, stripSize); + strip.assign (stripData, stripData + stripSize); + return true; + } + return false; +} + +template<typename T> +int CountTrianglesInStrip (const T* strip, int length) +{ + int count = 0; + // de-stripify : + int n = length; + for(int i=0;i<(n-2);i++) + { + T a = strip[i]; + T b = strip[i+1]; + T c = strip[i+2]; + + // skip degenerates + if ( a == b || a == c || b == c ) + continue; + + count++; + } + return count; +} + +template int CountTrianglesInStrip<UInt16> (const UInt16* strip, int length); +template int CountTrianglesInStrip<UInt32> (const UInt32* strip, int length); + +// WARNING: You have to make sure you have space for all indices in trilist (you can use CountTrianglesInStrip for that) +template<typename Tin, typename Tout> +void Destripify(const Tin* strip, int length, Tout* trilist, int capacity) +{ + // de-stripify : + int n = length; + int triIndex = 0; + for(int i=0;i<(n-2);i++) + { + Tin a = strip[i]; + Tin b = strip[i+1]; + Tin c = strip[i+2]; + + // skip degenerates + if ( a == b || a == c || b == c ) + continue; + + // do the winding flip-flop of strips : + if ( (i&1) == 1 ) + std::swap (a,b); + + trilist[triIndex++] = a; + trilist[triIndex++] = b; + trilist[triIndex++] = c; + } +} + + +template void Destripify<UInt32, UInt32> (const UInt32* strip, int length, UInt32* trilist, int capacity); +template void Destripify<UInt16, UInt32> (const UInt16* strip, int length, UInt32* trilist, int capacity); +template void Destripify<UInt16, UInt16> (const UInt16* strip, int length, UInt16* trilist, int capacity); + +void Destripify (const UInt16* strip, int length, UNITY_TEMP_VECTOR(UInt16)& trilist) +{ + int oldSize = trilist.size(); + trilist.resize (oldSize + CountTrianglesInStrip(strip, length) * 3); + Destripify(strip, length, &trilist[oldSize], trilist.size()); +} + +void Destripify (const UInt16* strip, int length, UNITY_TEMP_VECTOR(UInt32)& trilist) +{ + int oldSize = trilist.size(); + trilist.resize (oldSize + CountTrianglesInStrip(strip, length) * 3); + Destripify(strip, length, &trilist[oldSize], trilist.size()); +} + +void Destripify (const UInt32* strip, int length, UNITY_TEMP_VECTOR(UInt32)& trilist) +{ + int oldSize = trilist.size(); + trilist.resize (oldSize + CountTrianglesInStrip(strip, length) * 3); + Destripify(strip, length, &trilist[oldSize], trilist.size()); +} diff --git a/Runtime/Graphics/TriStripper.h b/Runtime/Graphics/TriStripper.h new file mode 100644 index 0000000..63d02fe --- /dev/null +++ b/Runtime/Graphics/TriStripper.h @@ -0,0 +1,22 @@ +#ifndef TRISTRIPPER_H +#define TRISTRIPPER_H + +#include <vector> +#include "Runtime/Allocator/MemoryMacros.h" +#include "Runtime/Modules/ExportModules.h" + +bool EXPORT_COREMODULE Stripify (const UInt32* faces, int count, UNITY_TEMP_VECTOR(UInt32)& strip); + +/// Destrips a triangle strip into a face list. +/// Adds the triangles from the strip into the trilist +void EXPORT_COREMODULE Destripify (const UInt32* strip, int length, UNITY_TEMP_VECTOR(UInt32)& trilist); +void EXPORT_COREMODULE Destripify (const UInt16* strip, int length, UNITY_TEMP_VECTOR(UInt32)& trilist); +void EXPORT_COREMODULE Destripify (const UInt16* strip, int length, UNITY_TEMP_VECTOR(UInt16)& trilist); + +template<typename Tin, typename Tout> +void EXPORT_COREMODULE Destripify(const Tin* strip, int length, Tout* trilist, int capacity); + +template<typename T> +int EXPORT_COREMODULE CountTrianglesInStrip (const T* strip, int length); + +#endif |