이번에는 C#에서 3D 관련 기능을 구현할 때 사용하는 것들을 아는 선에서 정리해보겠습니다.
아래에서 나타나는 모든 클래스는 WPF의 xaml, cs파일에서 둘 다 선언해서 사용 가능합니다.
또한 예시 코드들 중 (변수명 = new 클래스();) 형태인 것들은 전역 변수로 생성해놓은 것으로 이해하시면 됩니다.
해당 글에 나오는 예시 데이터는 xaml과 cs코드입니다.
* Viewport3D
- 매우매우 중요한 클래스 입니다. 이 클래스에 카메라 정보, 3D 도형 정보등의 모든 데이터를 집어넣어야하기 때문입니다.
일종의 캔버스라고 생각하시면 될 것 같습니다.
Viewport3D viewport3D = new Viewport3D();
선언은 cs파일에서 코드로 하게 될 경우 위와 같이 하면 되고, 아래처럼 xaml 파일에서도 생성 가능합니다.
예시에는 Grid 안에 넣어주었지만 실제로 사용시에는 Grid 바깥으로 빼도 상관없습니다.
그러나 버튼 등의 다른 컴포넌트들과 같이 사용할려면 Grid나 Panel처럼 틀 안에 넣어놓고 사용하는게 편할 것 입니다.
<Grid Grid.Row="1" Name="grid3D">
<Viewport3D x:Name="viewPort3D">
</Viewport3D>
</Grid>
Viewport3D안에 데이터를 추가하려면 아래와 같이 하면 됩니다.
예시에는 ModelVisual3D 클래스의 변수를 적용한 상태입니다.
viewport3D.Children.Add(modelVisual3DRoot);
* PerspectiveCamera
- Viewport3D 클래스의 카메라 상태(화면 상태)를 제어하는 클래스입니다.
- 가장 많이 쓰는 속성은 Position, LookDirection, UpDirection 3가지입니다.
- 아래는 PerspectiveCamera 데이터의 예시입니다.
Name이 있고, FieldOfView가 있는데 FieldOfView 속성은 확대, 축소와 관련된 속성 값 입니다.
<PerspectiveCamera x:Name="Cam" FieldOfView="50.5496895978667" Position="-131.421390982859, 125.053676440019, 3639.80841756678" LookDirection="0.000900214550631487, 0.171926743517843, -0.985109326154774" UpDirection="0.00515799680174048, 0.985095822519873, 0.171929100279409" />
아래는 사용 예시입니다. content는 Camera의 속성 값이 string 형태로 되어있다는 가정하에 설정한 것 입니다.
PerspectiveCamera 클래스로 변수를 하나 생성하고 해당 변수에 값을 설정해서 Viewport3D의 Camera 속성에 적용시키는 코드입니다.
PerspectiveCamera camera = new PerspectiveCamera();
camera.FieldOfView = Convert.ToDouble(content.Value);
string[] position = content.Value.Split(',');
camera.Position = new Point3D(Convert.ToDouble(position[0]), Convert.ToDouble(position[1]), Convert.ToDouble(position[2]));
string[] lookDirection = content.Value.Split(',');
camera.LookDirection = new Vector3D(Convert.ToDouble(lookPosition[0]), Convert.ToDouble(lookPosition[1]), Convert.ToDouble(lookPosition[2]));
string[] upDirection = content.Value.Split(',');
camera.UpDirection = new Vector3D(Convert.ToDouble(upDirection[0]), Convert.ToDouble(upDirection[1]), Convert.ToDouble(upDirection[2]));
viewport3D.Camera = camera;
* ModelVisual3D
- 대표적으로 ModelGroup3D, Model3D 등의 클래스를 Content 속성에 담아서 사용할 수 있는 클래스입니다.
- Transform 속성을 설정 가능합니다.
- 간단한 사용 예시는 최종 적용 부분에 나와있으며 생성 예시는 아래와 같습니다.
ModelVisual3D modelVisual3D = new ModelVisual3D();
* Transform3DGroup
- RotateTransform3D 등의 데이터를 그룹으로 묶어서 사용할 수 있게 하는 클래스입니다.
- 아래는 예시 데이터입니다.
<Transform3DGroup>
<RotateTransform3D CenterX="0" CenterY="0" CenterZ="0">
<RotateTransform3D.Rotation>
<AxisAngleRotation3D x:Name="rotateX" Axis="1 0 0" Angle="{Binding ElementName=xMod, Path=Value}" />
</RotateTransform3D.Rotation>
</RotateTransform3D>
<RotateTransform3D CenterX="0" CenterY="0" CenterZ="0">
<RotateTransform3D.Rotation>
<AxisAngleRotation3D x:Name="rotateY" Axis="0 1 0" Angle="{Binding ElementName=yMod, Path=Value}" />
</RotateTransform3D.Rotation>
</RotateTransform3D>
<RotateTransform3D CenterX="0" CenterY="0" CenterZ="0">
<RotateTransform3D.Rotation>
<AxisAngleRotation3D x:Name="rotateZ" Axis="0 0 1" Angle="{Binding ElementName=zMod, Path=Value}" />
</RotateTransform3D.Rotation>
</RotateTransform3D>
</Transform3DGroup>
- 사용 예시는 아래의 RotateTransform3D와 같이 있습니다.
* RotateTransform3D
- ModelVisual3D의 Transform 속성에 적용할 수 있는 클래스입니다.
- 회전할 때의 중심점을 지정하고 중심점을 기준으로 회전 각도를 지정하는 역할을 합니다.
- 아래는 예시 데이터입니다.
<RotateTransform3D CenterX="0" CenterY="0" CenterZ="0">
<RotateTransform3D.Rotation>
<AxisAngleRotation3D x:Name="rotateX" Axis="1 0 0" Angle="{Binding ElementName=xMod, Path=Value}" />
</RotateTransform3D.Rotation>
</RotateTransform3D>
- 아래는 사용 예시입니다.
rotateTransform3D = new RotateTransform3D();
rotateTransform3D.CenterX = 0;
rotateTransform3D.CenterY = 0;
rotateTransform3D.CenterZ = 0;
if (content.Name == "rotateX")
{
axisAngleRotation3DX = new AxisAngleRotation3D(new Vector3D(1, 0, 0), 1);
rotateTransform3D.Rotation = axisAngleRotation3DX;
}
else if (content.Name == "rotateY")
{
axisAngleRotation3DY = new AxisAngleRotation3D(new Vector3D(0, 1, 0), 1);
rotateTransform3D.Rotation = axisAngleRotation3DY;
}
else if (content.Name == "rotateZ")
{
axisAngleRotation3DZ = new AxisAngleRotation3D(new Vector3D(0, 0, 1), 1);
rotateTransform3D.Rotation = axisAngleRotation3DZ;
}
transform3DGroup.Children.Add(rotateTransform3D);
CenterX, CenterY, CenterZ 값을 설정하여 초기 중심점을 잡고,
rotate 값에 따라서 Rotation 속성을 다르게 지정하여, transform3DGroup 변수의 자식으로
CenterX, Y, Z 값이 0이고 AxisAngleRotation3D를 ((1, 0, 0,) 1), ((1, 0, 0,) 1), ((1, 0, 0,) 1) 총 3개를 추가하는 코드입니다.
마지막으로 설정이 끝난 RotateTransform3D 데이터를 Transform3DGroup에 자식으로 추가하는 모습입니다.
* Model3DGroup
- 여러개의 3D 모델들을 한 그룹처럼 사용할 수 있게 해주는 클래스입니다.
- DirectionalLight, Geometry3DModel 등의 내용을 자식으로 추가해서 사용할 수 있습니다.
switch (content.Name)
{
case "Positions":
{
string[] positionList = content.Value.Split(' ');
foreach (string positionXYZ in positionList)
{
string[] xyzList = positionXYZ.Split(',');
double x = Convert.ToDouble(xyzList[0]);
double y = Convert.ToDouble(xyzList[1]);
double z = Convert.ToDouble(xyzList[2]);
meshGeometry3D.Positions.Add(new Point3D(x, y, z));
}
}
break;
case "Normals":
{
string[] normalList = content.Value.Split(' ');
foreach (string normalXYZ in normalList)
{
string[] xyzList = normalXYZ.Split(',');
double x = Convert.ToDouble(xyzList[0]);
double y = Convert.ToDouble(xyzList[1]);
double z = Convert.ToDouble(xyzList[2]);
meshGeometry3D.Normals.Add(new Vector3D(x, y, z));
}
}
break;
case "TextureCoordinates":
{
string[] textureList = content.Value.Split(' ');
foreach (string textureXY in textureList)
{
string[] xyList = textureXY.Split(',');
double x = Convert.ToDouble(xyList[0]);
double y = Convert.ToDouble(xyList[1]);
meshGeometry3D.TextureCoordinates.Add(new Point(x, y));
}
}
break;
case "TriangleIndices":
{
string[] triangleList = content.Value.Split(' ');
foreach (string triangleXYZ in triangleList)
{
string[] xyzList = triangleXYZ.Split(',');
int point1 = Convert.ToInt32(xyzList[0]);
int point2 = Convert.ToInt32(xyzList[1]);
int point3 = Convert.ToInt32(xyzList[2]);
meshGeometry3D.TriangleIndices.Add(point1);
meshGeometry3D.TriangleIndices.Add(point2);
meshGeometry3D.TriangleIndices.Add(point3);
}
}
break;
}
metryModel3DData.Model3DData.Geometry = meshGeometry3D;
model3DGroup.Children.Add(geometryModel3DData.Model3DData);
* DirectionalLight
- 설정한 방향으로 빛을 비추는 효과를 내는 클래스입니다.
- 아래는 DirectionalLight 데이터의 예시입니다.
<DirectionalLight Color="sc#1 1 1 1" Direction="0.000900214550631487,0.171926743517843,-0.985109326154774" />
- 아래는 사용 예시입니다.
switch (content.Name)
{
case "Color":
{
string[] colorValueList = content.Replace("sc#", "").Split(' ');
byte alpha = Convert.ToByte(Convert.ToDouble(colorValueList[0]) * 255.0);
byte red = Convert.ToByte(Convert.ToDouble(colorValueList[1]) * 255.0);
byte green = Convert.ToByte(Convert.ToDouble(colorValueList[2]) * 255.0);
byte blue = Convert.ToByte(Convert.ToDouble(colorValueList[3]) * 255.0);
directionalLight.Color = Color.FromArgb(alpha, red, green, blue);
}
break;
case "Direction":
{
string[] directionList = content.Split(',');
directionalLight.Direction = new Vector3D(Convert.ToDouble(directionList[0]), Convert.ToDouble(directionList[1]), Convert.ToDouble(directionList[2]));
}
break;
}
DirectionalLight에는 Color와 Direction이라는 값이 존재합니다.
해당 값에 따라서 불빛의 색상 값을 추출해서 적용하고 불빛을 비추는 방향을 지정하는 코드입니다.
* GeometryModel3D
- 3D 객체를 그리고, 색상을 입히는 등 실질적인 객체들을 만드는 역할을 담당하는 클래스입니다.
- 아래는 색상등을 결정하는 DiffuseMaterial, EmissiveMaterial 데이터의 예시와 CS에서 적용하는 코드 예시입니다.
<GeometryModel3D>
<GeometryModel3D.Material>
<MaterialGroup>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<SolidColorBrush Color="#FFD3D3" Opacity="0.404255330562592" />
</DiffuseMaterial.Brush>
</DiffuseMaterial>
<EmissiveMaterial>
<EmissiveMaterial.Brush>
<SolidColorBrush Color="#000000" />
</EmissiveMaterial.Brush>
</EmissiveMaterial>
</MaterialGroup>
</GeometryModel3D.Material>
</GeometryModel3D>
GeometryModel3D model3D = new GeometryModel3D();
materialGroup = new MaterialGroup();
diffuseMaterial = new DiffuseMaterial();
emissiveMaterial = new EmissiveMaterial();
byte red = 0;
byte green = 0;
byte blue = 0;
double opacity = 1;
// 파싱
for (int i = 0; i < content.Name.Length; i++)
{
switch (content.Name[i])
{
case "Color":
{
red = Convert.ToByte(content.Value[i].Substring(1, 2), 16);
green = Convert.ToByte(content.Value[i].Substring(3, 2), 16);
blue = Convert.ToByte(content.Value[i].Substring(5, 2), 16);
}
break;
case "Opacity":
{
opacity = Convert.ToDouble(content.Value[i]);
}
break;
}
}
// DiffuseMaterial, EmissiveMaterial 적용
if (diffuseMaterialBrush == true)
{
diffuseMaterial.Brush = new SolidColorBrush(Color.FromRgb(red, green, blue));
diffuseMaterial.Brush.Opacity = opacity;
materialGroup.Children.Add(diffuseMaterial);
}
else
{
emissiveMaterial.Brush = new SolidColorBrush(Color.FromRgb(red, green, blue));
emissiveMaterial.Brush.Opacity = opacity;
materialGroup.Children.Add(emissiveMaterial);
}
model3D.Material = materialGroup;
Opacity는 투명도를 관장하는 값이며, Color를 적용할 때는 Brush 속성에 해야합니다. Color 속성도 존재하긴 하지만 색상이 적용되서 그려지지는 않는 것으로 확인되었습니다. 다른 부분에 적용되는 것 같은데 이것때메 삽질을 좀 했었지요...
적용 후 MaterialGroup에 두 가지의 Material을 추가한 후 GeometryModel3D의 Material 속성에 반영시킵니다.
그리고 투명도 적용 시에는 일반적인 방법으로 적용하면 반영되지 않으므로 주의가 필요합니다.
해당 내용은 아래의 글에서 확인해주세요.
https://skfkdkdlaos.tistory.com/48
- 아래는 그리기를 위해 필요한 Positions, Normals, TextureCoordinates, TriangleIndices 값의 데이터 예시와 활용 예시입니다.
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions="-1263.079,21.74019,225.1677 -1238.014,1457.708,225.1677 1082.987,-19.21055,225.1677 1108.052,1416.758,225.1677" Normals="0,0,1 0,0,1 0,0,1 0,0,1" TextureCoordinates="0,1 0,1 0,1 0,1" TriangleIndices="3,1,2 1,0,2" />
</GeometryModel3D.Geometry>
meshGeometry3D = new MeshGeometry3D();
// 파싱
for (int i = 0; i < content.Name.Length; i++)
{
switch (content.Name[i])
{
case "Positions":
{
string[] positionList = content.Value[i].Split(' ');
foreach (string positionXYZ in positionList)
{
string[] xyzList = positionXYZ.Split(',');
double x = Convert.ToDouble(xyzList[0]);
double y = Convert.ToDouble(xyzList[1]);
double z = Convert.ToDouble(xyzList[2]);
meshGeometry3D.Positions.Add(new Point3D(x, y, z));
}
}
break;
case "Normals":
{
string[] normalList = content.Value[i].Split(' ');
foreach (string normalXYZ in normalList)
{
string[] xyzList = normalXYZ.Split(',');
double x = Convert.ToDouble(xyzList[0]);
double y = Convert.ToDouble(xyzList[1]);
double z = Convert.ToDouble(xyzList[2]);
meshGeometry3D.Normals.Add(new Vector3D(x, y, z));
}
}
break;
case "TextureCoordinates":
{
string[] textureList = content.Value[i].Split(' ');
foreach (string textureXY in textureList)
{
string[] xyList = textureXY.Split(',');
double x = Convert.ToDouble(xyList[0]);
double y = Convert.ToDouble(xyList[1]);
meshGeometry3D.TextureCoordinates.Add(new Point(x, y));
}
}
break;
case "TriangleIndices":
{
string[] triangleList = content.Value[i].Split(' ');
foreach (string triangleXYZ in triangleList)
{
string[] xyzList = triangleXYZ.Split(',');
int point1 = Convert.ToInt32(xyzList[0]);
int point2 = Convert.ToInt32(xyzList[1]);
int point3 = Convert.ToInt32(xyzList[2]);
meshGeometry3D.TriangleIndices.Add(point1);
meshGeometry3D.TriangleIndices.Add(point2);
meshGeometry3D.TriangleIndices.Add(point3);
}
}
break;
}
}
model3D.Geometry = meshGeometry3D;
Point3D를 활용해서 MeshGeometry3D의 Position에 추가하는 이유는 3D기 때문에 x, y, z 3종류의 좌표를 사용하기 때문입니다. 여러 개를 추가하는 이유는 도형을 그리려면 점 하나만 찍어서는 도형이 완성되지 않기 때문에 들어있는 좌표 값을 순서대로 이어주어야 비로소 도형 모양이 나타나게 되므로 값이 많이 존재하는 것입니다.
마지막으로 3D를 내부적으로 다 그렸다면 캔버스에 반영을 해야합니다.
그래서 아래 코드처럼 진행합니다.
// 최종 적용
modelVisual3D.Content = model3DGroup;
viewport3D.Children.Add(modelVisual3D);
grid3D.Children.Add(viewport3D);
앞서 생성했던 ModelVisual3D 객체의 Content 속성에 Model3DGroup을 적용해주고,
제일 처음 생성했던 Viewport3D에 자식으로 Model3DGroup이 Content로 적용된 ModelVisual3D를 추가해줍니다.
그 다음 마지막으로 Grid에 Viewport3D를 자식으로 추가해주면 Grid에 그려진 3D가 표시됩니다. 이 작업은 Grid안에 Viewport3D를 넣지 않았다면 생략합니다.
마지막으로 테스트용으로 xaml 형태의 3D 예시 데이터를 합쳐서 추가해 놓겠습니다.
<Page Background="sc#1 0.627450980392157 0.627450980392157 0.627450980392157" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<DockPanel>
<ScrollBar Name="xMod" ToolTip="X Direction" DockPanel.Dock="Top" Orientation="Horizontal" Minimum="-180" Maximum="180" LargeChange="10" Value="1" />
<ScrollBar Name="yMod" ToolTip="Y Direction" DockPanel.Dock="Top" Orientation="Horizontal" Minimum="-180" Maximum="180" LargeChange="10" Value="1" />
<ScrollBar Name="zMod" ToolTip="Z Direction" DockPanel.Dock="Top" Orientation="Horizontal" Minimum="-180" Maximum="180" LargeChange="10" Value="1" />
<Viewport3D>
<Viewport3D.Camera>
<PerspectiveCamera x:Name="Cam" FieldOfView="50.5496895978667" Position="-131.421390982859, 125.053676440019, 3639.80841756678" LookDirection="0.000900214550631487, 0.171926743517843, -0.985109326154774" UpDirection="0.00515799680174048, 0.985095822519873, 0.171929100279409" />
</Viewport3D.Camera>
<Viewport3D.Children>
<ModelVisual3D>
<ModelVisual3D.Transform>
<Transform3DGroup>
<RotateTransform3D CenterX="0" CenterY="0" CenterZ="0">
<RotateTransform3D.Rotation>
<AxisAngleRotation3D x:Name="rotateX" Axis="1 0 0" Angle="{Binding ElementName=xMod, Path=Value}" />
</RotateTransform3D.Rotation>
</RotateTransform3D>
<RotateTransform3D CenterX="0" CenterY="0" CenterZ="0">
<RotateTransform3D.Rotation>
<AxisAngleRotation3D x:Name="rotateY" Axis="0 1 0" Angle="{Binding ElementName=yMod, Path=Value}" />
</RotateTransform3D.Rotation>
</RotateTransform3D>
<RotateTransform3D CenterX="0" CenterY="0" CenterZ="0">
<RotateTransform3D.Rotation>
<AxisAngleRotation3D x:Name="rotateZ" Axis="0 0 1" Angle="{Binding ElementName=zMod, Path=Value}" />
</RotateTransform3D.Rotation>
</RotateTransform3D>
</Transform3DGroup>
</ModelVisual3D.Transform>
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup>
<DirectionalLight Color="sc#1 1 1 1" Direction="0.000900214550631487,0.171926743517843,-0.985109326154774" />
<GeometryModel3D>
<GeometryModel3D.Material>
<MaterialGroup>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<SolidColorBrush Color="#FFD3D3" Opacity="0.404255330562592" />
</DiffuseMaterial.Brush>
</DiffuseMaterial>
<EmissiveMaterial>
<EmissiveMaterial.Brush>
<SolidColorBrush Color="#000000" />
</EmissiveMaterial.Brush>
</EmissiveMaterial>
</MaterialGroup>
</GeometryModel3D.Material>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions="-1263.079,21.74019,225.1677 -1238.014,1457.708,225.1677 1082.987,-19.21055,225.1677 1108.052,1416.758,225.1677" Normals="0,0,1 0,0,1 0,0,1 0,0,1" TextureCoordinates="0,1 0,1 0,1 0,1" TriangleIndices="3,1,2 1,0,2" />
</GeometryModel3D.Geometry>
</GeometryModel3D>
<GeometryModel3D>
<GeometryModel3D.Material>
<MaterialGroup>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<SolidColorBrush Color="#D5FDFD" Opacity="0.404255330562592" />
</DiffuseMaterial.Brush>
</DiffuseMaterial>
<EmissiveMaterial>
<EmissiveMaterial.Brush>
<SolidColorBrush Color="#000000" />
</EmissiveMaterial.Brush>
</EmissiveMaterial>
</MaterialGroup>
</GeometryModel3D.Material>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions="-1263.079,21.74019,0 -1238.014,1457.708,0 1082.987,-19.21055,0 1108.052,1416.758,0" Normals="0,0,1 0,0,1 0,0,1 0,0,1" TextureCoordinates="0,1 0,1 0,1 0,1" TriangleIndices="3,1,2 1,0,2" />
</GeometryModel3D.Geometry>
</GeometryModel3D>
</Model3DGroup>
</ModelVisual3D.Content>
</ModelVisual3D>
</ModelVisual3D>
</Viewport3D.Children>
</Viewport3D>
</DockPanel>
</Page>
위의 데이터가 정상적으로 그려졌다면 아래와 같이 나타날 것입니다.
평면의 색상이 다른 사각형 2개인데 아래 그림은 이해를 돕기 위하여 따로 회전시켜서 저렇게 보이는 것이고,
기본적으로는 하늘색 사각형이 가려져서 보일 것입니다.
이상으로 WPF의 3D에 관해서 간단하게 알아보았습니다.
C# dll 파일을 외부에서 참조 추가 시 설명 보이게 하는 방법 (0) | 2022.07.19 |
---|---|
C# JSON 형태로 데이터 읽기, 쓰기 (생성 포함) (0) | 2022.07.15 |
C# DataTable 중복 값 제거 (0) | 2022.07.01 |
C# HttpWebRequest의 '이 verb-type으로 content-body를 보낼 수 없습니다.' 예외에 대한 정리 (0) | 2022.06.30 |
C# PC에 잡혀있는 SerialPort 목록 가져오기 (0) | 2021.10.13 |
댓글 영역