#include "Affine2DMatrix.fx" #include "MatrixDecomposition.fx" const float2x3 Camera : Camera; const float2x3 View : View; const float2x3 Projection : Projection; const float2 ViewportSize : ViewportSize; static const float2 UnitSquareVertices[4] = { float2(-1, -1), float2(1, -1), float2(1, 1), float2(-1, 1), }; static const float2 TextureCoordinates[4] = { float2(0, 0), float2(1, 0), float2(0, 1), float2(1, 1), }; static const float2x3 Viewport = { ViewportSize.x/2, 0, floor(ViewportSize.x / 2), 0, -ViewportSize.y/2, floor(ViewportSize.y / 2), }; /// /// Abreviations: /// MS - Model Space (origin at center of object) /// WS - World Space /// PS - Pixel Space /// CS - Clip Space static const float SHAPE_BOX = 1; static const float SHAPE_CIRCLE = 2; static const float SHAPE_SPRITE = 3; static const float FIRST_NON_BASIC_PRIMITIVE = 4; static const float SHAPE_TRIANGLE = 4; static const float SHAPE_QUADRATIC_BEZIER_CURVE = 5; Texture2D texture0; SamplerState samplerState { MinFilter = point; MagFilter = point; MipFilter = none; AddressU = Clamp; AddressV = Clamp; }; struct Vertex_IN { float4 TranslationAndPosition : POSITION0; float4 ScaleAndOrientation : POSITION1; // Border in pixels, Border Percentage, BorderStyle, Type float4 BorderStyle : TEXCOORD0; float4 InteriorColor : COLOR0; float4 BorderColor : COLOR1; // Circle, box user data: // x = index of vertex // // Triangle user data: // (xy) = first point of border plane // (zw) = second point of border plane // // Sprite user data: // x = index of vertex // (zw) = uv coordinates of sprite float4 UserData_1 : TEXCOORD1; }; typedef struct //Vertex_OUT, Pixel_IN { float4 PositionCS : POSITION0; float4 InteriorColor : COLOR0; float4 BorderColor : COLOR1; // Border offset in pixel space (not circle or square), unused, BorderStyle (0 = solid, 1 = smooth), Type float4 BorderInformation : TEXCOORD0; // Circle user data: // _1.xy = UV coords in border circle space (interp) // _1.zw = UV coords in interior circle space (interp) // Box user data: // _1.xy = interior box UV coords (interp) // Triangle user data: // _1.xy = clip space coordinate of border line point A (invariant) // _1.zw = clip space coordinate of border line point B (invariant) // Sprite user data: // _1.x = index of vertex // _1.zw = uv coordintes of vertex float4 UserData_1 : TEXCOORD1; }Vertex_OUT, Pixel_IN; struct Pixel_OUT { float4 FinalColor : COLOR; }; Vertex_OUT MainVS(Vertex_IN IN) { ////////////////////////////////////////////// // // Setup return information and pull out some // basic information from what we pulled in // ////////////////////////////////////////////// Vertex_OUT returnMe; { returnMe.InteriorColor = IN.InteriorColor; returnMe.BorderColor = IN.BorderColor; returnMe.BorderInformation = IN.BorderStyle; returnMe.PositionCS = float4(0, 0, 0, 1); returnMe.UserData_1 = 0; } float2 WORLD_TRANSLATION = IN.TranslationAndPosition.xy; float2 PRIMITIVE_POSITION = IN.TranslationAndPosition.zw; float BORDER_RADIUS_PIXEL = IN.BorderStyle.x; float BORDER_RADIUS_PERCENT = IN.BorderStyle.y; float SHAPE_TYPE = IN.BorderStyle.w; float2x3 World = { IN.ScaleAndOrientation.xy, WORLD_TRANSLATION.x, IN.ScaleAndOrientation.zw, WORLD_TRANSLATION.y, }; float2x3 WCV = Affine2D_Mul(Affine2D_Mul(View, Camera), World); // Figure out how thick the border needs to be if(SHAPE_TYPE == SHAPE_CIRCLE) { // For a circle, model space vertices are the same as the UV coords we're going to use. // Decompose the current WCV transform in to SVD form U * Scale * V float2x2 U, V; float2 scale; { float2x2 rot = { WCV[0].xy, WCV[1].xy, }; SVD_FullDecompose(rot, U, scale, V); } // Scale the diagonal scale matrix of the SVD by the percentage float2 scalePerc_InPixels = scale * BORDER_RADIUS_PERCENT; // Make sure the actual thickness of the border is at least the proper number of pixels float borderThickness = max(BORDER_RADIUS_PIXEL, min(scalePerc_InPixels.x, scalePerc_InPixels.y)); borderThickness = min(min(scale.x, scale.y), borderThickness); // Find the least squares UV coords for the inner ellipse that will best match to a uniform thickness { int index = (int)IN.UserData_1.x; returnMe.UserData_1.xy = UnitSquareVertices[index]; float2x2 newScale = { scale.x / (scale.x - borderThickness), 0, 0, scale.y / (scale.y - borderThickness), }; float2x2 newXForm = mul(mul(transpose(V), newScale), V); returnMe.UserData_1.zw = mul(newXForm, UnitSquareVertices[index]); } } else if(SHAPE_TYPE == SHAPE_BOX) { float3x2 WCV_Transpose = transpose(WCV); float2x2 axesPS = { WCV_Transpose[0], WCV_Transpose[1], }; float2 lengths = float2(length(axesPS[0]), length(axesPS[1])); float2x2 normalizedAxes = { axesPS[0] / lengths.x, axesPS[1] / lengths.y, }; float borderSizePerc = BORDER_RADIUS_PERCENT * min(lengths[0], lengths[1]); float r = max(borderSizePerc, BORDER_RADIUS_PIXEL); float m = sqrt((2 * r * r) / (1 - dot(normalizedAxes[0], normalizedAxes[1]))); m = min(m, min(lengths.x, lengths.y)); float2 l_inv = 1 - m / lengths; float2 l = 1.0/l_inv; { int index = IN.UserData_1.x; returnMe.UserData_1.xy = l * UnitSquareVertices[index]; } } else if(SHAPE_TYPE == SHAPE_TRIANGLE) { // Border line in model space. float2 A = IN.UserData_1.xy; float2 B = IN.UserData_1.zw; float2 A_PS = Affine2D_MulPoint(Viewport, Affine2D_MulPoint(Projection, Affine2D_MulPoint(WCV, A))); float2 B_PS = Affine2D_MulPoint(Viewport, Affine2D_MulPoint(Projection, Affine2D_MulPoint(WCV, B))); returnMe.UserData_1.xy = A_PS; returnMe.UserData_1.zw = B_PS; // By convention, BORDER_RADIUS_PERCENT is actually distance in model space for triangles. float borderRadiusDistance_MS = BORDER_RADIUS_PERCENT; // Assumes uniform scale (nonuniform scale breaks the straight skeleton decomposition anyway). float borderRadiusDistance_PS = length(WCV[0]) * borderRadiusDistance_MS; returnMe.BorderInformation.x = max(BORDER_RADIUS_PIXEL, borderRadiusDistance_PS); if (returnMe.BorderInformation.x == 0) { // To prevent numerical noise from causing pixels to get // included in the border when there is no border returnMe.BorderInformation.x = 1; } } else if (SHAPE_TYPE == SHAPE_SPRITE) { returnMe.UserData_1 = IN.UserData_1; } else if (SHAPE_TYPE == SHAPE_QUADRATIC_BEZIER_CURVE) { returnMe.UserData_1 = IN.UserData_1; } // Clip space position of vertex { float2 vertexPositionMS = PRIMITIVE_POSITION; if (SHAPE_TYPE < FIRST_NON_BASIC_PRIMITIVE) { int index = IN.UserData_1.x; vertexPositionMS += UnitSquareVertices[index]; } float2 vertexPositionPS = Affine2D_MulPoint(WCV, vertexPositionMS); returnMe.PositionCS.xy = Affine2D_MulPoint(Projection, vertexPositionPS); } // Want to transform from alpha = transparency to alpha = opaqueness. returnMe.BorderColor.a = 1 - returnMe.BorderColor.a; returnMe.InteriorColor.a = 1 - returnMe.InteriorColor.a; return returnMe; } float cross2D(float2 a, float2 b) { return cross(float3(a, 0), float3(b, 0)).z; } Pixel_OUT MainPS(Pixel_IN IN, float2 PIXEL_SPACE_POS : VPOS) { Pixel_OUT returnMe; float isInsideBorder; float isInsideInterior; // DX SM3 adds a half offset to pixel position, so we need to undo that. PIXEL_SPACE_POS = floor(PIXEL_SPACE_POS); float SHAPE_TYPE = IN.BorderInformation.w; if (SHAPE_TYPE == SHAPE_CIRCLE) { // sqrt(x^2 + y^2) <= 1 isInsideBorder = step(length(IN.UserData_1.xy), 1); // sqrt(x^2 + y^2) <= 1 isInsideInterior = step(length(IN.UserData_1.zw), 1); } else if (SHAPE_TYPE == SHAPE_BOX) { // Always inside the quad isInsideBorder = 1; float2 subBoxPos = IN.UserData_1.xy; // |x| <= 1 && |y| <= 1 float2 isInsideInteriorXY = step(abs(subBoxPos), 1); isInsideInterior = isInsideInteriorXY.x * isInsideInteriorXY.y; } else if(SHAPE_TYPE == SHAPE_TRIANGLE) { float2 ptA_PS = IN.UserData_1.xy; float2 ptB_PS = IN.UserData_1.zw; // Always inside the triangle since we're a triangle isInsideBorder = 1; // scaledDistance / lengthAB = signed distance of point to border line. float scaledDistance = cross2D(ptA_PS - PIXEL_SPACE_POS, ptB_PS - PIXEL_SPACE_POS); float lengthAB = distance(ptA_PS, ptB_PS); float borderOffset = IN.BorderInformation.x; // scaledDistance / lengthAB <= borderOffset isInsideInterior = step(scaledDistance, lengthAB * borderOffset); } else if (SHAPE_TYPE == SHAPE_QUADRATIC_BEZIER_CURVE) { // See Resolution Independent Curve Rendering using Programmable Graphics Hardware // Charles Loop and Jim Blinn // https://www.microsoft.com/en-us/research/wp-content/uploads/2005/01/p1000-loop.pdf float3 uvPosition = IN.UserData_1.xyz; float aboveOrBelow = uvPosition.z * (uvPosition.x * uvPosition.x - uvPosition.y); isInsideInterior = step(aboveOrBelow, 0); } if (SHAPE_TYPE == SHAPE_SPRITE) { returnMe.FinalColor = tex2D(samplerState, IN.UserData_1.zw); } else { float isInsideBorderButNotInterior = isInsideBorder * (1 - isInsideInterior); // TODO: take in to account smooth border returnMe.FinalColor = isInsideBorder * lerp(IN.InteriorColor, IN.BorderColor, isInsideBorderButNotInterior); } return returnMe; } technique Main { pass Main { VertexShader = compile vs_3_0 MainVS(); PixelShader = compile ps_3_0 MainPS(); } }