Skip to content

Drawing Lines

Note

In this lesson, we also introduce the TSL named imports of smoothstep, clamp, dot, max, mix, mod, negate.

Video Lecture

Section Video Links
Lines Part 1 Lines Part 1
Lines Part 2 Lines Part 2

Working Example

<>

Start Script - Part 1

./src/main.ts

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import './style.css'
import * as THREE from 'three/webgpu'
import {
  abs,
  clamp,
  cos,
  dot,
  Fn,
  length,
  max,
  mix,
  mod,
  negate,
  oneMinus,
  positionLocal,
  sin,
  smoothstep,
  step,
  time,
  vec2,
  vec3,
} from 'three/tsl'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'

const scene = new THREE.Scene()

const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  10
)
camera.position.z = 1

const renderer = new THREE.WebGPURenderer()
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
renderer.setAnimationLoop(animate)

window.addEventListener('resize', function () {
  camera.aspect = window.innerWidth / window.innerHeight
  camera.updateProjectionMatrix()
  renderer.setSize(window.innerWidth, window.innerHeight)
})

const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true

// //@ts-ignore
// const Line = Fn(([position, direction, distance, thickness]) => {
//   const projection = dot(position, direction) // scalar projection

//   const line = projection
//   //const line = position.sub(projection)
//   //const line = position.sub(projection.mul(direction))
//   //const line = length(position.sub(projection.mul(direction)))
//   //const line = step(thickness, length(position.sub(projection.mul(direction)))).oneMinus()
//   //const line = smoothstep(thickness, 0.0, length(position.sub(projection.mul(direction))))

//   // const clampedProjection = clamp(projection, 0.0, distance)
//   // const line = smoothstep(
//   //   thickness,
//   //   0.0,
//   //   length(position.sub(clampedProjection.mul(direction)))
//   // )
//   return line
// })

//@ts-ignore
const Circle = Fn(([position, radius, thickness]) => {
  const distance = position
  //const distance = length(position)
  //const distance = length(position).sub(radius)
  return distance
  //return abs(distance)
  //return step(thickness, abs(distance)) //.oneMinus()
  //return smoothstep(thickness, 0, abs(distance))
})

const main = Fn(() => {
  const p = positionLocal.toVar()
  //p.mulAssign(2)

  const radius = 0.5
  const thickness = 0.01

  const circle = Circle(p, radius, thickness)

  // const circleOffset = vec2(-0.66, 0.66)
  // const circle = Circle(p.sub(circleOffset), radius, thickness)

  return circle

  // const finalColour = mix(circle, vec3(1, 0, 0), radiusLine)
  // return finalColour
})

const material = new THREE.NodeMaterial()
material.fragmentNode = main()

const mesh = new THREE.Mesh(new THREE.PlaneGeometry(), material)
scene.add(mesh)

// renderer.debug.getShaderAsync(scene, camera, mesh).then((e) => {
//   //console.log(e.vertexShader)
//   console.log(e.fragmentShader)
// })

function animate() {
  controls.update()

  renderer.render(scene, camera)
}

Start Script - Part 2

./src/main.ts

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import './style.css'
import * as THREE from 'three/webgpu'
import {
  abs,
  clamp,
  cos,
  dot,
  Fn,
  length,
  max,
  mix,
  mod,
  negate,
  oneMinus,
  positionLocal,
  sin,
  smoothstep,
  step,
  time,
  vec2,
  vec3,
} from 'three/tsl'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'

const scene = new THREE.Scene()

const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  10
)
camera.position.z = 1

const renderer = new THREE.WebGPURenderer()
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
renderer.setAnimationLoop(animate)

window.addEventListener('resize', function () {
  camera.aspect = window.innerWidth / window.innerHeight
  camera.updateProjectionMatrix()
  renderer.setSize(window.innerWidth, window.innerHeight)
})

const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true

//@ts-ignore
const Line = Fn(([position, direction, distance, thickness]) => {
  const projection = dot(position, direction) // scalar projection
  const clampedProjection = clamp(projection, 0.0, distance)
  const line = smoothstep(
    thickness,
    0.0,
    length(position.sub(clampedProjection.mul(direction)))
  )
  return line
})

//@ts-ignore
const Circle = Fn(([position, radius, thickness]) => {
  const distance = length(position).sub(radius)
  return smoothstep(thickness, 0, abs(distance))
})

const main = Fn(() => {
  const p = positionLocal.toVar()
  p.mulAssign(2)

  const radius = 0.5
  const thickness = 0.01

  const circleOffset = vec2(-0.66, 0.66)
  const circle = Circle(p.sub(circleOffset), radius, thickness)

  const angle = time
  const direction = vec2(cos(angle), sin(angle))

  const radiusLine = Line(p.sub(circleOffset), direction, radius, thickness)
  //const radiusEndPosition = direction.mul(radius)

  //const frequency = Math.PI * 2

  //const sineWave = p.y
  //const sineWave = p.y.sub(sin(p.x))
  //const sineWave = abs(p.y.sub(sin(p.x)))
  //const sineWave = smoothstep(thickness, 0, abs(p.y.sub(sin(p.x))))
  //const sineWave = smoothstep(thickness, 0, abs(p.y.sub(sin(p.x.mul(frequency))))) // add some frequency to shrink it on p.x
  //const sineWave = smoothstep(thickness, 0, abs(p.y.sub(sin(p.x.mul(frequency)).mul(0.5)))) // halve the height
  //const sineWave = smoothstep(thickness, 0, abs(p.y.sub(circleOffset.y).sub(sin(p.x.mul(frequency)).mul(0.5))))
  //const sineWave = smoothstep(thickness, 0, abs(p.y.sub(circleOffset.y).sub(sin(p.x.mul(frequency).sub(time)).mul(0.5))))  // add time
  //const sineWave = smoothstep(thickness, 0, abs(p.y.sub(circleOffset.y).sub(sin(p.x.mul(frequency).sub(time).sub(Math.PI)).mul(0.5)))) // offset it
  //sineWave.assign(step(0.01, p.x).mul(sineWave)) // draw only part of it right of p.x = 0.01

  const finalColour = mix(circle, vec3(1, 0, 0), radiusLine)
  //finalColour.assign(mix(finalColour, vec3(0, 1, 0), sineWave))

  return finalColour
})

