parent
b732af4b0d
commit
c01e387b89
@ -0,0 +1,138 @@ |
|||||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||||
|
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
||||||
|
<ItemGroup Label="ProjectConfigurations"> |
||||||
|
<ProjectConfiguration Include="Debug|Win32"> |
||||||
|
<Configuration>Debug</Configuration> |
||||||
|
<Platform>Win32</Platform> |
||||||
|
</ProjectConfiguration> |
||||||
|
<ProjectConfiguration Include="Release|Win32"> |
||||||
|
<Configuration>Release</Configuration> |
||||||
|
<Platform>Win32</Platform> |
||||||
|
</ProjectConfiguration> |
||||||
|
<ProjectConfiguration Include="Debug|x64"> |
||||||
|
<Configuration>Debug</Configuration> |
||||||
|
<Platform>x64</Platform> |
||||||
|
</ProjectConfiguration> |
||||||
|
<ProjectConfiguration Include="Release|x64"> |
||||||
|
<Configuration>Release</Configuration> |
||||||
|
<Platform>x64</Platform> |
||||||
|
</ProjectConfiguration> |
||||||
|
</ItemGroup> |
||||||
|
<PropertyGroup Label="Globals"> |
||||||
|
<VCProjectVersion>16.0</VCProjectVersion> |
||||||
|
<Keyword>Win32Proj</Keyword> |
||||||
|
<ProjectGuid>{735bd839-abf4-49ec-8a84-fad819015ccc}</ProjectGuid> |
||||||
|
<RootNamespace>Faceball2030</RootNamespace> |
||||||
|
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion> |
||||||
|
</PropertyGroup> |
||||||
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" /> |
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration"> |
||||||
|
<ConfigurationType>Application</ConfigurationType> |
||||||
|
<UseDebugLibraries>true</UseDebugLibraries> |
||||||
|
<PlatformToolset>v143</PlatformToolset> |
||||||
|
<CharacterSet>Unicode</CharacterSet> |
||||||
|
</PropertyGroup> |
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration"> |
||||||
|
<ConfigurationType>Application</ConfigurationType> |
||||||
|
<UseDebugLibraries>false</UseDebugLibraries> |
||||||
|
<PlatformToolset>v143</PlatformToolset> |
||||||
|
<WholeProgramOptimization>true</WholeProgramOptimization> |
||||||
|
<CharacterSet>Unicode</CharacterSet> |
||||||
|
</PropertyGroup> |
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration"> |
||||||
|
<ConfigurationType>Application</ConfigurationType> |
||||||
|
<UseDebugLibraries>true</UseDebugLibraries> |
||||||
|
<PlatformToolset>v143</PlatformToolset> |
||||||
|
<CharacterSet>Unicode</CharacterSet> |
||||||
|
</PropertyGroup> |
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration"> |
||||||
|
<ConfigurationType>Application</ConfigurationType> |
||||||
|
<UseDebugLibraries>false</UseDebugLibraries> |
||||||
|
<PlatformToolset>v143</PlatformToolset> |
||||||
|
<WholeProgramOptimization>true</WholeProgramOptimization> |
||||||
|
<CharacterSet>Unicode</CharacterSet> |
||||||
|
</PropertyGroup> |
||||||
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" /> |
||||||
|
<ImportGroup Label="ExtensionSettings"> |
||||||
|
</ImportGroup> |
||||||
|
<ImportGroup Label="Shared"> |
||||||
|
</ImportGroup> |
||||||
|
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'"> |
||||||
|
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> |
||||||
|
</ImportGroup> |
||||||
|
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'"> |
||||||
|
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> |
||||||
|
</ImportGroup> |
||||||
|
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> |
||||||
|
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> |
||||||
|
</ImportGroup> |
||||||
|
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> |
||||||
|
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> |
||||||
|
</ImportGroup> |
||||||
|
<PropertyGroup Label="UserMacros" /> |
||||||
|
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'"> |
||||||
|
<ClCompile> |
||||||
|
<WarningLevel>Level3</WarningLevel> |
||||||
|
<SDLCheck>true</SDLCheck> |
||||||
|
<PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions> |
||||||
|
<ConformanceMode>true</ConformanceMode> |
||||||
|
</ClCompile> |
||||||
|
<Link> |
||||||
|
<SubSystem>Console</SubSystem> |
||||||
|
<GenerateDebugInformation>true</GenerateDebugInformation> |
||||||
|
</Link> |
||||||
|
</ItemDefinitionGroup> |
||||||
|
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'"> |
||||||
|
<ClCompile> |
||||||
|
<WarningLevel>Level3</WarningLevel> |
||||||
|
<FunctionLevelLinking>true</FunctionLevelLinking> |
||||||
|
<IntrinsicFunctions>true</IntrinsicFunctions> |
||||||
|
<SDLCheck>true</SDLCheck> |
||||||
|
<PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions> |
||||||
|
<ConformanceMode>true</ConformanceMode> |
||||||
|
</ClCompile> |
||||||
|
<Link> |
||||||
|
<SubSystem>Console</SubSystem> |
||||||
|
<EnableCOMDATFolding>true</EnableCOMDATFolding> |
||||||
|
<OptimizeReferences>true</OptimizeReferences> |
||||||
|
<GenerateDebugInformation>true</GenerateDebugInformation> |
||||||
|
</Link> |
||||||
|
</ItemDefinitionGroup> |
||||||
|
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> |
||||||
|
<ClCompile> |
||||||
|
<WarningLevel>Level3</WarningLevel> |
||||||
|
<SDLCheck>true</SDLCheck> |
||||||
|
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions> |
||||||
|
<ConformanceMode>true</ConformanceMode> |
||||||
|
</ClCompile> |
||||||
|
<Link> |
||||||
|
<SubSystem>Console</SubSystem> |
||||||
|
<GenerateDebugInformation>true</GenerateDebugInformation> |
||||||
|
</Link> |
||||||
|
</ItemDefinitionGroup> |
||||||
|
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> |
||||||
|
<ClCompile> |
||||||
|
<WarningLevel>Level3</WarningLevel> |
||||||
|
<FunctionLevelLinking>true</FunctionLevelLinking> |
||||||
|
<IntrinsicFunctions>true</IntrinsicFunctions> |
||||||
|
<SDLCheck>true</SDLCheck> |
||||||
|
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions> |
||||||
|
<ConformanceMode>true</ConformanceMode> |
||||||
|
</ClCompile> |
||||||
|
<Link> |
||||||
|
<SubSystem>Console</SubSystem> |
||||||
|
<EnableCOMDATFolding>true</EnableCOMDATFolding> |
||||||
|
<OptimizeReferences>true</OptimizeReferences> |
||||||
|
<GenerateDebugInformation>true</GenerateDebugInformation> |
||||||
|
</Link> |
||||||
|
</ItemDefinitionGroup> |
||||||
|
<ItemGroup> |
||||||
|
<ClCompile Include="main.cpp" /> |
||||||
|
</ItemGroup> |
||||||
|
<ItemGroup> |
||||||
|
<ClInclude Include="pixelGameEngine.h" /> |
||||||
|
</ItemGroup> |
||||||
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" /> |
||||||
|
<ImportGroup Label="ExtensionTargets"> |
||||||
|
</ImportGroup> |
||||||
|
</Project> |
After Width: | Height: | Size: 218 KiB |
@ -0,0 +1,673 @@ |
|||||||
|
#define OLC_PGE_APPLICATION |
||||||
|
#include "pixelGameEngine.h" |
||||||
|
#include <strstream> |
||||||
|
#include <algorithm> |
||||||
|
|
||||||
|
using namespace olc; |
||||||
|
|
||||||
|
const float PI = 3.14159f; |
||||||
|
|
||||||
|
struct vec2d |
||||||
|
{ |
||||||
|
float u = 0; |
||||||
|
float v = 0; |
||||||
|
float w = 1; |
||||||
|
}; |
||||||
|
|
||||||
|
|
||||||
|
struct vec3d |
||||||
|
{ |
||||||
|
float x = 0; |
||||||
|
float y = 0; |
||||||
|
float z = 0; |
||||||
|
float w = 1; |
||||||
|
}; |
||||||
|
|
||||||
|
struct triangle |
||||||
|
{ |
||||||
|
vec3d p[3]; |
||||||
|
vec2d uv[3]; |
||||||
|
Pixel col; |
||||||
|
}; |
||||||
|
|
||||||
|
struct mesh |
||||||
|
{ |
||||||
|
std::vector<triangle> tris; |
||||||
|
Decal* texture; |
||||||
|
|
||||||
|
void Parse(std::string str, int& v, int& uv) { |
||||||
|
std::cout << str << "\n"; |
||||||
|
std::stringstream s(str.substr(0, str.find("/") + 1)); |
||||||
|
s >> v; |
||||||
|
str.erase(0, str.find("/") + 1); |
||||||
|
std::stringstream s2(str.substr(0, str.find("/") + 1)); |
||||||
|
s2 >> uv; |
||||||
|
//std::cout<<" "<<v<<"/"<<uv<<"\n";
|
||||||
|
} |
||||||
|
|
||||||
|
bool LoadFromObjectFile(std::string sFilename) |
||||||
|
{ |
||||||
|
std::ifstream f(sFilename); |
||||||
|
if (!f.is_open()) |
||||||
|
return false; |
||||||
|
|
||||||
|
// Local cache of verts
|
||||||
|
std::vector<vec3d> verts; |
||||||
|
std::vector<vec2d> uvs; |
||||||
|
|
||||||
|
std::string data; |
||||||
|
while (f.good()) { |
||||||
|
f >> data; |
||||||
|
if (data == "v") { |
||||||
|
float x, y, z; |
||||||
|
f >> x >> y >> z; |
||||||
|
verts.push_back({ x,y,z }); |
||||||
|
//std::cout<<x<<" "<<y<<" "<<z<<"\n";
|
||||||
|
} |
||||||
|
else |
||||||
|
if (data == "vt") { |
||||||
|
float u, v; |
||||||
|
f >> u >> v; |
||||||
|
uvs.push_back({ u,v }); |
||||||
|
//std::cout<<u<<" "<<v<<"\n";
|
||||||
|
} |
||||||
|
else |
||||||
|
if (data == "f") { |
||||||
|
//std::cout<<"face\n";
|
||||||
|
std::string t1, t2, t3; |
||||||
|
f >> t1 >> t2 >> t3; |
||||||
|
int v1, v2, v3, uv1, uv2, uv3; |
||||||
|
Parse(t1, v1, uv1); |
||||||
|
Parse(t2, v2, uv2); |
||||||
|
Parse(t3, v3, uv3); |
||||||
|
tris.push_back({ verts[v1 - 1],verts[v2 - 1],verts[v3 - 1], |
||||||
|
uvs[uv1 - 1],uvs[uv2 - 1],uvs[uv3 - 1] }); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
}; |
||||||
|
|
||||||
|
struct mat4x4 |
||||||
|
{ |
||||||
|
float m[4][4] = { 0 }; |
||||||
|
}; |
||||||
|
|
||||||
|
class olcEngine3D : public PixelGameEngine |
||||||
|
{ |
||||||
|
|
||||||
|
public: |
||||||
|
|
||||||
|
Decal* texture; |
||||||
|
olcEngine3D() |
||||||
|
{ |
||||||
|
sAppName = "3D Demo"; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
private: |
||||||
|
mesh meshCube; |
||||||
|
mat4x4 matProj; |
||||||
|
|
||||||
|
vec3d vCamera = { 0,0,0 }; |
||||||
|
vec3d vLookDir; |
||||||
|
|
||||||
|
float zOffset = 2; |
||||||
|
|
||||||
|
float fTheta = 0; |
||||||
|
float fYaw = 0; |
||||||
|
float pitch = -PI / 6; |
||||||
|
|
||||||
|
vec3d |
||||||
|
Matrix_MultiplyVector(mat4x4& m, vec3d& i) |
||||||
|
{ |
||||||
|
vec3d v; |
||||||
|
v.x = i.x * m.m[0][0] + i.y * m.m[1][0] + i.z * m.m[2][0] + i.w * m.m[3][0]; |
||||||
|
v.y = i.x * m.m[0][1] + i.y * m.m[1][1] + i.z * m.m[2][1] + i.w * m.m[3][1]; |
||||||
|
v.z = i.x * m.m[0][2] + i.y * m.m[1][2] + i.z * m.m[2][2] + i.w * m.m[3][2]; |
||||||
|
v.w = i.x * m.m[0][3] + i.y * m.m[1][3] + i.z * m.m[2][3] + i.w * m.m[3][3]; |
||||||
|
return v; |
||||||
|
} |
||||||
|
|
||||||
|
mat4x4 Matrix_MakeIdentity() |
||||||
|
{ |
||||||
|
mat4x4 matrix; |
||||||
|
matrix.m[0][0] = 1.0f; |
||||||
|
matrix.m[1][1] = 1.0f; |
||||||
|
matrix.m[2][2] = 1.0f; |
||||||
|
matrix.m[3][3] = 1.0f; |
||||||
|
return matrix; |
||||||
|
} |
||||||
|
|
||||||
|
mat4x4 Matrix_MakeRotationX(float fAngleRad) |
||||||
|
{ |
||||||
|
mat4x4 matrix; |
||||||
|
matrix.m[0][0] = 1.0f; |
||||||
|
matrix.m[1][1] = cosf(fAngleRad); |
||||||
|
matrix.m[1][2] = sinf(fAngleRad); |
||||||
|
matrix.m[2][1] = -sinf(fAngleRad); |
||||||
|
matrix.m[2][2] = cosf(fAngleRad); |
||||||
|
matrix.m[3][3] = 1.0f; |
||||||
|
return matrix; |
||||||
|
} |
||||||
|
|
||||||
|
mat4x4 Matrix_MakeRotationY(float fAngleRad) |
||||||
|
{ |
||||||
|
mat4x4 matrix; |
||||||
|
matrix.m[0][0] = cosf(fAngleRad); |
||||||
|
matrix.m[0][2] = sinf(fAngleRad); |
||||||
|
matrix.m[2][0] = -sinf(fAngleRad); |
||||||
|
matrix.m[1][1] = 1.0f; |
||||||
|
matrix.m[2][2] = cosf(fAngleRad); |
||||||
|
matrix.m[3][3] = 1.0f; |
||||||
|
return matrix; |
||||||
|
} |
||||||
|
|
||||||
|
mat4x4 Matrix_MakeRotationZ(float fAngleRad) |
||||||
|
{ |
||||||
|
mat4x4 matrix; |
||||||
|
matrix.m[0][0] = cosf(fAngleRad); |
||||||
|
matrix.m[0][1] = sinf(fAngleRad); |
||||||
|
matrix.m[1][0] = -sinf(fAngleRad); |
||||||
|
matrix.m[1][1] = cosf(fAngleRad); |
||||||
|
matrix.m[2][2] = 1.0f; |
||||||
|
matrix.m[3][3] = 1.0f; |
||||||
|
return matrix; |
||||||
|
} |
||||||
|
|
||||||
|
mat4x4 Matrix_MakeTranslation(float x, float y, float z) |
||||||
|
{ |
||||||
|
mat4x4 matrix; |
||||||
|
matrix.m[0][0] = 1.0f; |
||||||
|
matrix.m[1][1] = 1.0f; |
||||||
|
matrix.m[2][2] = 1.0f; |
||||||
|
matrix.m[3][3] = 1.0f; |
||||||
|
matrix.m[3][0] = x; |
||||||
|
matrix.m[3][1] = y; |
||||||
|
matrix.m[3][2] = z; |
||||||
|
return matrix; |
||||||
|
} |
||||||
|
|
||||||
|
mat4x4 Matrix_MakeProjection(float fFovDegrees, float fAspectRatio, float fNear, float fFar) |
||||||
|
{ |
||||||
|
float fFovRad = 1.0f / tanf(fFovDegrees * 0.5f / 180.0f * 3.14159f); |
||||||
|
mat4x4 matrix; |
||||||
|
matrix.m[0][0] = fAspectRatio * fFovRad; |
||||||
|
matrix.m[1][1] = fFovRad; |
||||||
|
matrix.m[2][2] = fFar / (fFar - fNear); |
||||||
|
matrix.m[3][2] = (-fFar * fNear) / (fFar - fNear); |
||||||
|
matrix.m[2][3] = 1.0f; |
||||||
|
matrix.m[3][3] = 0.0f; |
||||||
|
return matrix; |
||||||
|
} |
||||||
|
|
||||||
|
mat4x4 Matrix_MultiplyMatrix(mat4x4& m1, mat4x4& m2) |
||||||
|
{ |
||||||
|
mat4x4 matrix; |
||||||
|
for (int c = 0; c < 4; c++) |
||||||
|
for (int r = 0; r < 4; r++) |
||||||
|
matrix.m[r][c] = m1.m[r][0] * m2.m[0][c] + m1.m[r][1] * m2.m[1][c] + m1.m[r][2] * m2.m[2][c] + m1.m[r][3] * m2.m[3][c]; |
||||||
|
return matrix; |
||||||
|
} |
||||||
|
|
||||||
|
mat4x4 Matrix_PointAt(vec3d& pos, vec3d& target, vec3d& up) |
||||||
|
{ |
||||||
|
// Calculate new forward direction
|
||||||
|
vec3d newForward = Vector_Sub(target, pos); |
||||||
|
newForward = Vector_Normalise(newForward); |
||||||
|
|
||||||
|
// Calculate new Up direction
|
||||||
|
vec3d a = Vector_Mul(newForward, Vector_DotProduct(up, newForward)); |
||||||
|
vec3d newUp = Vector_Sub(up, a); |
||||||
|
newUp = Vector_Normalise(newUp); |
||||||
|
|
||||||
|
// New Right direction is easy, its just cross product
|
||||||
|
vec3d newRight = Vector_CrossProduct(newUp, newForward); |
||||||
|
|
||||||
|
// Construct Dimensioning and Translation Matrix
|
||||||
|
mat4x4 matrix; |
||||||
|
matrix.m[0][0] = newRight.x; matrix.m[0][1] = newRight.y; matrix.m[0][2] = newRight.z; matrix.m[0][3] = 0.0f; |
||||||
|
matrix.m[1][0] = newUp.x; matrix.m[1][1] = newUp.y; matrix.m[1][2] = newUp.z; matrix.m[1][3] = 0.0f; |
||||||
|
matrix.m[2][0] = newForward.x; matrix.m[2][1] = newForward.y; matrix.m[2][2] = newForward.z; matrix.m[2][3] = 0.0f; |
||||||
|
matrix.m[3][0] = pos.x; matrix.m[3][1] = pos.y; matrix.m[3][2] = pos.z; matrix.m[3][3] = 1.0f; |
||||||
|
return matrix; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
mat4x4 Matrix_QuickInverse(mat4x4& m) // Only for Rotation/Translation Matrices
|
||||||
|
{ |
||||||
|
mat4x4 matrix; |
||||||
|
matrix.m[0][0] = m.m[0][0]; matrix.m[0][1] = m.m[1][0]; matrix.m[0][2] = m.m[2][0]; matrix.m[0][3] = 0.0f; |
||||||
|
matrix.m[1][0] = m.m[0][1]; matrix.m[1][1] = m.m[1][1]; matrix.m[1][2] = m.m[2][1]; matrix.m[1][3] = 0.0f; |
||||||
|
matrix.m[2][0] = m.m[0][2]; matrix.m[2][1] = m.m[1][2]; matrix.m[2][2] = m.m[2][2]; matrix.m[2][3] = 0.0f; |
||||||
|
matrix.m[3][0] = -(m.m[3][0] * matrix.m[0][0] + m.m[3][1] * matrix.m[1][0] + m.m[3][2] * matrix.m[2][0]); |
||||||
|
matrix.m[3][1] = -(m.m[3][0] * matrix.m[0][1] + m.m[3][1] * matrix.m[1][1] + m.m[3][2] * matrix.m[2][1]); |
||||||
|
matrix.m[3][2] = -(m.m[3][0] * matrix.m[0][2] + m.m[3][1] * matrix.m[1][2] + m.m[3][2] * matrix.m[2][2]); |
||||||
|
matrix.m[3][3] = 1.0f; |
||||||
|
return matrix; |
||||||
|
} |
||||||
|
|
||||||
|
vec3d Vector_Add(vec3d& v1, vec3d& v2) |
||||||
|
{ |
||||||
|
return { v1.x + v2.x, v1.y + v2.y, v1.z + v2.z }; |
||||||
|
} |
||||||
|
|
||||||
|
vec3d Vector_Sub(vec3d& v1, vec3d& v2) |
||||||
|
{ |
||||||
|
return { v1.x - v2.x, v1.y - v2.y, v1.z - v2.z }; |
||||||
|
} |
||||||
|
|
||||||
|
vec3d Vector_Mul(vec3d& v1, float k) |
||||||
|
{ |
||||||
|
return { v1.x * k, v1.y * k, v1.z * k }; |
||||||
|
} |
||||||
|
|
||||||
|
vec3d Vector_Div(vec3d& v1, float k) |
||||||
|
{ |
||||||
|
return { v1.x / k, v1.y / k, v1.z / k }; |
||||||
|
} |
||||||
|
|
||||||
|
float Vector_DotProduct(vec3d& v1, vec3d& v2) |
||||||
|
{ |
||||||
|
return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z; |
||||||
|
} |
||||||
|
|
||||||
|
float Vector_Length(vec3d& v) |
||||||
|
{ |
||||||
|
return sqrtf(Vector_DotProduct(v, v)); |
||||||
|
} |
||||||
|
|
||||||
|
vec3d Vector_Normalise(vec3d& v) |
||||||
|
{ |
||||||
|
float l = Vector_Length(v); |
||||||
|
return { v.x / l, v.y / l, v.z / l }; |
||||||
|
} |
||||||
|
|
||||||
|
vec3d Vector_CrossProduct(vec3d& v1, vec3d& v2) |
||||||
|
{ |
||||||
|
vec3d v; |
||||||
|
v.x = v1.y * v2.z - v1.z * v2.y; |
||||||
|
v.y = v1.z * v2.x - v1.x * v2.z; |
||||||
|
v.z = v1.x * v2.y - v1.y * v2.x; |
||||||
|
return v; |
||||||
|
} |
||||||
|
|
||||||
|
vec3d Vector_IntersectPlane(vec3d& plane_p, vec3d& plane_n, vec3d& lineStart, vec3d& lineEnd, float& t) |
||||||
|
{ |
||||||
|
plane_n = Vector_Normalise(plane_n); |
||||||
|
float plane_d = -Vector_DotProduct(plane_n, plane_p); |
||||||
|
float ad = Vector_DotProduct(lineStart, plane_n); |
||||||
|
float bd = Vector_DotProduct(lineEnd, plane_n); |
||||||
|
t = (-plane_d - ad) / (bd - ad); |
||||||
|
vec3d lineStartToEnd = Vector_Sub(lineEnd, lineStart); |
||||||
|
vec3d lineToIntersect = Vector_Mul(lineStartToEnd, t); |
||||||
|
return Vector_Add(lineStart, lineToIntersect); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
int Triangle_ClipAgainstPlane(vec3d plane_p, vec3d plane_n, triangle& in_tri, triangle& out_tri1, triangle& out_tri2) |
||||||
|
{ |
||||||
|
// Make sure plane normal is indeed normal
|
||||||
|
plane_n = Vector_Normalise(plane_n); |
||||||
|
|
||||||
|
// Return signed shortest distance from point to plane, plane normal must be normalised
|
||||||
|
auto dist = [&](vec3d& p) |
||||||
|
{ |
||||||
|
vec3d n = Vector_Normalise(p); |
||||||
|
return (plane_n.x * p.x + plane_n.y * p.y + plane_n.z * p.z - Vector_DotProduct(plane_n, plane_p)); |
||||||
|
}; |
||||||
|
|
||||||
|
// Create two temporary storage arrays to classify points either side of plane
|
||||||
|
// If distance sign is positive, point lies on "inside" of plane
|
||||||
|
vec3d* inside_points[3]; int nInsidePointCount = 0; |
||||||
|
vec3d* outside_points[3]; int nOutsidePointCount = 0; |
||||||
|
vec2d* inside_tex[3]; int nInsideTexCount = 0; |
||||||
|
vec2d* outside_tex[3]; int nOutsideTexCount = 0; |
||||||
|
|
||||||
|
|
||||||
|
// Get signed distance of each point in triangle to plane
|
||||||
|
float d0 = dist(in_tri.p[0]); |
||||||
|
float d1 = dist(in_tri.p[1]); |
||||||
|
float d2 = dist(in_tri.p[2]); |
||||||
|
|
||||||
|
if (d0 >= 0) { inside_points[nInsidePointCount++] = &in_tri.p[0]; inside_tex[nInsideTexCount++] = &in_tri.uv[0]; } |
||||||
|
else { |
||||||
|
outside_points[nOutsidePointCount++] = &in_tri.p[0]; outside_tex[nOutsideTexCount++] = &in_tri.uv[0]; |
||||||
|
} |
||||||
|
if (d1 >= 0) { |
||||||
|
inside_points[nInsidePointCount++] = &in_tri.p[1]; inside_tex[nInsideTexCount++] = &in_tri.uv[1]; |
||||||
|
} |
||||||
|
else { |
||||||
|
outside_points[nOutsidePointCount++] = &in_tri.p[1]; outside_tex[nOutsideTexCount++] = &in_tri.uv[1]; |
||||||
|
} |
||||||
|
if (d2 >= 0) { |
||||||
|
inside_points[nInsidePointCount++] = &in_tri.p[2]; inside_tex[nInsideTexCount++] = &in_tri.uv[2]; |
||||||
|
} |
||||||
|
else { |
||||||
|
outside_points[nOutsidePointCount++] = &in_tri.p[2]; outside_tex[nOutsideTexCount++] = &in_tri.uv[2]; |
||||||
|
} |
||||||
|
|
||||||
|
// Now classify triangle points, and break the input triangle into
|
||||||
|
// smaller output triangles if required. There are four possible
|
||||||
|
// outcomes...
|
||||||
|
|
||||||
|
if (nInsidePointCount == 0) |
||||||
|
{ |
||||||
|
// All points lie on the outside of plane, so clip whole triangle
|
||||||
|
// It ceases to exist
|
||||||
|
|
||||||
|
return 0; // No returned triangles are valid
|
||||||
|
} |
||||||
|
|
||||||
|
if (nInsidePointCount == 3) |
||||||
|
{ |
||||||
|
// All points lie on the inside of plane, so do nothing
|
||||||
|
// and allow the triangle to simply pass through
|
||||||
|
out_tri1 = in_tri; |
||||||
|
|
||||||
|
return 1; // Just the one returned original triangle is valid
|
||||||
|
} |
||||||
|
|
||||||
|
if (nInsidePointCount == 1 && nOutsidePointCount == 2) |
||||||
|
{ |
||||||
|
// Triangle should be clipped. As two points lie outside
|
||||||
|
// the plane, the triangle simply becomes a smaller triangle
|
||||||
|
|
||||||
|
// Copy appearance info to new triangle
|
||||||
|
out_tri1.col = in_tri.col; |
||||||
|
|
||||||
|
// The inside point is valid, so keep that...
|
||||||
|
out_tri1.p[0] = *inside_points[0]; |
||||||
|
out_tri1.uv[0] = *inside_tex[0]; |
||||||
|
|
||||||
|
// but the two new points are at the locations where the
|
||||||
|
// original sides of the triangle (lines) intersect with the plane
|
||||||
|
float t; |
||||||
|
out_tri1.p[1] = Vector_IntersectPlane(plane_p, plane_n, *inside_points[0], *outside_points[0], t); |
||||||
|
out_tri1.uv[1].u = t * (outside_tex[0]->u - inside_tex[0]->u) + inside_tex[0]->u; |
||||||
|
out_tri1.uv[1].v = t * (outside_tex[0]->v - inside_tex[0]->v) + inside_tex[0]->v; |
||||||
|
out_tri1.uv[1].w = t * (outside_tex[0]->w - inside_tex[0]->w) + inside_tex[0]->w; |
||||||
|
|
||||||
|
out_tri1.p[2] = Vector_IntersectPlane(plane_p, plane_n, *inside_points[0], *outside_points[1], t); |
||||||
|
out_tri1.uv[2].u = t * (outside_tex[1]->u - inside_tex[0]->u) + inside_tex[0]->u; |
||||||
|
out_tri1.uv[2].v = t * (outside_tex[1]->v - inside_tex[0]->v) + inside_tex[0]->v; |
||||||
|
out_tri1.uv[2].w = t * (outside_tex[1]->w - inside_tex[0]->w) + inside_tex[0]->w; |
||||||
|
|
||||||
|
return 1; // Return the newly formed single triangle
|
||||||
|
} |
||||||
|
|
||||||
|
if (nInsidePointCount == 2 && nOutsidePointCount == 1) |
||||||
|
{ |
||||||
|
// Triangle should be clipped. As two points lie inside the plane,
|
||||||
|
// the clipped triangle becomes a "quad". Fortunately, we can
|
||||||
|
// represent a quad with two new triangles
|
||||||
|
|
||||||
|
// Copy appearance info to new triangles
|
||||||
|
out_tri1.col = in_tri.col; |
||||||
|
out_tri2.col = in_tri.col; |
||||||
|
|
||||||
|
// The first triangle consists of the two inside points and a new
|
||||||
|
// point determined by the location where one side of the triangle
|
||||||
|
// intersects with the plane
|
||||||
|
out_tri1.p[0] = *inside_points[0]; |
||||||
|
out_tri1.p[1] = *inside_points[1]; |
||||||
|
out_tri1.uv[0] = *inside_tex[0]; |
||||||
|
out_tri1.uv[1] = *inside_tex[1]; |
||||||
|
|
||||||
|
float t; |
||||||
|
out_tri1.p[2] = Vector_IntersectPlane(plane_p, plane_n, *inside_points[0], *outside_points[0], t); |
||||||
|
out_tri1.uv[2].u = t * (outside_tex[0]->u - inside_tex[0]->u) + inside_tex[0]->u; |
||||||
|
out_tri1.uv[2].v = t * (outside_tex[0]->v - inside_tex[0]->v) + inside_tex[0]->v; |
||||||
|
out_tri1.uv[2].w = t * (outside_tex[0]->w - inside_tex[0]->w) + inside_tex[0]->w; |
||||||
|
|
||||||
|
// The second triangle is composed of one of he inside points, a
|
||||||
|
// new point determined by the intersection of the other side of the
|
||||||
|
// triangle and the plane, and the newly created point above
|
||||||
|
out_tri2.p[0] = *inside_points[1]; |
||||||
|
out_tri2.uv[0] = *inside_tex[1]; |
||||||
|
out_tri2.p[1] = out_tri1.p[2]; |
||||||
|
out_tri2.uv[1] = out_tri1.uv[2]; |
||||||
|
out_tri2.p[2] = Vector_IntersectPlane(plane_p, plane_n, *inside_points[1], *outside_points[0], t); |
||||||
|
out_tri2.uv[2].u = t * (outside_tex[0]->u - inside_tex[1]->u) + inside_tex[1]->u; |
||||||
|
out_tri2.uv[2].v = t * (outside_tex[0]->v - inside_tex[1]->v) + inside_tex[1]->v; |
||||||
|
out_tri2.uv[2].w = t * (outside_tex[0]->w - inside_tex[1]->w) + inside_tex[1]->w; |
||||||
|
return 2; // Return two newly formed triangles which form a quad
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public: |
||||||
|
bool OnUserCreate() override |
||||||
|
{ |
||||||
|
texture = new Decal(new Sprite("High.png")); |
||||||
|
meshCube.LoadFromObjectFile("Artisans Hub.obj"); |
||||||
|
|
||||||
|
matProj = Matrix_MakeProjection(90.0f, (float)ScreenHeight() / (float)ScreenWidth(), 0.1f, 1000.0f); |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
bool OnUserUpdate(float fElapsedTime) override |
||||||
|
{ |
||||||
|
if (GetKey(olc::DOWN).bHeld) { |
||||||
|
pitch -= 1 * fElapsedTime; |
||||||
|
} |
||||||
|
if (GetKey(olc::UP).bHeld) { |
||||||
|
pitch += 1 * fElapsedTime; |
||||||
|
} |
||||||
|
vec3d vForward = Vector_Mul(vLookDir, 20 * fElapsedTime); |
||||||
|
if (GetKey(olc::W).bHeld) { |
||||||
|
vCamera = Vector_Add(vCamera, vForward); |
||||||
|
} |
||||||
|
if (GetKey(olc::S).bHeld) { |
||||||
|
vCamera = Vector_Sub(vCamera, vForward); |
||||||
|
} |
||||||
|
if (GetKey(olc::A).bHeld) { |
||||||
|
fYaw -= 2 * fElapsedTime; |
||||||
|
} |
||||||
|
if (GetKey(olc::D).bHeld) { |
||||||
|
fYaw += 2 * fElapsedTime; |
||||||
|
} |
||||||
|
|
||||||
|
// Set up rotation matrices
|
||||||
|
mat4x4 matRotZ, matRotX, matTrans, matWorld; |
||||||
|
|
||||||
|
matRotZ = Matrix_MakeRotationZ(fTheta * 0.5f); |
||||||
|
matRotX = Matrix_MakeRotationX(fTheta); |
||||||
|
|
||||||
|
matTrans = Matrix_MakeTranslation(0.0f, 0.0f, 5.0f); |
||||||
|
matWorld = Matrix_MakeIdentity(); |
||||||
|
matWorld = Matrix_MultiplyMatrix(matRotZ, matRotX); |
||||||
|
matWorld = Matrix_MultiplyMatrix(matWorld, matTrans); |
||||||
|
|
||||||
|
vec3d vUp = { 0,1,0 }; |
||||||
|
vec3d vTarget = { 0,sinf(pitch),cosf(pitch) }; |
||||||
|
mat4x4 matCameraRot = Matrix_MakeRotationY(fYaw); |
||||||
|
vLookDir = Matrix_MultiplyVector(matCameraRot, vTarget); |
||||||
|
vTarget = Vector_Add(vCamera, vLookDir); |
||||||
|
mat4x4 matCamera = Matrix_PointAt(vCamera, vTarget, vUp); |
||||||
|
mat4x4 matView = Matrix_QuickInverse(matCamera); |
||||||
|
|
||||||
|
std::vector<triangle>vecTrianglesToRaster; |
||||||
|
|
||||||
|
// Draw Triangles
|
||||||
|
for (auto& tri : meshCube.tris) |
||||||
|
{ |
||||||
|
triangle triProjected, triTransformed, triViewed; |
||||||
|
|
||||||
|
triTransformed.p[0] = Matrix_MultiplyVector(matWorld, tri.p[0]); |
||||||
|
triTransformed.p[1] = Matrix_MultiplyVector(matWorld, tri.p[1]); |
||||||
|
triTransformed.p[2] = Matrix_MultiplyVector(matWorld, tri.p[2]); |
||||||
|
triTransformed.uv[0] = tri.uv[0]; |
||||||
|
triTransformed.uv[1] = tri.uv[1]; |
||||||
|
triTransformed.uv[2] = tri.uv[2]; |
||||||
|
|
||||||
|
vec3d normal, line1, line2; |
||||||
|
line1 = Vector_Sub(triTransformed.p[1], triTransformed.p[0]); |
||||||
|
line2 = Vector_Sub(triTransformed.p[2], triTransformed.p[0]); |
||||||
|
|
||||||
|
normal = Vector_CrossProduct(line1, line2); |
||||||
|
normal = Vector_Normalise(normal); |
||||||
|
|
||||||
|
vec3d vCameraRay = Vector_Sub(triTransformed.p[0], vCamera); |
||||||
|
|
||||||
|
if (Vector_DotProduct(normal, vCameraRay) < 0) { |
||||||
|
vec3d light_dir = Vector_Mul(vLookDir, -1); |
||||||
|
light_dir = Vector_Normalise(light_dir); |
||||||
|
|
||||||
|
float dp = std::max(0.7f, Vector_DotProduct(light_dir, normal)); |
||||||
|
|
||||||
|
triViewed.p[0] = Matrix_MultiplyVector(matView, triTransformed.p[0]); |
||||||
|
triViewed.p[1] = Matrix_MultiplyVector(matView, triTransformed.p[1]); |
||||||
|
triViewed.p[2] = Matrix_MultiplyVector(matView, triTransformed.p[2]); |
||||||
|
triViewed.uv[0] = triTransformed.uv[0]; |
||||||
|
triViewed.uv[1] = triTransformed.uv[1]; |
||||||
|
triViewed.uv[2] = triTransformed.uv[2]; |
||||||
|
triViewed.col = Pixel(255 * dp * dp, 255 * dp * dp, 255 * dp * dp); |
||||||
|
|
||||||
|
int nClippedTriangles = 0; |
||||||
|
triangle clipped[2]; |
||||||
|
nClippedTriangles = Triangle_ClipAgainstPlane({ 0.0f, 0.0f, 0.1f }, { 0.0f, 0.0f, 1.0f }, triViewed, clipped[0], clipped[1]); |
||||||
|
|
||||||
|
for (int n = 0; n < nClippedTriangles; n++) { |
||||||
|
// Project triangles from 3D --> 2D
|
||||||
|
triProjected.p[0] = Matrix_MultiplyVector(matProj, clipped[n].p[0]); |
||||||
|
triProjected.p[1] = Matrix_MultiplyVector(matProj, clipped[n].p[1]); |
||||||
|
triProjected.p[2] = Matrix_MultiplyVector(matProj, clipped[n].p[2]); |
||||||
|
triProjected.col = clipped[n].col; |
||||||
|
triProjected.uv[0] = clipped[n].uv[0]; |
||||||
|
triProjected.uv[1] = clipped[n].uv[1]; |
||||||
|
triProjected.uv[2] = clipped[n].uv[2]; |
||||||
|
triProjected.uv[0].u = triProjected.uv[0].u / triProjected.p[0].w; |
||||||
|
triProjected.uv[1].u = triProjected.uv[1].u / triProjected.p[1].w; |
||||||
|
triProjected.uv[2].u = triProjected.uv[2].u / triProjected.p[2].w; |
||||||
|
|
||||||
|
triProjected.uv[0].v = triProjected.uv[0].v / triProjected.p[0].w; |
||||||
|
triProjected.uv[1].v = triProjected.uv[1].v / triProjected.p[1].w; |
||||||
|
triProjected.uv[2].v = triProjected.uv[2].v / triProjected.p[2].w; |
||||||
|
|
||||||
|
triProjected.uv[0].w = 1.0f / triProjected.p[0].w; |
||||||
|
triProjected.uv[1].w = 1.0f / triProjected.p[1].w; |
||||||
|
triProjected.uv[2].w = 1.0f / triProjected.p[2].w; |
||||||
|
|
||||||
|
|
||||||
|
triProjected.p[0] = Vector_Div(triProjected.p[0], triProjected.p[0].w); |
||||||
|
triProjected.p[1] = Vector_Div(triProjected.p[1], triProjected.p[1].w); |
||||||
|
triProjected.p[2] = Vector_Div(triProjected.p[2], triProjected.p[2].w); |
||||||
|
|
||||||
|
triProjected.p[0].x *= -1.0f; |
||||||
|
triProjected.p[1].x *= -1.0f; |
||||||
|
triProjected.p[2].x *= -1.0f; |
||||||
|
triProjected.p[0].y *= -1.0f; |
||||||
|
triProjected.p[1].y *= -1.0f; |
||||||
|
triProjected.p[2].y *= -1.0f; |
||||||
|
|
||||||
|
// Scale into view
|
||||||
|
vec3d vOffsetView = { 1,1,0 }; |
||||||
|
triProjected.p[0] = Vector_Add(triProjected.p[0], vOffsetView); |
||||||
|
triProjected.p[1] = Vector_Add(triProjected.p[1], vOffsetView); |
||||||
|
triProjected.p[2] = Vector_Add(triProjected.p[2], vOffsetView); |
||||||
|
triProjected.p[0].x *= 0.5f * (float)ScreenWidth(); |
||||||
|
triProjected.p[0].y *= 0.5f * (float)ScreenHeight(); |
||||||
|
triProjected.p[1].x *= 0.5f * (float)ScreenWidth(); |
||||||
|
triProjected.p[1].y *= 0.5f * (float)ScreenHeight(); |
||||||
|
triProjected.p[2].x *= 0.5f * (float)ScreenWidth(); |
||||||
|
triProjected.p[2].y *= 0.5f * (float)ScreenHeight(); |
||||||
|
|
||||||
|
vecTrianglesToRaster.push_back(triProjected); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
//std::sort(vecTrianglesToRaster.begin(),vecTrianglesToRaster.end(),[](triangle&t1,triangle&t2){return (t1.p[0].z+t1.p[1].z+t1.p[2].z)/3.0f>(t2.p[0].z+t2.p[1].z+t2.p[2].z)/3.0f;});
|
||||||
|
ClearBuffer(BLACK, true); |
||||||
|
int triRenderCount = 0; |
||||||
|
for (auto& triToRaster : vecTrianglesToRaster) { |
||||||
|
|
||||||
|
triangle clipped[2]; |
||||||
|
std::list<triangle>listTriangles; |
||||||
|
listTriangles.push_back(triToRaster); |
||||||
|
int nNewTriangles = 1; |
||||||
|
|
||||||
|
for (int p = 0; p < 4; p++) |
||||||
|
{ |
||||||
|
int nTrisToAdd = 0; |
||||||
|
while (nNewTriangles > 0) |
||||||
|
{ |
||||||
|
// Take triangle from front of queue
|
||||||
|
triangle test = listTriangles.front(); |
||||||
|
listTriangles.pop_front(); |
||||||
|
nNewTriangles--; |
||||||
|
|
||||||
|
// Clip it against a plane. We only need to test each
|
||||||
|
// subsequent plane, against subsequent new triangles
|
||||||
|
// as all triangles after a plane clip are guaranteed
|
||||||
|
// to lie on the inside of the plane. I like how this
|
||||||
|
// comment is almost completely and utterly justified
|
||||||
|
switch (p) |
||||||
|
{ |
||||||
|
case 0: nTrisToAdd = Triangle_ClipAgainstPlane({ 0.0f, 0.0f, 0.0f }, { 0.0f, 1.0f, 0.0f }, test, clipped[0], clipped[1]); break; |
||||||
|
case 1: nTrisToAdd = Triangle_ClipAgainstPlane({ 0.0f, (float)ScreenHeight() - 1, 0.0f }, { 0.0f, -1.0f, 0.0f }, test, clipped[0], clipped[1]); break; |
||||||
|
case 2: nTrisToAdd = Triangle_ClipAgainstPlane({ 0.0f, 0.0f, 0.0f }, { 1.0f, 0.0f, 0.0f }, test, clipped[0], clipped[1]); break; |
||||||
|
case 3: nTrisToAdd = Triangle_ClipAgainstPlane({ (float)ScreenWidth() - 1, 0.0f, 0.0f }, { -1.0f, 0.0f, 0.0f }, test, clipped[0], clipped[1]); break; |
||||||
|
} |
||||||
|
|
||||||
|
// Clipping may yield a variable number of triangles, so
|
||||||
|
// add these new ones to the back of the queue for subsequent
|
||||||
|
// clipping against next planes
|
||||||
|
for (int w = 0; w < nTrisToAdd; w++) |
||||||
|
listTriangles.push_back(clipped[w]); |
||||||
|
} |
||||||
|
nNewTriangles = listTriangles.size(); |
||||||
|
} |
||||||
|
|
||||||
|
for (auto& t : listTriangles) { |
||||||
|
// Rasterize triangle
|
||||||
|
SetDecalStructure(DecalStructure::LIST); |
||||||
|
SetDecalMode(DecalMode::NORMAL); |
||||||
|
DrawPolygonDecal(texture, { |
||||||
|
{t.p[0].x, t.p[0].y}, |
||||||
|
{t.p[1].x, t.p[1].y}, |
||||||
|
{t.p[2].x, t.p[2].y} |
||||||
|
}, { |
||||||
|
{t.uv[0].u,t.uv[0].v}, |
||||||
|
{t.uv[1].u,t.uv[1].v}, |
||||||
|
{t.uv[2].u,t.uv[2].v}, |
||||||
|
}, { t.uv[0].w,t.uv[1].w,t.uv[2].w }, { t.p[0].z,t.p[1].z,t.p[2].z }, { t.col,t.col,t.col }); |
||||||
|
/*SetDecalMode(DecalMode::WIREFRAME);
|
||||||
|
DrawPolygonDecal(nullptr,{ |
||||||
|
{t.p[0].x, t.p[0].y}, |
||||||
|
{t.p[1].x, t.p[1].y}, |
||||||
|
{t.p[2].x, t.p[2].y} |
||||||
|
},{ |
||||||
|
{0,0}, |
||||||
|
{0,0}, |
||||||
|
{0,0}, |
||||||
|
},WHITE);*/ |
||||||
|
SetDecalStructure(DecalStructure::FAN); |
||||||
|
triRenderCount++; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
SetDecalMode(DecalMode::NORMAL); |
||||||
|
DrawStringDecal({ 0,0 }, "Triangles: " + std::to_string(triRenderCount), WHITE, { 2,2 }); |
||||||
|
|
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
}; |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
int main() |
||||||
|
{ |
||||||
|
olcEngine3D demo; |
||||||
|
if (demo.Construct(1280, 720, 1, 1)) |
||||||
|
demo.Start(); |
||||||
|
return 0; |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue