public static Data2D <byte> ReadData2DBytes(this BinaryReader br) { Rectangle bounds = br.ReadRectangle(); int area = bounds.Width * bounds.Height; Data2D <byte> result = new Data2D <byte>(area != 0 ? br.ReadBytes(area) : null, bounds); return(result); }
public void Optimise() { Rectangle extents = heightmapData.FindTrimBounds(DefaultHeight); if (heightmapData.Bounds != extents) { heightmapData = heightmapData.CopyWithNewBounds(extents, DefaultHeight); } }
public static void WriteData2DBytes(this BinaryWriter bw, Data2D <byte> data) { bw.Write(data.Bounds); int area = data.Bounds.Width * data.Bounds.Height; if (area != 0) { Debug.Assert(data.Data.Length == area); bw.Write(data.Data); } }
/// <summary>Create a sprite from world-oriented color data</summary> public static Sprite MakeSpriteFromWorld(this Data2D <Color> data, GraphicsDevice device) { Debug.Assert(data.Width > 0 && data.Height > 0); var clientOrientedData = data.CopyFlipY(); Texture2D texture = new Texture2D(device, clientOrientedData.Width, clientOrientedData.Height); texture.SetData(clientOrientedData.Data); return(new Sprite(texture, clientOrientedData.OriginInData)); }
public static Data2D <Color> CreateColorData(this MaskData mask, Color color) { var data = new Data2D <Color>(mask.Bounds); for (var y = data.StartY; y < data.EndY; y++) { for (var x = data.StartX; x < data.EndX; x++) { data[x, y] = mask[x, y] ? color : Color.Transparent; } } return(data); }
/// <summary>Set an area to a flat height given a mask of the base of the object</summary> public void SetFromFlatBaseMask(MaskData maskData, byte height) { // Ensure that there's enough room in the heightmap to contain mask... heightmapData = heightmapData.LazyCopyExpandToContain(maskData.Bounds, DefaultHeight); for (int y = maskData.StartY; y < maskData.EndY; y++) { for (int x = maskData.StartX; x < maskData.EndX; x++) { if (maskData[x, y]) { heightmapData[x, y] = height; } } } }
/// <summary>Set from a 1px deep alpha mask (such as for a railing)</summary> public void SetFromRailingMask(MaskData maskData) { heightmapData = heightmapData.LazyCopyExpandToContain(new Rectangle(maskData.OffsetX, 0, maskData.Width, 1), DefaultHeight); for (int x = maskData.StartX; x < maskData.EndX; x++) // For each column in the image { for (int y = maskData.EndY - 1; y >= maskData.StartY; y--) // Search top-to-bottom { if (maskData[x, y]) { heightmapData[x, 0] = (byte)y; goto nextColumn; } } nextColumn: ; } }
/// <summary>Create a sprite from client-oriented color data</summary> public static Sprite MakeSprite(this Data2D <Color> data, GraphicsDevice device) { Debug.Assert(data.Width > 0 && data.Height > 0); Texture2D texture; if (device != null) // <- allows loading headless { texture = new Texture2D(device, data.Width, data.Height); texture.SetData(data.Data); } else { texture = null; } return(new Sprite(texture, data.OriginInData)); }
/// <summary>Convert color data to a trimmed 1-bit mask</summary> public static MaskData CreateMask(this Data2D <Color> data, Color color, bool inverse = false) { Rectangle trimBounds = data.FindTrimBounds(color, inverse); // TODO: Avoid this copy... Data2D <Color> trimData = data.CopyWithNewBounds(trimBounds); MaskData mask = new MaskData(trimData.Bounds); for (int y = trimData.StartY; y < trimData.EndY; y++) { for (int x = trimData.StartX; x < trimData.EndX; x++) { mask[x, y] = ((trimData[x, y] == color) != inverse); } } return(mask); }
public Heightmap(AnimationDeserializeContext context) { DefaultHeight = context.br.ReadByte(); OneWay = context.br.ReadBoolean(); OneWayThickness = context.br.ReadByte(); if (context.br.ReadBoolean()) { Rectangle bounds = context.br.ReadRectangle(); byte[] data = context.br.ReadBytes(bounds.Width * bounds.Height); heightmapData = new Data2D <byte>(data, bounds); } else { heightmapData = default(Data2D <byte>); } instructions = HeightmapInstructionExtensions.Deserialize(context); }
/// <param name="slope">Number of pixels to travel backwards before traveling in the oblique direction</param> public void SetFromFrontEdge(MaskData maskData, int frontEdgeDepth, int depth, Oblique obliqueDirection, int slope, int offset) { Debug.Assert(depth > 0); Debug.Assert(slope > 0); // How far do we travel on the X axis as we go backwards? int pixelsTraveledSideways = ((depth + slope - 1) / slope - 1) * (int)obliqueDirection; int outputStartX = Math.Min(maskData.StartX, maskData.StartX + pixelsTraveledSideways); int outputEndX = Math.Max(maskData.EndX, maskData.EndX + pixelsTraveledSideways); // Ensure that there's enough room in the heightmap to contain the maximum extents of the processed mask... Rectangle outputPotentialBounds = new Rectangle(outputStartX, frontEdgeDepth, outputEndX - outputStartX, depth); heightmapData = heightmapData.LazyCopyExpandToContain(outputPotentialBounds, DefaultHeight); // Read the mask upwards to find the "lip" of the mask surface for (int x = maskData.StartX; x < maskData.EndX; x++) // For each column in the image { for (int y = maskData.StartY; y < maskData.EndY; y++) // Search from bottom-to-top { if (maskData[x, y]) { // Found the lip at a given Y height, copy it backwards at the given pitch for (int d = 0; d < depth; d++) { int zz = frontEdgeDepth + d; int xx = x + (d / slope) * (int)obliqueDirection; heightmapData[xx, zz] = (byte)(y - frontEdgeDepth + offset); } goto nextColumn; } } nextColumn: ; } }
/// <summary>Perform a transform of the data around its origin, using the given transform matrix</summary> public Data2D <T> Transform(IntMatrix2 matrix) { // Local corners (inclusive) Point TL = new Point(OffsetX, OffsetY); Point TR = new Point(OffsetX + Width - 1, OffsetY); Point BL = new Point(OffsetX, OffsetY + Height - 1); Point BR = new Point(OffsetX + Width - 1, OffsetY + Height - 1); // Transformed corners: Point cornerA = matrix.Transform(TL); Point cornerB = matrix.Transform(TR); Point cornerC = matrix.Transform(BL); Point cornerD = matrix.Transform(BR); // Convert to rectangle: int minX = Math.Min(Math.Min(cornerA.X, cornerB.X), Math.Min(cornerC.X, cornerD.X)); int minY = Math.Min(Math.Min(cornerA.Y, cornerB.Y), Math.Min(cornerC.Y, cornerD.Y)); int maxX = Math.Max(Math.Max(cornerA.X, cornerB.X), Math.Max(cornerC.X, cornerD.X)); int maxY = Math.Max(Math.Max(cornerA.Y, cornerB.Y), Math.Max(cornerC.Y, cornerD.Y)); Rectangle outputBounds = new Rectangle(minX, minY, maxX - minX + 1, maxY - minY + 1); // Now transform the data: Data2D <T> transformed = new Data2D <T>(outputBounds); for (int y = StartY; y < EndY; y++) { for (int x = StartX; x < EndX; x++) { // INLINED: IntMatrix2.Transform int outX = x * matrix.xToX + y * matrix.yToX; int outY = x * matrix.xToY + y * matrix.yToY; // INLINED: indexer operator (x2) transformed.Data[(outX - transformed.OffsetX) + (outY - transformed.OffsetY) * transformed.Width] = Data[(x - OffsetX) + (y - OffsetY) * Width]; } } return(transformed); }
// TODO: This is disabled at the moment, because it will probably make authoring difficult... // (Also it's less of a big deal than physics heightmaps) public void OptimiseForShadowReceiver() { if (heightmapData.Data == null) { return; // <- nothing to optimise } Debug.Assert(heightmapData.Data.Length > 0); // <- Data2D should be enforcing this condition // See if we have a heightmap filled with just a single value: byte startValue = heightmapData.Data[0]; for (int i = 0; i < heightmapData.Data.Length; i++) { if (heightmapData.Data[i] != startValue) { goto hasMoreThanOneValue; } } // Convert to fixed-height: DefaultHeight = startValue; heightmapData = new Data2D <byte>(); return; hasMoreThanOneValue: Rectangle extents = heightmapData.FindTrimBoundsForShadowReceiver(); // Assert that we're shrinking (because if we expand, then we're adding in wrong data, due to DefaultHeight) Debug.Assert(extents.Left >= heightmapData.Bounds.Left); Debug.Assert(extents.Right <= heightmapData.Bounds.Right); Debug.Assert(extents.Top >= heightmapData.Bounds.Top); Debug.Assert(extents.Bottom <= heightmapData.Bounds.Bottom); if (heightmapData.Bounds != extents) { heightmapData = heightmapData.CopyWithNewBounds(extents, DefaultHeight); } }
/// <summary>Set heights from a top-surface mask where the front edge is at a particular depth</summary> /// <param name="maskData">The 1-bit mask representing the top surface</param> /// <param name="frontEdgeDepth">The depth of the front edge of pixels in the mask</param> /// <param name="perspective">Oblique direction that the mask projects backwards towards</param> public void SetFromObliqueTopMask(MaskData maskData, int frontEdgeDepth, Oblique oblique) { // Ensure that there's enough room in the heightmap to contain the maximum extents of the mask Rectangle outputPotentialBounds = maskData.Bounds; outputPotentialBounds.Y = frontEdgeDepth; // The usable Z range = [frontEdgeDepth, frontEdgeDepth + Height) heightmapData = heightmapData.LazyCopyExpandToContain(outputPotentialBounds, DefaultHeight); // Note: Y axis seeks from the top downwards (from back to front) for (int y = maskData.EndY - 1; y >= maskData.StartY; y--) { for (int x = maskData.StartX; x < maskData.EndX; x++) { if (maskData[x, y]) { int height = GetHeightByWalkingObliqueForward(maskData, frontEdgeDepth, oblique, x, y); int z = y - height; heightmapData[x, z] = (byte)height; // (If height overflows... at least it will be obvious) } } } }
public void ClearToHeight(byte height) { DefaultHeight = height; heightmapData = default(Data2D <byte>); }
public static Rectangle FindTrimBounds(this Data2D <byte> data, byte defaultValue) { if (data.Data == null) { return(Rectangle.Empty); } // NOTE: Bounds are found on non-offset data, and offset is applied at the end int top = 0; // Search downwards to find top row with a matching pixel... for (; top < data.Height; top++) { for (int x = 0; x < data.Width; x++) { if (data.Data[x + top * data.Width] != defaultValue) { goto foundTop; } } } return(Rectangle.Empty); // No matching data found foundTop: int bottom = data.Height - 1, left = 0, right = data.Width - 1; // Search upwards to find bottom row with a matching pixel... for (; bottom > top; bottom--) // <- (exit if we reach the 'top' row: 1px tall) { for (int x = 0; x < data.Width; x++) { if (data.Data[x + bottom * data.Width] != defaultValue) { goto foundBottom; } } } foundBottom: // Search left to find left column with a matching pixel... for (; left < data.Width; left++) { for (int y = top; y <= bottom; y++) { if (data.Data[left + y * data.Width] != defaultValue) { goto foundLeft; } } } foundLeft: // Search right to find right column with a matching pixel... for (; right > left; right--) // <- (exit if we reach the 'left' column: 1px wide) { for (int y = top; y <= bottom; y++) { if (data.Data[right + y * data.Width] != defaultValue) { goto foundRight; } } } foundRight: return(new Rectangle(left + data.OffsetX, top + data.OffsetY, right - left + 1, bottom - top + 1)); }
public void SetFromObliqueSide(MaskData maskData, Oblique obliqueDirection, int offset) { // Straight and Right use the same input direction (because Straight input does not make sense, but Straight output is ok) int inputReadDirection = 1; int x = maskData.StartX; if (obliqueDirection == Oblique.Left) { inputReadDirection = -1; x = maskData.EndX - 1; } int y; while (x >= maskData.StartX && x < maskData.EndX) { for (y = maskData.StartY; y < maskData.EndY; y++) // read bottom-to-top { if (maskData[x, y]) { goto foundStartPosition; } } x += inputReadDirection; } // No data found! return; foundStartPosition: // Ensure that there's enough room in the heightmap to contain the maximum extents of the processed mask... { int left, right; if (inputReadDirection == 1) { left = x; right = maskData.EndX - 1; } else // reading right-to-left { left = maskData.StartX; right = x; } // Account for offset: left += Math.Min(offset, 0); right += Math.Max(offset, 0); int front = y; int back = front + (right - left); // can move back one pixel for each column of input Rectangle outputPotentialBounds = new Rectangle(left, front, (right - left) + 1, (back - front) + 1); heightmapData = heightmapData.LazyCopyExpandToContain(outputPotentialBounds, DefaultHeight); } // Convert mask to heightmap: int writeX = x; int writeZ = y; int baseY = y; while (x >= maskData.StartX && x < maskData.EndX) // For each column to end of image { y = baseY; // Count pixels from base upwards while (y < maskData.EndY && maskData[x, y]) { y++; } int height = y - baseY; if (height > 0) { int i = 0; do { heightmapData[writeX + i * Math.Sign(offset), writeZ] = (byte)Math.Min(byte.MaxValue, height); i++; } while(i < Math.Abs(offset)); } // Move input: x += inputReadDirection; baseY++; // Move output: writeX += (int)obliqueDirection; writeZ++; } }
// TODO: If we can pass in a query direction, we can super-optimise at the top and bottom sides public static Rectangle FindTrimBoundsForShadowReceiver(this Data2D <byte> data) { if (data.Data == null) { return(Rectangle.Empty); } // NOTE: Bounds are found on non-offset data, and offset is applied at the end // Top must all be the same value (due to angular queries) byte topValue = data.Data[0]; int top = 0; for (; top < data.Height; top++) { for (int x = 0; x < data.Width; x++) { if (data.Data[x + top * data.Width] != topValue) { goto foundTop; } } } foundTop: int bottom = data.Height - 1, left = 0, right = data.Width - 1; // Bottom must all be the same value (due to angular queries) byte bottomValue = data.Data[bottom * data.Width]; for (; bottom > top; bottom--) // <- (exit if we reach the 'top' row: 1px tall) { for (int x = 0; x < data.Width; x++) { if (data.Data[x + bottom * data.Width] != bottomValue) { goto foundBottom; } } } foundBottom: // Left queries inwards, so columns must match for (; left + 1 < data.Width; left++) { for (int y = top; y <= bottom; y++) { if (data.Data[left + 1 + y * data.Width] != data.Data[left + y * data.Width]) { goto foundLeft; } } } foundLeft: // Right queries inwards, so columns must match for (; right - 1 > left; right--) // <- (exit if we reach the 'left' column: 1px wide) { for (int y = top; y <= bottom; y++) { if (data.Data[right - 1 + y * data.Width] != data.Data[right + y * data.Width]) { goto foundRight; } } } foundRight: return(new Rectangle(left + data.OffsetX, top + data.OffsetY, right - left + 1, bottom - top + 1)); }
// TODO: Factor this out such that frames can be appended on-the-fly (for capturing "animated screenshots"). // // May want to do threading (outside this class). Could thread the following: // - Pulling GPU surfaces (want to return surfaces quikcly) // - GIF encoding (maybe fast enough to combine, rather than take buffer re-cache hit and thread costs) // - File I/O (want to avoid blocking useful work while writing to file, and built-in async allocates, uses a thread pool anyway??) // Debatable about what, if anything, to combine. public void WriteAnimation(Animation animation) { if (animation == null) { throw new ArgumentNullException("animation"); } if (animation.FrameCount == 0) { return; } if (stream == null) { throw new InvalidOperationException("No stream set"); } Rectangle animationBounds = animation.GetSoftRenderBounds(true); Position accumulatedGameplayMotion = Position.Zero; bool animated = animation.FrameCount > 1; // Preconditions: if (animationBounds.Width > ushort.MaxValue || animationBounds.Height > ushort.MaxValue) { return; } bool transparency = true; // GIF: Header { WriteBuffer(gifHeader); } // GIF: Logical Screen Descriptor { WriteUShort((ushort)animationBounds.Width); WriteUShort((ushort)animationBounds.Height); byte packed = 0; packed |= 1 << 7; // Global colour table present packed |= 7 << 4; // Bits-per-channel minus 1, in source data packed |= 0 << 3; // Is GCT priority-sorted? packed |= 7 << 0; // Global colour table size = 2^(N+1) WriteByte(packed); WriteByte(0); // Background colour index into GCT (or 0 if not present) WriteByte(0); // Pixel aspect ratio, or 0 for "not-specified" } // GIF: Global Color Table // TODO: Consider using pre-built NES palette? (Especially for game export) // TODO: Add support for local color tables // Reserve and clear space in output buffer: const int colorTableBytes = 256 * 3; // 256 RGB Colors int gctStart = bufferPosition; EnsureBufferSpace(colorTableBytes); bufferPosition += colorTableBytes; Array.Clear(buffer, gctStart, colorTableBytes); int gctCount = 1; // <- first value is always pure black, and transparent if transparency is enabled // GIF: NETSCAPE2.0 Application Extension Block if (animated) { WriteBuffer(netscapeBlock); } foreach (var frame in animation.Frames) { var data = frame.SoftRender(); // HACK: If the image has zero size (blank, cropped to nothing), replace it with 1px of transparency // (Because I'm not fiddling around trying to figure out how to encode a zero-sized frame, or how compatible that is, right now -AR) if (data.Width == 0 || data.Height == 0) { data = new Data2D <Color>(animationBounds.X, animationBounds.Y, 1, 1); } accumulatedGameplayMotion += frame.positionDelta; Point motion = accumulatedGameplayMotion.ToWorldZero.FlipY(); // Convert to texture space data.OffsetX += motion.X; data.OffsetY += motion.Y; int delayTime = (frame.delay * 100) / 60; // Convert ticks at 60FPS to ticks at 100FPS (the GIF standard) // NOTE: Browsers do retarded things with timings of 0 or 1. Some even do stupid things with 2-5. // See http://nullsleep.tumblr.com/post/16524517190/animated-gif-minimum-frame-delay-browser for details delayTime = System.Math.Max(2, System.Math.Min(delayTime, ushort.MaxValue)); // GIF: Graphic Control Extension { WriteByte(0x21); // extension block WriteByte(0xF9); // graphics control label WriteByte(0x04); // block size in bytes (fixed) byte packed = 0; packed |= (byte)((animated ? 2u : 0u) << 2); // Disposal method (0 = none specified, 1 = do not clear, 2 = clear to BG) packed |= 0 << 1; // User input flag packed |= (byte)((transparency ? 1u : 0) << 0); // Transparent colour flag WriteByte(packed); WriteUShort((ushort)(animated ? delayTime : 0)); // Delay time WriteByte(0); // Transparent colour index // TODO: Put this outside the table, so that non-transparent images don't have fixed black entry in palette. WriteByte(0); // Block terminator } // GIF: Image descriptor { WriteByte(0x2C); // Image separator WriteUShort((ushort)(data.OffsetX - animationBounds.X)); // Image left WriteUShort((ushort)(data.OffsetY - animationBounds.Y)); // Image top WriteUShort((ushort)data.Width); // Image width WriteUShort((ushort)data.Height); // Image height byte packed = 0; packed |= 0 << 7; // Local colour table packed |= 0 << 6; // Interlace packed |= 0 << 5; // Is LCT priority sorted? packed |= 0 << 0; // Local colour table size = 2^(N+1) WriteByte(packed); // NOTE: Local colour table would folow if specified } // GIF: Image Data (with LZW compression) { WriteByte(8); // LZW minimum code size (hard-coding for 256-colour images for now) WriteByte(0); // Placeholder for chunk size PendingBitsAndAndChunks pending = new PendingBitsAndAndChunks(); // Code table is a series of unbalanced binary trees of available suffixes, one for each prefix LZWTableEntry[] codeTable; ushort[] treeRoots; if (codeTableStorage == null) { codeTable = codeTableStorage = new LZWTableEntry[4096 - codeStart]; // maximum 12-bit codes treeRoots = treeRootsStorage = new ushort[4096]; } else { codeTable = codeTableStorage; treeRoots = treeRootsStorage; Array.Clear(treeRoots, 0, treeRoots.Length); } int codeCount = 0; // <- number of used entries in the (stored) code table int codeBitsUsed = 9; // Always start with clear code: WriteVariableBitsChunked(clearCode, codeBitsUsed, ref pending); // Setup active prefix: uint activePrefix = (uint)GetColorTableIndex(buffer, gctStart, ref gctCount, data.Data[0], transparency); Debug.Assert(activePrefix < 256); // Loop through remaining indicies: int imageSize = data.Width * data.Height; for (int i = 1; i < imageSize; i++) { uint activeSuffix = (uint)GetColorTableIndex(buffer, gctStart, ref gctCount, data.Data[i], transparency); Debug.Assert(activeSuffix < 256); uint lastTreePosition = 0; // <- sentinal value for the root uint foundSuffix = 0; // Search for matching entry: { uint treePosition = treeRoots[activePrefix]; while (treePosition != 0) { lastTreePosition = treePosition; foundSuffix = codeTable[treePosition - codeStart].SuffixValue; if (activeSuffix == foundSuffix) { // FOUND: activePrefix = treePosition; goto nextIndex; } else if (activeSuffix < foundSuffix) { treePosition = codeTable[treePosition - codeStart].LowerIndex; } else // activeSuffix > foundSuffix { treePosition = codeTable[treePosition - codeStart].HigherIndex; } } } // NOT FOUND: // Write the code we do know about WriteVariableBitsChunked(activePrefix, codeBitsUsed, ref pending); // If we fill up the code table, reset it if (codeCount == codeTable.Length) { WriteVariableBitsChunked(clearCode, codeBitsUsed, ref pending); Array.Clear(treeRoots, 0, treeRoots.Length); codeCount = 0; codeBitsUsed = 9; } else // Add to the code table { // Insert into unbalanced binary tree (prefix index is implicit in the tree itself) if (lastTreePosition == 0) { treeRoots[activePrefix] = (ushort)(codeStart + codeCount); } else if (activeSuffix < foundSuffix) { codeTable[lastTreePosition - codeStart].LowerIndex = (uint)(codeStart + codeCount); } else // activeSuffix > foundSuffix { codeTable[lastTreePosition - codeStart].HigherIndex = (uint)(codeStart + codeCount); } // If the new code is past the maximum representable at this code width, increase the code width if ((codeStart + codeCount) == (1 << codeBitsUsed)) { codeBitsUsed++; // NOTE: GIF does *not* do an early change of the code-width } // Fill table entry: codeTable[codeCount++] = new LZWTableEntry(activeSuffix); } // Active prefix has been written, start searching from the suffix activePrefix = activeSuffix; nextIndex :; } WriteVariableBitsChunked(activePrefix, codeBitsUsed, ref pending); WriteVariableBitsChunked(endCode, codeBitsUsed, ref pending); FinishVariableBitsChunked(ref pending); FinishChunked(ref pending); } } // GIF: Trailer WriteByte(0x3B); // Finished creating gif data at this point, write it out stream.Write(buffer, 0, bufferPosition); bufferPosition = 0; }