├── LICENSE ├── LineDrawing.swift ├── README.md ├── readme_images ├── img1.jpg ├── img2.png ├── img3.png └── recording.gif └── shaders.metal /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 bialylis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LineDrawing.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SceneKit 3 | 4 | extension SCNGeometry { 5 | class func lineThrough(points: [SCNVector3], width:Int = 20, closed: Bool = false, color: CGColor = UIColor.black.cgColor, mitter: Bool = false) -> SCNGeometry { 6 | 7 | // Becouse we cannot use geometry shaders in metal, every point on the line has to be changed into 4 verticles 8 | let vertices: [SCNVector3] = points.flatMap { p in [p, p, p, p] } 9 | 10 | // Create Geometry Source object 11 | let source = SCNGeometrySource(vertices: vertices) 12 | 13 | // Create Geometry Element object 14 | var indices = Array((0...size*lineData.count), forKeyPath: "lineData") 26 | 27 | // map verticles into float3 28 | let floatPoints = vertices.map { float3($0) } 29 | geometry.setValue(NSData(bytes: floatPoints, length: MemoryLayout.size * floatPoints.count), forKeyPath: "vertices") 30 | 31 | // map color into float 32 | let colorFloat = color.components!.map { Float($0) } 33 | geometry.setValue(NSData(bytes: colorFloat, length: MemoryLayout.size * color.numberOfComponents), forKey: "color") 34 | 35 | // Set the shader program 36 | let program = SCNProgram() 37 | program.fragmentFunctionName = "thickLinesFragment" 38 | program.vertexFunctionName = "thickLinesVertex" 39 | geometry.program = program 40 | 41 | return geometry 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ThickRedLine 2 | Thick Red Line - drawing thick lines with constant on-screen width independent of perspective, for SceneKit with metal shaders 3 | 4 | Example: 5 | ```swift 6 | let geometry = SCNGeometry.lineThrough(points: [SCNVector3(0, 0,0), SCNVector3(0, 10, 0), SCNVector3(10, 10, 0)], 7 | width: 20, 8 | closed: false, 9 | color: UIColor.red.cgColor) 10 | let node = SCNNode(geometry: geometry) 11 | scene.rootNode.addChildNode(node) 12 | ``` 13 | ![Thick line gif](https://github.com/bialylis/ThickRedLine/blob/master/readme_images/recording.gif "Animated gif of of thick red line") 14 | 15 | Parameters: 16 | + points - array of SCNVector3 indicating points on the line 17 | + width - (int) width of line in points 18 | + closed - (bool) if line should for a loop 19 | + color - (CGColor) color of the line 20 | + mitter - (bool) if line should form a sharp mitter at the joints. Feature is WIP - not supported with closed (the first and last joint will not be mittered) and there are artefacts when angle between the lines is too small 21 | 22 | ![img1](https://github.com/bialylis/ThickRedLine/blob/master/readme_images/img1.jpg) 23 | ![img2](https://github.com/bialylis/ThickRedLine/blob/master/readme_images/img2.png) 24 | ![img3](https://github.com/bialylis/ThickRedLine/blob/master/readme_images/img3.png) 25 | 26 | 27 | -------------------------------------------------------------------------------- /readme_images/img1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bialylis/ThickRedLine/d4ec5cd4184c29afc863c63d6d44c0b3e5dd9b0b/readme_images/img1.jpg -------------------------------------------------------------------------------- /readme_images/img2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bialylis/ThickRedLine/d4ec5cd4184c29afc863c63d6d44c0b3e5dd9b0b/readme_images/img2.png -------------------------------------------------------------------------------- /readme_images/img3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bialylis/ThickRedLine/d4ec5cd4184c29afc863c63d6d44c0b3e5dd9b0b/readme_images/img3.png -------------------------------------------------------------------------------- /readme_images/recording.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bialylis/ThickRedLine/d4ec5cd4184c29afc863c63d6d44c0b3e5dd9b0b/readme_images/recording.gif -------------------------------------------------------------------------------- /shaders.metal: -------------------------------------------------------------------------------- 1 | #include 2 | using namespace metal; 3 | #include 4 | 5 | struct MyNodeBuffer { 6 | float4x4 modelViewProjectionTransform; 7 | }; 8 | 9 | struct lineDataBuffer { 10 | int width; 11 | int verticleCount; 12 | int miter; 13 | int loop; 14 | }; 15 | 16 | struct SimpleVertex 17 | { 18 | float4 position [[position]]; 19 | float4 color; 20 | }; 21 | 22 | vertex SimpleVertex thickLinesVertex(constant SCNSceneBuffer& scn_frame [[buffer(0)]], 23 | constant MyNodeBuffer& scn_node [[buffer(1)]], 24 | constant float3* vertices [[buffer(2)]], 25 | constant lineDataBuffer& lineData [[buffer(3)]], 26 | constant float4* color [[buffer(4)]], 27 | uint v_id [[vertex_id]]) 28 | { 29 | 30 | uint point_id = v_id/4; //line point id (not vertex) 31 | float sign = v_id%2?-1:1; //should point be up or down in line 32 | 33 | SimpleVertex vert; 34 | vert.color = *color; //pass the color data to fragment shader 35 | 36 | float2 aspect = float2( scn_frame.viewportSize.x / scn_frame.viewportSize.y, 1); //aspect ratio 37 | 38 | vert.position = scn_node.modelViewProjectionTransform * float4(vertices[v_id], 1.0); //position of the point 39 | 40 | if (lineData.miter == 0 || point_id == 0 || point_id + 1 == uint(lineData.verticleCount)){ 41 | //Active when there it's first point, last point or no mitter middle point 42 | //TODO: get rid of conditionals 43 | 44 | uint currentPointToProcess; 45 | uint nextPointToProcess; 46 | 47 | int lineToNext = 0; //should line be calcualted from current point to next, or from current to previous 48 | lineToNext |= point_id == 0 && !lineData.loop; //always go to next if its first point 49 | lineToNext |= point_id*4+2 == v_id; //always go to next if its third or fourth vertex of current point 50 | lineToNext |= point_id*4+3 == v_id; 51 | lineToNext &= point_id + 1 != uint(lineData.verticleCount) || lineData.loop; //always go to prevous if its a last point 52 | 53 | currentPointToProcess = (point_id-1+lineToNext + lineData.verticleCount) % lineData.verticleCount; 54 | nextPointToProcess = (point_id+lineToNext) % lineData.verticleCount; 55 | 56 | //calculate MVP transform for both points 57 | float4 currentProjection = scn_node.modelViewProjectionTransform * float4(vertices[currentPointToProcess*4], 1.0); 58 | float4 nextProjection = scn_node.modelViewProjectionTransform * float4(vertices[nextPointToProcess*4], 1.0); 59 | 60 | //get 2d position in screen space 61 | float2 currentScreen = currentProjection.xy / currentProjection.w * aspect; 62 | float2 nextScreen = nextProjection.xy / nextProjection.w * aspect; 63 | 64 | //get vector of the line 65 | float2 dir = normalize(nextScreen - currentScreen); 66 | //vector of diretion of thickness 67 | float2 normal = float2(-dir.y, dir.x); 68 | normal /= aspect; 69 | 70 | //get thickness in pixels in screen space 71 | float thickness = float(lineData.width)/scn_frame.viewportSize.y; 72 | 73 | //move current point up or down, by thickness, with the same distance independent on depth 74 | vert.position += float4(sign*normal*thickness*vert.position.w, 0, 0 ); 75 | 76 | }else { 77 | //TODO: Switch to normal mode of miter size is to big 78 | //other points, calculate mitter 79 | 80 | //Similar to previus case, but looking always at 3 points - current, prevoius and next 81 | 82 | float4 previousProjection= scn_node.modelViewProjectionTransform * float4(vertices[(point_id - 1)*4], 1.0); 83 | float4 currentProjection= scn_node.modelViewProjectionTransform * float4(vertices[point_id*4], 1.0); 84 | float4 nextProjection = scn_node.modelViewProjectionTransform * float4(vertices[(point_id + 1)*4], 1.0); 85 | 86 | float2 previousScreen = previousProjection.xy / previousProjection.w * aspect; 87 | float2 currentScreen = currentProjection.xy / currentProjection.w * aspect; 88 | float2 nextScreen = nextProjection.xy / nextProjection.w * aspect; 89 | 90 | //vector tangential to the joint 91 | float2 tangent = normalize( normalize(nextScreen-currentScreen) + normalize(currentScreen-previousScreen) ); 92 | 93 | float2 dir = normalize(nextScreen - currentScreen); 94 | float2 normal = float2(-dir.y, dir.x); 95 | 96 | //mitter line - normal to the tangent 97 | float2 miter = float2( -tangent.y, tangent.x ); 98 | 99 | float thickness = float(lineData.width)/scn_frame.viewportSize.y; 100 | 101 | //mitter length - crossing of one of the edges with mitter line 102 | float miterLength = thickness / dot( miter, normal ); 103 | miter /= aspect; 104 | 105 | vert.position += float4(sign*miter*miterLength*vert.position.w, 0,0 ); 106 | } 107 | 108 | return vert; 109 | } 110 | 111 | fragment float4 thickLinesFragment(SimpleVertex in [[stage_in]]) 112 | { 113 | float4 color; 114 | color = in.color; 115 | return color; 116 | } 117 | --------------------------------------------------------------------------------