const material = new THREE.NodeMaterial()
material.fragmentNode = main()

const mesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material)
scene.add(mesh)

// renderer.debug.getShaderAsync(scene, camera, mesh).then((e) => {
//   //console.log(e.vertexShader)
//   console.log(e.fragmentShader)
// })

function animate() {
  controls.update()

  renderer.render(scene, camera)
}

Final Script

./src/main.ts

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
import './style.css'
import * as THREE from 'three/webgpu'
import {
  abs,
  clamp,
  cos,
  dot,
  Fn,
  length,
  max,
  mix,
  mod,
  negate,
  oneMinus,
  positionLocal,
  sin,
  smoothstep,
  step,
  time,
  vec2,
  vec3,
} from 'three/tsl'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'

const scene = new THREE.Scene()

const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  10
)
camera.position.z = 1

const renderer = new THREE.WebGPURenderer()
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
renderer.setAnimationLoop(animate)

window.addEventListener('resize', function () {
  camera.aspect = window.innerWidth / window.innerHeight
  camera.updateProjectionMatrix()
  renderer.setSize(window.innerWidth, window.innerHeight)
})

const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true

//@ts-ignore
const Line = Fn(([position, direction, distance, thickness]) => {
  const projection = dot(position, direction) // scalar projection
  const clampedProjection = clamp(projection, 0.0, distance)
  const line = smoothstep(
    thickness,
    0.0,
    length(position.sub(clampedProjection.mul(direction)))
  )
  return line
})

//@ts-ignore
const Circle = Fn(([position, radius, thickness]) => {
  const distance = length(position).sub(radius)
  return smoothstep(thickness, 0, abs(distance))
})

const main = Fn(() => {
  const p = positionLocal.toVar()
  p.mulAssign(2)

  const radius = 0.5
  const thickness = 0.01

  const circleOffset = vec2(-0.66, 0.66)
  const circle = Circle(p.sub(circleOffset), radius, thickness)

  const angle = time
  const direction = vec2(cos(angle), sin(angle))

  const radiusLine = Line(p.sub(circleOffset), direction, radius, thickness)
  const radiusEndPosition = direction.mul(radius)

  const frequency = Math.PI * 2

  const sineWave = smoothstep(
    thickness,
    0,
    abs(
      p.y
        .sub(circleOffset.y)
        .sub(sin(p.x.mul(frequency).sub(time).sub(Math.PI)).mul(0.5))
    )
  ) // offset it
  sineWave.assign(step(0, p.x).mul(sineWave)) // draw only part of it right of p.x = 0

  const cosineWave = smoothstep(
    thickness,
    0,
    abs(
      p.x.sub(circleOffset.x).sub(cos(p.y.mul(frequency).add(time)).mul(0.5))
    )
  ) // offset it
  cosineWave.assign(step(p.y, 0).mul(cosineWave)) // draw only part of it bottom of p.y = 0

  const sineReferenceLine = Line(
    p.sub(circleOffset).sub(radiusEndPosition),
    vec2(1, 0),
    negate(radiusEndPosition.x).sub(circleOffset.x),
    thickness
  )
  const cosineReferenceLine = Line(
    p.sub(circleOffset).sub(radiusEndPosition),
    vec2(0, -1),
    radiusEndPosition.y.add(circleOffset.y),
    thickness
  )

  const dottedSineReferenceLine = sineReferenceLine.mul(
    step(0.5, mod(p.x.mul(10), 1))
  )

  const dottedCosineReferenceLine = cosineReferenceLine.mul(
    step(0.5, mod(p.y.mul(10), 1))
  )

  const finalColour = mix(circle, vec3(1, 0, 0), radiusLine)
  finalColour.assign(mix(finalColour, vec3(0, 1, 0), sineWave))
  finalColour.assign(mix(finalColour, vec3(0, 1, 0), cosineWave))
  finalColour.assign(mix(finalColour, vec3(1, 1, 0), dottedSineReferenceLine))
  finalColour.assign(
    mix(finalColour, vec3(1, 1, 0), dottedCosineReferenceLine)
  )

  return finalColour
})

const material = new THREE.NodeMaterial()
material.fragmentNode = main()

const mesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material)
scene.add(mesh)

// renderer.debug.getShaderAsync(scene, camera, mesh).then((e) => {
//   //console.log(e.vertexShader)
//   console.log(e.fragmentShader)
// })

function animate() {
  controls.update()

  renderer.render(scene, camera)
}