ฉันหวังว่าบางคนสามารถอธิบายสิ่งนี้กับฉันได้เหมือนฉันอายุ 5 ขวบเพราะฉันดิ้นรนกับสิ่งนี้เป็นเวลาหลายชั่วโมงและไม่สามารถเข้าใจสิ่งที่ฉันทำผิดได้
ฉันเขียนCamera
ชั้นเรียนสำหรับเกม 2.5D ของฉัน ความตั้งใจคือการสนับสนุนโลกและพื้นที่หน้าจอเช่นนี้:
กล้องเป็นสีดำด้านขวา แกน + Z เลื่อนขึ้นด้านบนของภาพโดยให้ -Z ชี้ลง อย่างที่คุณเห็นทั้งอวกาศโลกและพื้นที่จอภาพมี (0, 0) ที่มุมบนซ้าย
ฉันเริ่มเขียนการทดสอบหน่วยเพื่อพิสูจน์ว่ากล้องของฉันทำงานได้ตามที่คาดไว้และนั่นคือสิ่งที่เริ่มแปลก ... การทดสอบของฉันพล็อตพิกัดในโลกมุมมองและพื้นที่หน้าจอ ในที่สุดฉันจะใช้การเปรียบเทียบภาพเพื่อยืนยันว่าถูกต้อง แต่สำหรับตอนนี้การทดสอบของฉันเพิ่งแสดงผลลัพธ์
ตรรกะการเรนเดอร์ใช้Camera.ViewMatrix
ในการแปลงพื้นที่ของโลกเพื่อดูพื้นที่และCamera.WorldPointToScreen
เพื่อแปลงพื้นที่โลกเป็นพื้นที่หน้าจอ
นี่คือตัวอย่างทดสอบ:
[Fact]
public void foo()
{
var camera = new Camera(new Viewport(0, 0, 250, 100));
DrawingVisual worldRender;
DrawingVisual viewRender;
DrawingVisual screenRender;
this.Render(camera, out worldRender, out viewRender, out screenRender, new Vector3(30, 0, 0), new Vector3(30, 40, 0));
this.ShowRenders(camera, worldRender, viewRender, screenRender);
}
และนี่คือสิ่งที่ปรากฏขึ้นเมื่อฉันรันการทดสอบนี้:
พื้นที่โลกดูโอเคแม้ว่าฉันสงสัยว่าแกน z กำลังเข้าสู่หน้าจอแทนที่จะเข้าหาผู้ชม
มุมมองพื้นที่ทำให้ฉันงุนงงอย่างสมบูรณ์ ฉันคาดหวังว่ากล้องจะอยู่ด้านบน (0, 0) และมองไปที่ศูนย์กลางของฉาก ดูเหมือนว่าแกน z นั้นจะผิดไปในทางที่ผิดและกล้องก็อยู่ในตำแหน่งตรงข้ามกับที่ฉันคาดไว้!
ฉันสงสัยว่าพื้นที่หน้าจอจะเป็นอีกสิ่งหนึ่งโดยสิ้นเชิง แต่ทุกคนสามารถอธิบายสิ่งที่ฉันทำผิดในCamera
ชั้นเรียนได้ไหม
UPDATE
ฉันทำให้ความคืบหน้าในแง่ของการได้รับสิ่งต่าง ๆ ให้มองเห็นตามที่ฉันคาดหวัง แต่ผ่านสัญชาตญาณ: ไม่ใช่ความเข้าใจที่แท้จริงของสิ่งที่ฉันทำ การตรัสรู้ใด ๆ จะได้รับการชื่นชมอย่างมาก
ฉันรู้ว่าพื้นที่การดูของฉันพลิกทั้งในแนวตั้งและแนวนอนเมื่อเทียบกับที่ฉันคาดไว้ดังนั้นฉันจึงเปลี่ยนมุมมองเมทริกซ์เพื่อปรับขนาดตาม:
this.viewMatrix = Matrix.CreateLookAt(this.location, this.target, this.up) *
Matrix.CreateScale(this.zoom, this.zoom, 1) *
Matrix.CreateScale(-1, -1, 1);
ฉันสามารถรวมCreateScale
สายสองสายได้ แต่แยกสายไว้เพื่อความชัดเจน อีกครั้งฉันไม่รู้ว่าทำไมสิ่งนี้ถึงมีความจำเป็น แต่มันทำให้พื้นที่การดูของฉันคงที่:
แต่ตอนนี้พื้นที่หน้าจอของฉันต้องพลิกในแนวตั้งดังนั้นฉันจึงปรับเปลี่ยนเมทริกซ์การฉายตาม:
this.projectionMatrix = Matrix.CreatePerspectiveFieldOfView(0.7853982f, viewport.AspectRatio, 1, 2)
* Matrix.CreateScale(1, -1, 1);
และผลลัพธ์นี้เป็นสิ่งที่ฉันคาดหวังจากความพยายามครั้งแรก:
ฉันเพิ่งลองใช้Camera
เพื่อแสดงสไปรต์ผ่านทางSpriteBatch
เพื่อให้แน่ใจว่าทุกอย่างทำงานได้ที่นั่นเช่นกันและทำเช่นนั้น
แต่คำถามยังคงอยู่: ทำไมฉันต้องทำทุกอย่างเพื่อให้ได้พิกัดอวกาศตามที่ฉันคาดไว้
อัพเดท 2
ฉันได้ปรับปรุงตรรกะการเรนเดอร์ของฉันในชุดการทดสอบของฉันเพื่อให้รองรับรูปทรงเรขาคณิตและเพื่อให้บรรทัดมีน้ำหนักเบายิ่งห่างจากกล้องมากขึ้น ฉันต้องการทำเช่นนี้เพื่อหลีกเลี่ยงภาพลวงตาและเพื่อพิสูจน์ตัวเองต่อไปว่าฉันกำลังดูสิ่งที่ฉันคิดว่าฉันเป็น
นี่คือตัวอย่าง:
ในกรณีนี้ฉันมีรูปทรง 3 แบบคือลูกบาศก์ทรงกลมและรูปหลายเหลี่ยมที่ด้านบนสุดของลูกบาศก์ ขอให้สังเกตว่าการปรับความเข้มและการลดแสงของเส้นระบุส่วนต่างๆของรูปทรงเรขาคณิตให้ใกล้กับกล้องมากขึ้นอย่างไร
หากฉันลบมาตราส่วนเชิงลบที่ฉันต้องใส่เข้าไปฉันจะเห็น:
ดังนั้นคุณจะเห็นว่าฉันยังอยู่ในเรือลำเดียวกัน - ฉันยังต้องการการพลิกแนวตั้งและแนวนอนในเมทริกซ์เพื่อให้สิ่งต่าง ๆ ปรากฏอย่างถูกต้อง
ในความสนใจของการให้คนเล่นซ้ำกับที่นี่เป็นรหัสที่สมบูรณ์ที่จำเป็นในการสร้างข้างต้น หากคุณต้องการเรียกใช้ผ่านชุดทดสอบทดสอบเพียงติดตั้งแพ็คเกจ xunit:
Camera.cs :
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System.Diagnostics;
public sealed class Camera
{
private readonly Viewport viewport;
private readonly Matrix projectionMatrix;
private Matrix? viewMatrix;
private Vector3 location;
private Vector3 target;
private Vector3 up;
private float zoom;
public Camera(Viewport viewport)
{
this.viewport = viewport;
// for an explanation of the negative scaling, see: http://gamedev.stackexchange.com/questions/63409/
this.projectionMatrix = Matrix.CreatePerspectiveFieldOfView(0.7853982f, viewport.AspectRatio, 1, 2)
* Matrix.CreateScale(1, -1, 1);
// defaults
this.location = new Vector3(this.viewport.Width / 2, this.viewport.Height, 100);
this.target = new Vector3(this.viewport.Width / 2, this.viewport.Height / 2, 0);
this.up = new Vector3(0, 0, 1);
this.zoom = 1;
}
public Viewport Viewport
{
get { return this.viewport; }
}
public Vector3 Location
{
get { return this.location; }
set
{
this.location = value;
this.viewMatrix = null;
}
}
public Vector3 Target
{
get { return this.target; }
set
{
this.target = value;
this.viewMatrix = null;
}
}
public Vector3 Up
{
get { return this.up; }
set
{
this.up = value;
this.viewMatrix = null;
}
}
public float Zoom
{
get { return this.zoom; }
set
{
this.zoom = value;
this.viewMatrix = null;
}
}
public Matrix ProjectionMatrix
{
get { return this.projectionMatrix; }
}
public Matrix ViewMatrix
{
get
{
if (this.viewMatrix == null)
{
// for an explanation of the negative scaling, see: http://gamedev.stackexchange.com/questions/63409/
this.viewMatrix = Matrix.CreateLookAt(this.location, this.target, this.up) *
Matrix.CreateScale(this.zoom) *
Matrix.CreateScale(-1, -1, 1);
}
return this.viewMatrix.Value;
}
}
public Vector2 WorldPointToScreen(Vector3 point)
{
var result = viewport.Project(point, this.ProjectionMatrix, this.ViewMatrix, Matrix.Identity);
return new Vector2(result.X, result.Y);
}
public void WorldPointsToScreen(Vector3[] points, Vector2[] destination)
{
Debug.Assert(points != null);
Debug.Assert(destination != null);
Debug.Assert(points.Length == destination.Length);
for (var i = 0; i < points.Length; ++i)
{
destination[i] = this.WorldPointToScreen(points[i]);
}
}
}
CameraFixture.cs :
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using Xunit;
using XNA = Microsoft.Xna.Framework;
public sealed class CameraFixture
{
[Fact]
public void foo()
{
var camera = new Camera(new Viewport(0, 0, 250, 100));
DrawingVisual worldRender;
DrawingVisual viewRender;
DrawingVisual screenRender;
this.Render(
camera,
out worldRender,
out viewRender,
out screenRender,
new Sphere(30, 15) { WorldMatrix = XNA.Matrix.CreateTranslation(155, 50, 0) },
new Cube(30) { WorldMatrix = XNA.Matrix.CreateTranslation(75, 60, 15) },
new PolyLine(new XNA.Vector3(0, 0, 0), new XNA.Vector3(10, 10, 0), new XNA.Vector3(20, 0, 0), new XNA.Vector3(0, 0, 0)) { WorldMatrix = XNA.Matrix.CreateTranslation(65, 55, 30) });
this.ShowRenders(worldRender, viewRender, screenRender);
}
#region Supporting Fields
private static readonly Pen xAxisPen = new Pen(Brushes.Red, 2);
private static readonly Pen yAxisPen = new Pen(Brushes.Green, 2);
private static readonly Pen zAxisPen = new Pen(Brushes.Blue, 2);
private static readonly Pen viewportPen = new Pen(Brushes.Gray, 1);
private static readonly Pen nonScreenSpacePen = new Pen(Brushes.Black, 0.5);
private static readonly Color geometryBaseColor = Colors.Black;
#endregion
#region Supporting Methods
private void Render(Camera camera, out DrawingVisual worldRender, out DrawingVisual viewRender, out DrawingVisual screenRender, params Geometry[] geometries)
{
var worldDrawingVisual = new DrawingVisual();
var viewDrawingVisual = new DrawingVisual();
var screenDrawingVisual = new DrawingVisual();
const int axisLength = 15;
using (var worldDrawingContext = worldDrawingVisual.RenderOpen())
using (var viewDrawingContext = viewDrawingVisual.RenderOpen())
using (var screenDrawingContext = screenDrawingVisual.RenderOpen())
{
// draw lines around the camera's viewport
var viewportBounds = camera.Viewport.Bounds;
var viewportLines = new Tuple<int, int, int, int>[]
{
Tuple.Create(viewportBounds.Left, viewportBounds.Bottom, viewportBounds.Left, viewportBounds.Top),
Tuple.Create(viewportBounds.Left, viewportBounds.Top, viewportBounds.Right, viewportBounds.Top),
Tuple.Create(viewportBounds.Right, viewportBounds.Top, viewportBounds.Right, viewportBounds.Bottom),
Tuple.Create(viewportBounds.Right, viewportBounds.Bottom, viewportBounds.Left, viewportBounds.Bottom)
};
foreach (var viewportLine in viewportLines)
{
var viewStart = XNA.Vector3.Transform(new XNA.Vector3(viewportLine.Item1, viewportLine.Item2, 0), camera.ViewMatrix);
var viewEnd = XNA.Vector3.Transform(new XNA.Vector3(viewportLine.Item3, viewportLine.Item4, 0), camera.ViewMatrix);
var screenStart = camera.WorldPointToScreen(new XNA.Vector3(viewportLine.Item1, viewportLine.Item2, 0));
var screenEnd = camera.WorldPointToScreen(new XNA.Vector3(viewportLine.Item3, viewportLine.Item4, 0));
worldDrawingContext.DrawLine(viewportPen, new Point(viewportLine.Item1, viewportLine.Item2), new Point(viewportLine.Item3, viewportLine.Item4));
viewDrawingContext.DrawLine(viewportPen, new Point(viewStart.X, viewStart.Y), new Point(viewEnd.X, viewEnd.Y));
screenDrawingContext.DrawLine(viewportPen, new Point(screenStart.X, screenStart.Y), new Point(screenEnd.X, screenEnd.Y));
}
// draw axes
var axisLines = new Tuple<int, int, int, int, int, int, Pen>[]
{
Tuple.Create(0, 0, 0, axisLength, 0, 0, xAxisPen),
Tuple.Create(0, 0, 0, 0, axisLength, 0, yAxisPen),
Tuple.Create(0, 0, 0, 0, 0, axisLength, zAxisPen)
};
foreach (var axisLine in axisLines)
{
var viewStart = XNA.Vector3.Transform(new XNA.Vector3(axisLine.Item1, axisLine.Item2, axisLine.Item3), camera.ViewMatrix);
var viewEnd = XNA.Vector3.Transform(new XNA.Vector3(axisLine.Item4, axisLine.Item5, axisLine.Item6), camera.ViewMatrix);
var screenStart = camera.WorldPointToScreen(new XNA.Vector3(axisLine.Item1, axisLine.Item2, axisLine.Item3));
var screenEnd = camera.WorldPointToScreen(new XNA.Vector3(axisLine.Item4, axisLine.Item5, axisLine.Item6));
worldDrawingContext.DrawLine(axisLine.Item7, new Point(axisLine.Item1, axisLine.Item2), new Point(axisLine.Item4, axisLine.Item5));
viewDrawingContext.DrawLine(axisLine.Item7, new Point(viewStart.X, viewStart.Y), new Point(viewEnd.X, viewEnd.Y));
screenDrawingContext.DrawLine(axisLine.Item7, new Point(screenStart.X, screenStart.Y), new Point(screenEnd.X, screenEnd.Y));
}
// for all points in all geometries to be rendered, find the closest and furthest away from the camera so we can lighten lines that are further away
var distancesToAllGeometrySections = from geometry in geometries
let geometryViewMatrix = geometry.WorldMatrix * camera.ViewMatrix
from section in geometry.Sections
from point in new XNA.Vector3[] { section.Item1, section.Item2 }
let viewPoint = XNA.Vector3.Transform(point, geometryViewMatrix)
select viewPoint.Length();
var furthestDistance = distancesToAllGeometrySections.Max();
var closestDistance = distancesToAllGeometrySections.Min();
var deltaDistance = Math.Max(0.000001f, furthestDistance - closestDistance);
// draw each geometry
for (var i = 0; i < geometries.Length; ++i)
{
var geometry = geometries[i];
// there's probably a more correct name for this, but basically this gets the geometry relative to the camera so we can check how far away each point is from the camera
var geometryViewMatrix = geometry.WorldMatrix * camera.ViewMatrix;
// we order roughly by those sections furthest from the camera to those closest, so that the closer ones "overwrite" the ones further away
var orderedSections = from section in geometry.Sections
let startPointRelativeToCamera = XNA.Vector3.Transform(section.Item1, geometryViewMatrix)
let endPointRelativeToCamera = XNA.Vector3.Transform(section.Item2, geometryViewMatrix)
let startPointDistance = startPointRelativeToCamera.Length()
let endPointDistance = endPointRelativeToCamera.Length()
orderby (startPointDistance + endPointDistance) descending
select new { Section = section, DistanceToStart = startPointDistance, DistanceToEnd = endPointDistance };
foreach (var orderedSection in orderedSections)
{
var start = XNA.Vector3.Transform(orderedSection.Section.Item1, geometry.WorldMatrix);
var end = XNA.Vector3.Transform(orderedSection.Section.Item2, geometry.WorldMatrix);
var viewStart = XNA.Vector3.Transform(start, camera.ViewMatrix);
var viewEnd = XNA.Vector3.Transform(end, camera.ViewMatrix);
worldDrawingContext.DrawLine(nonScreenSpacePen, new Point(start.X, start.Y), new Point(end.X, end.Y));
viewDrawingContext.DrawLine(nonScreenSpacePen, new Point(viewStart.X, viewStart.Y), new Point(viewEnd.X, viewEnd.Y));
// screen rendering is more complicated purely because I wanted geometry to fade the further away it is from the camera
// otherwise, it's very hard to tell whether the rendering is actually correct or not
var startDistanceRatio = (orderedSection.DistanceToStart - closestDistance) / deltaDistance;
var endDistanceRatio = (orderedSection.DistanceToEnd - closestDistance) / deltaDistance;
// lerp towards white based on distance from camera, but only to a maximum of 90%
var startColor = Lerp(geometryBaseColor, Colors.White, startDistanceRatio * 0.9f);
var endColor = Lerp(geometryBaseColor, Colors.White, endDistanceRatio * 0.9f);
var screenStart = camera.WorldPointToScreen(start);
var screenEnd = camera.WorldPointToScreen(end);
var brush = new LinearGradientBrush
{
StartPoint = new Point(screenStart.X, screenStart.Y),
EndPoint = new Point(screenEnd.X, screenEnd.Y),
MappingMode = BrushMappingMode.Absolute
};
brush.GradientStops.Add(new GradientStop(startColor, 0));
brush.GradientStops.Add(new GradientStop(endColor, 1));
var pen = new Pen(brush, 1);
brush.Freeze();
pen.Freeze();
screenDrawingContext.DrawLine(pen, new Point(screenStart.X, screenStart.Y), new Point(screenEnd.X, screenEnd.Y));
}
}
}
worldRender = worldDrawingVisual;
viewRender = viewDrawingVisual;
screenRender = screenDrawingVisual;
}
private static float Lerp(float start, float end, float amount)
{
var difference = end - start;
var adjusted = difference * amount;
return start + adjusted;
}
private static Color Lerp(Color color, Color to, float amount)
{
var sr = color.R;
var sg = color.G;
var sb = color.B;
var er = to.R;
var eg = to.G;
var eb = to.B;
var r = (byte)Lerp(sr, er, amount);
var g = (byte)Lerp(sg, eg, amount);
var b = (byte)Lerp(sb, eb, amount);
return Color.FromArgb(255, r, g, b);
}
private void ShowRenders(DrawingVisual worldRender, DrawingVisual viewRender, DrawingVisual screenRender)
{
var itemsControl = new ItemsControl();
itemsControl.Items.Add(new HeaderedContentControl { Header = "World", Content = new DrawingVisualHost(worldRender)});
itemsControl.Items.Add(new HeaderedContentControl { Header = "View", Content = new DrawingVisualHost(viewRender) });
itemsControl.Items.Add(new HeaderedContentControl { Header = "Screen", Content = new DrawingVisualHost(screenRender) });
var window = new Window
{
Title = "Renders",
Content = itemsControl,
ShowInTaskbar = true,
SizeToContent = SizeToContent.WidthAndHeight
};
window.ShowDialog();
}
#endregion
#region Supporting Types
// stupidly simple 3D geometry class, consisting of a series of sections that will be connected by lines
private abstract class Geometry
{
public abstract IEnumerable<Tuple<XNA.Vector3, XNA.Vector3>> Sections
{
get;
}
public XNA.Matrix WorldMatrix
{
get;
set;
}
}
private sealed class Line : Geometry
{
private readonly XNA.Vector3 magnitude;
public Line(XNA.Vector3 magnitude)
{
this.magnitude = magnitude;
}
public override IEnumerable<Tuple<XNA.Vector3, XNA.Vector3>> Sections
{
get
{
yield return Tuple.Create(XNA.Vector3.Zero, this.magnitude);
}
}
}
private sealed class PolyLine : Geometry
{
private readonly XNA.Vector3[] points;
public PolyLine(params XNA.Vector3[] points)
{
this.points = points;
}
public override IEnumerable<Tuple<XNA.Vector3, XNA.Vector3>> Sections
{
get
{
if (this.points.Length < 2)
{
yield break;
}
var end = this.points[0];
for (var i = 1; i < this.points.Length; ++i)
{
var start = end;
end = this.points[i];
yield return Tuple.Create(start, end);
}
}
}
}
private sealed class Cube : Geometry
{
private readonly float size;
public Cube(float size)
{
this.size = size;
}
public override IEnumerable<Tuple<XNA.Vector3, XNA.Vector3>> Sections
{
get
{
var halfSize = this.size / 2;
var frontBottomLeft = new XNA.Vector3(-halfSize, halfSize, -halfSize);
var frontBottomRight = new XNA.Vector3(halfSize, halfSize, -halfSize);
var frontTopLeft = new XNA.Vector3(-halfSize, halfSize, halfSize);
var frontTopRight = new XNA.Vector3(halfSize, halfSize, halfSize);
var backBottomLeft = new XNA.Vector3(-halfSize, -halfSize, -halfSize);
var backBottomRight = new XNA.Vector3(halfSize, -halfSize, -halfSize);
var backTopLeft = new XNA.Vector3(-halfSize, -halfSize, halfSize);
var backTopRight = new XNA.Vector3(halfSize, -halfSize, halfSize);
// front face
yield return Tuple.Create(frontBottomLeft, frontBottomRight);
yield return Tuple.Create(frontBottomLeft, frontTopLeft);
yield return Tuple.Create(frontTopLeft, frontTopRight);
yield return Tuple.Create(frontTopRight, frontBottomRight);
// left face
yield return Tuple.Create(frontTopLeft, backTopLeft);
yield return Tuple.Create(backTopLeft, backBottomLeft);
yield return Tuple.Create(backBottomLeft, frontBottomLeft);
// right face
yield return Tuple.Create(frontTopRight, backTopRight);
yield return Tuple.Create(backTopRight, backBottomRight);
yield return Tuple.Create(backBottomRight, frontBottomRight);
// back face
yield return Tuple.Create(backBottomLeft, backBottomRight);
yield return Tuple.Create(backTopLeft, backTopRight);
}
}
}
private sealed class Sphere : Geometry
{
private readonly float radius;
private readonly int subsections;
public Sphere(float radius, int subsections)
{
this.radius = radius;
this.subsections = subsections;
}
public override IEnumerable<Tuple<XNA.Vector3, XNA.Vector3>> Sections
{
get
{
var latitudeLines = this.subsections;
var longitudeLines = this.subsections;
// see http://stackoverflow.com/a/4082020/5380
var results = from latitudeLine in Enumerable.Range(0, latitudeLines)
from longitudeLine in Enumerable.Range(0, longitudeLines)
let latitudeRatio = latitudeLine / (float)latitudeLines
let longitudeRatio = longitudeLine / (float)longitudeLines
let nextLatitudeRatio = (latitudeLine + 1) / (float)latitudeLines
let nextLongitudeRatio = (longitudeLine + 1) / (float)longitudeLines
let z1 = Math.Cos(Math.PI * latitudeRatio)
let z2 = Math.Cos(Math.PI * nextLatitudeRatio)
let x1 = Math.Sin(Math.PI * latitudeRatio) * Math.Cos(Math.PI * 2 * longitudeRatio)
let y1 = Math.Sin(Math.PI * latitudeRatio) * Math.Sin(Math.PI * 2 * longitudeRatio)
let x2 = Math.Sin(Math.PI * nextLatitudeRatio) * Math.Cos(Math.PI * 2 * longitudeRatio)
let y2 = Math.Sin(Math.PI * nextLatitudeRatio) * Math.Sin(Math.PI * 2 * longitudeRatio)
let x3 = Math.Sin(Math.PI * latitudeRatio) * Math.Cos(Math.PI * 2 * nextLongitudeRatio)
let y3 = Math.Sin(Math.PI * latitudeRatio) * Math.Sin(Math.PI * 2 * nextLongitudeRatio)
let start = new XNA.Vector3((float)x1 * radius, (float)y1 * radius, (float)z1 * radius)
let firstEnd = new XNA.Vector3((float)x2 * radius, (float)y2 * radius, (float)z2 * radius)
let secondEnd = new XNA.Vector3((float)x3 * radius, (float)y3 * radius, (float)z1 * radius)
select new { First = Tuple.Create(start, firstEnd), Second = Tuple.Create(start, secondEnd) };
foreach (var result in results)
{
yield return result.First;
yield return result.Second;
}
}
}
}
#endregion
}
Viewport.Project
ต้องใช้เมทริกซ์โลก ดังนั้นฉันจึงเพิ่มเมทริกซ์ของโลกลงใน API ของฉัน อาจเป็นได้ว่าฉันต้องลบมันออกถ้าจำเป็น