public ShadowReceiver(Heightmap heightmap, Oblique heightmapExtendDirection) { if (heightmap == null) { throw new ArgumentNullException("heightmap"); } this.heightmap = heightmap; this.heightmapExtendDirection = heightmapExtendDirection; }
public static int CalculateFlatPhysicsDepth(int physicsWidth, Oblique flatDirection) { // NOTE: This method should match calculation of flat Z-sort data if (flatDirection == Oblique.Straight) { return(0); } return(physicsWidth = System.Math.Min(physicsWidth, ((int)byte.MaxValue) + 1)); }
public void SetPhysicsForCharacter() { if (Heightmap != null) { return; } // Default values for a character (based on default CharacterPhysics) physicsStartX = defaultCharacterStartX; physicsEndX = defaultCharacterEndX; physicsStartZ = 0; physicsEndZ = 0; physicsHeight = defaultCharacterHeight; flatDirection = Oblique.Straight; RegenerateDepthBounds(); }
public static string ToFancyString(this Oblique oblique, bool straightMeansFlat = false) { switch (oblique) { case Oblique.Left: return("Left ↖"); case Oblique.Straight: return(straightMeansFlat ? "Flat ↑" : "Straight ↑"); case Oblique.Right: return("Right ↗"); default: throw new ArgumentOutOfRangeException("oblique"); } }
public void SetPhysicsForCarpet() { if (Heightmap != null) { return; } // Default values for a static prop if (HasDefaultAnimation) { var bounds = DefaultAnimation.CalculateGraphicsBounds(); physicsStartX = bounds.X; physicsEndX = bounds.X + bounds.Width; physicsStartZ = bounds.Y; physicsEndZ = bounds.Y + bounds.Height; physicsHeight = 0; // <- The magic. flatDirection = Oblique.Straight; // <- Arbitrary } RegenerateDepthBounds(); }
public void SetPhysicsForFlat() { if (Heightmap != null) { return; } // Default values for a static prop if (HasDefaultAnimation) { var bounds = DefaultAnimation.CalculateGraphicsBounds(); physicsStartX = bounds.X; physicsEndX = bounds.X + bounds.Width; physicsStartZ = 0; physicsEndZ = 0; physicsHeight = System.Math.Max(0, bounds.Y + bounds.Height); // Z-sort disregards anything below-ground flatDirection = Oblique.Straight; } RegenerateDepthBounds(); }
/// <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) } } } }
/// <summary>Deserialize into new object instance</summary> public ShadowReceiver(AnimationDeserializeContext context) { heightmap = new Heightmap(context); heightmapExtendDirection = context.br.ReadOblique(); }
public AnimationSet(AnimationDeserializeContext context) { friendlyName = context.br.ReadNullableString(); importOrigin = context.br.ReadPoint(); behaviour = context.br.ReadNullableString(); if (context.br.ReadBoolean()) { Heightmap = new Heightmap(context); } if (Heightmap != null) { physicsStartX = Heightmap.StartX; physicsEndX = Heightmap.EndX; physicsStartZ = Heightmap.StartZ; physicsEndZ = Heightmap.EndZ; // Assume that reading is faster than walking the heightmap: physicsHeight = context.br.ReadInt32(); if (physicsHeight > 0) { depthBounds = new DepthBounds(context); } flatDirection = context.br.ReadOblique(); // <- for the sake of editing, keep this value around } else { physicsStartX = context.br.ReadInt32(); physicsEndX = context.br.ReadInt32(); physicsStartZ = context.br.ReadInt32(); physicsHeight = context.br.ReadInt32(); flatDirection = context.br.ReadOblique(); if (physicsHeight == 0) { physicsEndZ = context.br.ReadInt32(); // physicsEndZ gets auto-set during regen, except for carpets } RegenerateDepthBounds(); // <- Know this is reasonably fast to generate } if (context.Version >= 38) { coplanarPriority = context.br.ReadInt32(); } if (context.Version >= 36) { doAboveCheck = context.br.ReadBoolean(); } if (context.br.ReadBoolean()) { Ceiling = new Heightmap(context); } animations = new TagLookup <Animation>(context, () => new Animation(context)); // Unused Animations { int count = context.br.ReadInt32(); if (count > 0) // unusedAnimations is lazy-initialized { unusedAnimations = new List <Animation>(count); for (int i = 0; i < count; i++) { unusedAnimations.Add(new Animation(context)); } } } cue = context.br.ReadNullableString(); // Shadow layers { int shadowLayerCount = context.br.ReadInt32(); if (shadowLayerCount <= 0) { shadowLayers = null; } else { shadowLayers = new List <ShadowLayer>(); for (int i = 0; i < shadowLayerCount; i++) { shadowLayers.Add(new ShadowLayer( context.br.ReadInt32(), new SpriteRef(context))); } cachedShadowBounds = context.br.ReadBounds(); } } // // FIX-UPS: // // TODO: Commenting this out and seeing if anything breaks (remove it eventually) -AR 7/7/16 //if(physicsStartX == physicsEndX) // <- Why do we have zero width? //{ // Debug.WriteLine("WARNING: AnimationSet \"" + friendlyName + "\" has zero physics width"); // AutoGeneratePhysicsAndDepthBounds(); //} }
public static void Write(this BinaryWriter bw, Oblique oblique) { bw.Write((sbyte)oblique); }
/// <summary>Create a new heightmap that extends its content (non-default heights) out in oblique directions (sutiable for creating shadow heightmaps)</summary> public Heightmap CreateExtendedOblique(Oblique oblique) { // Expand the region containing content in the specified oblique direction(s) #region ASCII Art... // // Given a heightmap that looks like this: // ________ // | /\ /\ | // |/ \/ \| // |\ /| // |/ \| // |\ /\ /| // |_\/__\/_| // // Expand it by projecting its content in the oblique direction: // ________________ // |. | /\ /\ | | // | . |/ \/ \| | // | .|\ /. | // | ./ \|. | // | |\ /\ /| . | // |___|_\/__\/_|__.| // #endregion int extendedLeft = StartX; int extendedRight = EndX - 1; // <- inclusive for (int z = StartZ; z < EndZ; z++) { for (int x = StartX; x < EndX; x++) { if (this[x, z] != DefaultHeight) // Has content to extend { int deltaFromFront = z - StartZ; // (positive) int deltaFromBack = z - (EndZ - 1); // (negative) int frontX = x - deltaFromFront * (int)oblique; int backX = x - deltaFromBack * (int)oblique; // Expand: if (frontX < extendedLeft) { extendedLeft = frontX; } if (frontX > extendedRight) { extendedRight = frontX; } if (backX < extendedLeft) { extendedLeft = backX; } if (backX > extendedRight) { extendedRight = backX; } } } } // HACK: To allow edges to have zero values when filled, without adding expansion to the Fill methods, // simply expand by one pixel here: // TODO: Allow the FillLeft/FillRight methods to automatically expand the size of the heightmap sideways if necessary // (So that zero values on the very edge aren't lost) extendedLeft -= 1; extendedRight += 1; Rectangle destinationBounds = new Rectangle(extendedLeft, StartZ, extendedRight - extendedLeft + 1, EndZ - StartZ); Heightmap destination = new Heightmap(DefaultHeight); destination.heightmapData = new Data2D <byte>(destinationBounds); // Copy while extending content for (int destinationZ = destination.StartZ; destinationZ < destination.EndZ; destinationZ++) { for (int destinationX = destination.StartX; destinationX < destination.EndX; destinationX++) { // We could add a bunch of early-outs here (but we don't care if this is a tad slow) int sourceX = destinationX, sourceZ = destinationZ; // Try and extend to find content: if (this[sourceX, sourceZ] == DefaultHeight) { // Search backwards to back of heightmap while (true) { sourceZ += 1; sourceX += (int)oblique; if (sourceZ >= this.EndZ) { goto searchForward; } if (this[sourceX, sourceZ] != DefaultHeight) { goto done; } } // Search forward to front of heightmap searchForward: sourceX = destinationX; sourceZ = destinationZ; while (true) { sourceZ -= 1; sourceX -= (int)oblique; if (sourceZ < this.StartZ) { goto fail; } if (this[sourceX, sourceZ] != DefaultHeight) { goto done; } } fail: sourceX = destinationX; sourceZ = destinationZ; done: ; } destination[destinationX, destinationZ] = this[sourceX, sourceZ]; } } return(destination); }
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++; } }
/// <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: ; } }
public int GetHeightByWalkingObliqueForward(MaskData maskData, int frontEdgeDepth, Oblique oblique, int x, int y) { // Try to walk forward in the mask to find the front edge, from which we have a specified depth, and can calculate the height while (true) { // NOTE: Don't need to do special handling of "upright" sections, because our caller works top-down through the image // (So these sections will be overwritten as appropriate) int nextX = x - (int)oblique; // Walk forward (down the mask) in the oblique direction int nextY = y - 1; if (!maskData.GetOrDefault(nextX, nextY)) // Reached the front of the mask data { return(y - frontEdgeDepth); } x = nextX; y = nextY; } }
/// <summary>Get the height at a given position in the heightmap, with special shadow indexing (does not treat default height as special)</summary> public int GetHeightForShadow(int startX, int endX, int z, Oblique extendDirection) { Debug.Assert(heightmap.HasData); // <- NOTE: Expect our caller to skip calculation if there is no data!! // Transform into heightmap space: int transformedStartX; int transformedEndX; if (!flipX) { transformedStartX = startX - position.X; transformedEndX = endX - position.X; } else { transformedStartX = -(endX - position.X - 1); transformedEndX = -(startX - position.X - 1); extendDirection = (Oblique)(-(int)extendDirection); } Debug.Assert(transformedStartX < transformedEndX); z -= position.Z; // Move into heightmap Z range in oblique direction if (z < heightmap.StartZ) { int distance = heightmap.StartZ - z; // (positive) distance *= (int)extendDirection; transformedStartX += distance; transformedEndX += distance; z = heightmap.StartZ; } else if (z >= heightmap.EndZ) { int distance = (heightmap.EndZ - 1) - z; // (negative) distance *= (int)extendDirection; transformedStartX += distance; transformedEndX += distance; z = heightmap.EndZ - 1; } // If we are outside the range, early-out: if (transformedEndX <= heightmap.StartX) { return(position.Y + heightmap.heightmapData[heightmap.StartX, z]); // Skips bounds check } if (transformedStartX >= (heightmap.EndX - 1)) { return(position.Y + heightmap.heightmapData[heightmap.EndX - 1, z]); // Skips bounds check } // Ensure we are clipped to the range: if (transformedStartX < heightmap.StartX) { transformedStartX = heightmap.StartX; } if (transformedEndX > heightmap.EndX) { transformedEndX = heightmap.EndX; } int maxHeight = 0; for (int x = transformedStartX; x < transformedEndX; x++) { int h = heightmap.heightmapData[x, z]; // Skips bounds check if (h > maxHeight) { maxHeight = h; } } return(position.Y + maxHeight); }
public AnimationSet(AnimationDeserializeContext context) { friendlyName = context.br.ReadNullableString(); importOrigin = context.br.ReadPoint(); behaviour = context.br.ReadNullableString(); if (context.br.ReadBoolean()) { Heightmap = new Heightmap(context); } if (Heightmap != null) { physicsStartX = Heightmap.StartX; physicsEndX = Heightmap.EndX; physicsStartZ = Heightmap.StartZ; physicsEndZ = Heightmap.EndZ; // Assume that reading is faster than walking the heightmap: physicsHeight = context.br.ReadInt32(); if (physicsHeight > 0) { depthBounds = new DepthBounds(context); } flatDirection = context.br.ReadOblique(); // <- for the sake of editing, keep this value around } else { physicsStartX = context.br.ReadInt32(); physicsEndX = context.br.ReadInt32(); physicsStartZ = context.br.ReadInt32(); physicsHeight = context.br.ReadInt32(); flatDirection = context.br.ReadOblique(); if (physicsHeight == 0) { physicsEndZ = context.br.ReadInt32(); // physicsEndZ gets auto-set during regen, except for carpets } RegenerateDepthBounds(); // <- Know this is reasonably fast to generate } if (context.Version >= 38) { coplanarPriority = context.br.ReadInt32(); } if (context.Version >= 36) { doAboveCheck = context.br.ReadBoolean(); } if (context.br.ReadBoolean()) { Ceiling = new Heightmap(context); } animations = context.DeserializeTagLookup(() => new Animation(context)); // Unused Animations { int count = context.br.ReadInt32(); if (count > 0) // unusedAnimations is lazy-initialized { unusedAnimations = new List <Animation>(count); for (var i = 0; i < count; i++) { unusedAnimations.Add(new Animation(context)); } } } cue = context.br.ReadNullableString(); // Shadow layers { int shadowLayerCount = context.br.ReadInt32(); if (shadowLayerCount <= 0) { shadowLayers = null; } else { shadowLayers = new List <ShadowLayer>(); for (var i = 0; i < shadowLayerCount; i++) { shadowLayers.Add(new ShadowLayer(context.br.ReadInt32(), new SpriteRef(context))); } cachedShadowBounds = context.br.ReadBounds(); } } // Properties: if (context.Version >= 40) { int count = context.br.ReadInt32(); for (int i = 0; i < count; i++) { properties.Add(context.br.ReadString(), context.br.ReadString()); } } }