├── .gitignore ├── LICENSE.txt ├── README.md ├── bundle.go ├── bundle.wasm ├── color ├── color.go ├── gradient.go ├── gradient_test.go └── interpolation.go ├── gltypes ├── arrays.go └── gltypes.go ├── go.mod ├── go.sum ├── index.html ├── main.go ├── makefile ├── models ├── model.go └── stl.go ├── renderer └── renderer.go └── wasm_exec.js /.gitignore: -------------------------------------------------------------------------------- 1 | wasm-rotating-cube 2 | .idea 3 | *.stl 4 | wasm-stl-viewer 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Eric Suedmeier 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WASM STL Viewer 2 | 3 | This project is built on my wasm-rotating-cube project, Check it out for a more basic example. Instead of drawing only a 3D cube, this project draws arbitrary models imported with from STLs. 4 | 5 | ## Demo 6 | 7 | To see this project running check out this link: [https://bobcob7.github.io/wasm-stl-viewer/](https://bobcob7.github.io/wasm-stl-viewer/) 8 | 9 | ## Running 10 | 11 | To run you can use the makefile with `make run` 12 | -------------------------------------------------------------------------------- /bundle.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | "errors" 6 | "fmt" 7 | "math" 8 | "strings" 9 | "syscall/js" 10 | 11 | "github.com/bobcob7/wasm-stl-viewer/gltypes" 12 | "github.com/bobcob7/wasm-stl-viewer/models" 13 | "github.com/bobcob7/wasm-stl-viewer/renderer" 14 | ) 15 | 16 | var ( 17 | gl js.Value 18 | glTypes gltypes.GLTypes 19 | ) 20 | 21 | //// BUFFERS + SHADERS //// 22 | // Shamelessly copied from https://www.tutorialspoint.com/webgl/webgl_cube_rotation.htm // 23 | var verticesNative = []float32{ 24 | -1, -1, -1, 1, -1, -1, 1, 1, -1, -1, 1, -1, 25 | -1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, 1, 26 | -1, -1, -1, -1, 1, -1, -1, 1, 1, -1, -1, 1, 27 | 1, -1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, 28 | -1, -1, -1, -1, -1, 1, 1, -1, 1, 1, -1, -1, 29 | -1, 1, -1, -1, 1, 1, 1, 1, 1, 1, 1, -1, 30 | } 31 | var colorsNative = []float32{ 32 | 5, 3, 7, 5, 3, 7, 5, 3, 7, 5, 3, 7, 33 | 1, 1, 3, 1, 1, 3, 1, 1, 3, 1, 1, 3, 34 | 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 35 | 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 36 | 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 37 | 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 38 | } 39 | var indicesNative = []uint32{ 40 | 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 41 | 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 42 | 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23, 43 | } 44 | 45 | const vertShaderCode = ` 46 | attribute vec3 position; 47 | uniform mat4 Pmatrix; 48 | uniform mat4 Vmatrix; 49 | uniform mat4 Mmatrix; 50 | attribute vec3 color; 51 | varying vec3 vColor; 52 | 53 | void main(void) { 54 | gl_Position = Pmatrix*Vmatrix*Mmatrix*vec4(position, 1.); 55 | vColor = color; 56 | } 57 | ` 58 | const fragShaderCode = ` 59 | precision mediump float; 60 | varying vec3 vColor; 61 | void main(void) { 62 | gl_FragColor = vec4(vColor, 1.); 63 | } 64 | ` 65 | 66 | var reader js.Value 67 | var render renderer.Renderer 68 | var speedSliderXValue js.Value 69 | var speedSliderYValue js.Value 70 | var speedSliderZValue js.Value 71 | var canvasElement js.Value 72 | var currentZoom float32 = 3 73 | 74 | func uploading(this js.Value, args []js.Value) interface{} { 75 | files := this.Get("files") 76 | file := files.Index(0) 77 | currentFileName := file.Get("name").String() 78 | fmt.Println("Uploading", currentFileName) 79 | reader.Call("readAsDataURL", file) 80 | return nil 81 | } 82 | 83 | func parseBase64File(input string) (output []byte, err error) { 84 | searchString := "base64," 85 | index := strings.Index(input, searchString) 86 | if index < 0 { 87 | err = errors.New("Error opening file") 88 | return 89 | } 90 | sBuffer := input[index+len(searchString):] 91 | return base64.StdEncoding.DecodeString(sBuffer) 92 | } 93 | 94 | func uploaded(this js.Value, args []js.Value) interface{} { 95 | fmt.Println("Finished uploading") 96 | result := args[0].Get("target").Get("result").String() 97 | func() { 98 | defer func() { 99 | if r := recover(); r != nil { 100 | fmt.Println("Recovered in upload", r) 101 | js.Global().Call("alert", "Failed to parse file") 102 | } 103 | }() 104 | uploadedFile, err := parseBase64File(result) 105 | if err != nil { 106 | panic(err) 107 | } 108 | stlSolid, err := models.NewSTL(uploadedFile) 109 | if err != nil { 110 | js.Global().Call("alert", "Could not parse file") 111 | } 112 | vert, colors, indices := stlSolid.GetModel() 113 | modelSize := getMaxScalar(vert) 114 | currentZoom = modelSize * 3 115 | render.SetZoom(currentZoom) 116 | render.SetModel(colors, vert, indices) 117 | }() 118 | return nil 119 | } 120 | 121 | func getMaxScalar(vertices []float32) float32 { 122 | var max float32 123 | for baseIndex := 0; baseIndex < len(vertices); baseIndex += 3 { 124 | testScale := scalar(vertices[baseIndex], vertices[baseIndex], vertices[baseIndex]) 125 | if testScale > max { 126 | max = testScale 127 | } 128 | } 129 | return max 130 | } 131 | 132 | func scalar(x float32, y float32, z float32) float32 { 133 | xy := math.Sqrt(float64(x*x + y*y)) 134 | return float32(math.Sqrt(xy*xy + float64(z*z))) 135 | } 136 | 137 | func uploadError(this js.Value, args []js.Value) interface{} { 138 | fmt.Println("Uploading Error") 139 | return nil 140 | } 141 | 142 | func uploadAborted(this js.Value, args []js.Value) interface{} { 143 | fmt.Println("Upload Aborted") 144 | return nil 145 | } 146 | 147 | func main() { 148 | fmt.Println("Returned normally from f.") 149 | 150 | // Init Canvas stuff 151 | doc := js.Global().Get("document") 152 | 153 | canvasResizeCallback := js.FuncOf(canvasResize) 154 | canvasElement = doc.Call("getElementById", "gocanvas") 155 | js.Global().Get("window").Call("addEventListener", "resize", canvasResizeCallback) 156 | 157 | width := canvasElement.Get("clientWidth").Int() 158 | height := canvasElement.Get("clientHeight").Int() 159 | canvasElement.Set("width", width) 160 | canvasElement.Set("height", height) 161 | upload := doc.Call("getElementById", "upload") 162 | newReader := js.Global().Get("FileReader") 163 | reader = newReader.New() 164 | 165 | sliderSpeedXCallback := js.FuncOf(sliderChangeX) 166 | speedSliderX := doc.Call("getElementById", "speedSliderX") 167 | speedSliderX.Call("addEventListener", "input", sliderSpeedXCallback) 168 | speedSliderXValue = doc.Call("getElementById", "speedSliderXValue") 169 | 170 | sliderSpeedYCallback := js.FuncOf(sliderChangeY) 171 | speedSliderY := doc.Call("getElementById", "speedSliderY") 172 | speedSliderY.Call("addEventListener", "input", sliderSpeedYCallback) 173 | speedSliderYValue = doc.Call("getElementById", "speedSliderYValue") 174 | 175 | sliderSpeedZCallback := js.FuncOf(sliderChangeZ) 176 | speedSliderZ := doc.Call("getElementById", "speedSliderZ") 177 | speedSliderZ.Call("addEventListener", "input", sliderSpeedZCallback) 178 | speedSliderZValue = doc.Call("getElementById", "speedSliderZValue") 179 | 180 | zoomChangeCallback := js.FuncOf(zoomChange) 181 | js.Global().Get("window").Call("addEventListener", "wheel", zoomChangeCallback) 182 | 183 | uploadCallback := js.FuncOf(uploading) 184 | uploadedCallback := js.FuncOf(uploaded) 185 | errorUploadCallback := js.FuncOf(uploadError) 186 | uploadAbortedCallback := js.FuncOf(uploadAborted) 187 | defer uploadCallback.Release() 188 | defer uploadedCallback.Release() 189 | defer errorUploadCallback.Release() 190 | defer uploadAbortedCallback.Release() 191 | reader.Call("addEventListener", "load", uploadedCallback) 192 | reader.Call("addEventListener", "error", errorUploadCallback) 193 | reader.Call("addEventListener", "abort", uploadAbortedCallback) 194 | upload.Call("addEventListener", "change", uploadCallback) 195 | 196 | gl = canvasElement.Call("getContext", "webgl") 197 | if gl.IsUndefined() { 198 | gl = canvasElement.Call("getContext", "experimental-webgl") 199 | } 200 | if gl.IsUndefined() { 201 | js.Global().Call("alert", "browser might not support webgl") 202 | return 203 | } 204 | 205 | config := renderer.InitialConfig{ 206 | Width: width, 207 | Height: height, 208 | SpeedX: 0.5, 209 | SpeedY: 0.3, 210 | SpeedZ: 0.2, 211 | Colors: colorsNative, 212 | Vertices: verticesNative, 213 | Indices: indicesNative, 214 | FragmentShaderCode: fragShaderCode, 215 | VertexShaderCode: vertShaderCode, 216 | } 217 | var err error 218 | render, err = renderer.NewRenderer(gl, config) 219 | if err != nil { 220 | js.Global().Call("alert", fmt.Sprintf("Cannot load webgl %v", err)) 221 | return 222 | } 223 | render.SetZoom(currentZoom) 224 | defer render.Release() 225 | 226 | x, y, z := render.GetSpeed() 227 | speedSliderX.Set("value", fmt.Sprint(x)) 228 | speedSliderXValue.Set("innerHTML", fmt.Sprint(x)) 229 | speedSliderY.Set("value", fmt.Sprint(y)) 230 | speedSliderYValue.Set("innerHTML", fmt.Sprint(y)) 231 | speedSliderZ.Set("value", fmt.Sprint(z)) 232 | speedSliderZValue.Set("innerHTML", fmt.Sprint(z)) 233 | 234 | var renderFrame js.Func 235 | renderFrame = js.FuncOf(func(this js.Value, args []js.Value) interface{} { 236 | render.Render(this, args) 237 | js.Global().Call("requestAnimationFrame", renderFrame) 238 | return nil 239 | }) 240 | js.Global().Call("requestAnimationFrame", renderFrame) 241 | 242 | done := make(chan struct{}, 0) 243 | <-done 244 | } 245 | 246 | func canvasResize(this js.Value, args []js.Value) interface{} { 247 | width := canvasElement.Get("clientWidth").Int() 248 | height := canvasElement.Get("clientHeight").Int() 249 | canvasElement.Set("width", width) 250 | canvasElement.Set("height", height) 251 | render.SetSize(height, width) 252 | return nil 253 | } 254 | 255 | func sliderChangeX(this js.Value, args []js.Value) interface{} { 256 | var speed float32 257 | sSpeed := this.Get("value").String() 258 | fmt.Sscan(sSpeed, &speed) 259 | render.SetSpeedX(speed) 260 | speedSliderXValue.Set("innerHTML", sSpeed) 261 | return nil 262 | } 263 | 264 | func sliderChangeY(this js.Value, args []js.Value) interface{} { 265 | var speed float32 266 | sSpeed := this.Get("value").String() 267 | fmt.Sscan(sSpeed, &speed) 268 | render.SetSpeedY(speed) 269 | speedSliderYValue.Set("innerHTML", sSpeed) 270 | return nil 271 | } 272 | 273 | func sliderChangeZ(this js.Value, args []js.Value) interface{} { 274 | var speed float32 275 | sSpeed := this.Get("value").String() 276 | fmt.Sscan(sSpeed, &speed) 277 | render.SetSpeedZ(speed) 278 | speedSliderZValue.Set("innerHTML", sSpeed) 279 | return nil 280 | } 281 | 282 | func zoomChange(this js.Value, args []js.Value) interface{} { 283 | deltaY := args[0].Get("deltaY").Float() 284 | deltaScale := 1 - (float32(deltaY) * 0.001) 285 | currentZoom *= deltaScale 286 | render.SetZoom(currentZoom) 287 | return nil 288 | } 289 | -------------------------------------------------------------------------------- /bundle.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobcob7/wasm-stl-viewer/77289c9d58dade8be6b7d0ab8248ab70b0f644a4/bundle.wasm -------------------------------------------------------------------------------- /color/color.go: -------------------------------------------------------------------------------- 1 | package color 2 | 3 | import ( 4 | "math/rand" 5 | ) 6 | 7 | type Color struct { 8 | Red float32 9 | Green float32 10 | Blue float32 11 | } 12 | 13 | func NewRandomColor() Color { 14 | return Color{ 15 | rand.Float32(), 16 | rand.Float32(), 17 | rand.Float32(), 18 | } 19 | } 20 | 21 | func (c Color) Subtract(d Color) Color { 22 | return Color{ 23 | c.Red - d.Red, 24 | c.Green - d.Green, 25 | c.Blue - d.Blue, 26 | } 27 | } 28 | 29 | func (c Color) Add(d Color) Color { 30 | return Color{ 31 | c.Red + d.Red, 32 | c.Green + d.Green, 33 | c.Blue + d.Blue, 34 | } 35 | } 36 | 37 | func (c Color) MultiplyFloat(x float32) Color { 38 | return Color{ 39 | c.Red * x, 40 | c.Green * x, 41 | c.Blue * x, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /color/gradient.go: -------------------------------------------------------------------------------- 1 | package color 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | func GenerateGradient(numColors int, steps int) []Color { 8 | // Evenly space colors 9 | distribution := distributeColors(numColors, steps) 10 | colors := make([]Color, numColors) 11 | for i := 0; i < numColors; i++ { 12 | colors[i] = NewRandomColor() 13 | } 14 | // Interpolate between colors 15 | outputBuffer := make([]Color, 0, steps) 16 | for index := 0; index < numColors; index++ { 17 | if index >= numColors-1 { 18 | size := steps - distribution[index] 19 | interpolation := NewColorInterpolation(colors[index-1], colors[index]) 20 | buffer := generateSingleGradient(interpolation, size) 21 | outputBuffer = append(outputBuffer, buffer...) 22 | break 23 | } 24 | currentStep := distribution[index] 25 | nextStep := distribution[index+1] 26 | size := nextStep - currentStep 27 | interpolation := NewColorInterpolation(colors[index], colors[index+1]) 28 | buffer := generateSingleGradient(interpolation, size) 29 | outputBuffer = append(outputBuffer, buffer...) 30 | } 31 | // Create output 32 | return outputBuffer 33 | } 34 | 35 | func distributeColors(numColors int, steps int) []int { 36 | diff := int(math.Ceil(float64(steps) / float64(numColors))) 37 | output := make([]int, numColors) 38 | for i := 0; i < numColors; i++ { 39 | output[i] = diff * i 40 | } 41 | return output 42 | } 43 | 44 | func generateSingleGradient(c ColorInterpolation, numSteps int) []Color { 45 | output := make([]Color, numSteps) 46 | for i := 0; i < numSteps; i++ { 47 | percent := float32(i) / float32(numSteps) 48 | output[i] = c.Interpolate(percent) 49 | } 50 | return output 51 | } 52 | -------------------------------------------------------------------------------- /color/gradient_test.go: -------------------------------------------------------------------------------- 1 | package color 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func Test_distributeColors(t *testing.T) { 9 | type args struct { 10 | numColors int 11 | steps int 12 | } 13 | tests := []struct { 14 | name string 15 | args args 16 | want []int 17 | }{ 18 | { 19 | name: "3 Colors", 20 | args: args{ 21 | numColors: 3, 22 | steps: 5, 23 | }, 24 | want: []int{0, 2, 4}, 25 | }, 26 | { 27 | name: "Extra 1 Color", 28 | args: args{ 29 | numColors: 3, 30 | steps: 7, 31 | }, 32 | want: []int{0, 3, 6}, 33 | }, 34 | { 35 | name: "Extra 2 Colors", 36 | args: args{ 37 | numColors: 3, 38 | steps: 8, 39 | }, 40 | want: []int{0, 3, 6}, 41 | }, 42 | { 43 | name: "Extra 3 Colors", 44 | args: args{ 45 | numColors: 3, 46 | steps: 9, 47 | }, 48 | want: []int{0, 3, 6}, 49 | }, 50 | { 51 | name: "Extra 1 Color 10", 52 | args: args{ 53 | numColors: 3, 54 | steps: 10, 55 | }, 56 | want: []int{0, 4, 8}, 57 | }, 58 | } 59 | for _, tt := range tests { 60 | t.Run(tt.name, func(t *testing.T) { 61 | if got := distributeColors(tt.args.numColors, tt.args.steps); !reflect.DeepEqual(got, tt.want) { 62 | t.Errorf("distributeColors() = %v, want %v", got, tt.want) 63 | } 64 | }) 65 | } 66 | } 67 | 68 | func TestNewRandomGradient(t *testing.T) { 69 | type args struct { 70 | numColors int 71 | steps int 72 | } 73 | tests := []struct { 74 | name string 75 | args args 76 | wantLen int 77 | want Gradient 78 | }{ 79 | { 80 | name: "3 Colors", 81 | args: args{ 82 | numColors: 3, 83 | steps: 5, 84 | }, 85 | want: Gradient{[]Color{}}, 86 | }, 87 | { 88 | name: "Extra 2 Colors", 89 | args: args{ 90 | numColors: 3, 91 | steps: 8, 92 | }, 93 | want: Gradient{[]Color{}}, 94 | }, 95 | } 96 | for _, tt := range tests { 97 | t.Run(tt.name, func(t *testing.T) { 98 | got := NewRandomGradient(tt.args.numColors, tt.args.steps) 99 | if len(got.colors) != tt.args.steps { 100 | t.Errorf("NewRandomGradient().Len = %d, want %d", len(got.colors), tt.args.steps) 101 | } 102 | }) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /color/interpolation.go: -------------------------------------------------------------------------------- 1 | package color 2 | 3 | func NewColorInterpolation(a Color, b Color) ColorInterpolation { 4 | return ColorInterpolation{ 5 | a, 6 | b, 7 | a.Subtract(b), 8 | } 9 | } 10 | 11 | type ColorInterpolation struct { 12 | startColor Color 13 | endColor Color 14 | deltaColor Color 15 | } 16 | 17 | func (c ColorInterpolation) Interpolate(percent float32) Color { 18 | scaled := c.deltaColor.MultiplyFloat(percent) 19 | return c.startColor.Add(scaled) 20 | } 21 | -------------------------------------------------------------------------------- /gltypes/arrays.go: -------------------------------------------------------------------------------- 1 | package gltypes 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "runtime" 7 | "syscall/js" 8 | "unsafe" 9 | ) 10 | 11 | // Shamelessly stolen from: https://github.com/golang/go/issues/32402 12 | // Thanks hajimehoshi 13 | func sliceToByteSlice(s interface{}) []byte { 14 | switch s := s.(type) { 15 | case []int8: 16 | h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) 17 | return *(*[]byte)(unsafe.Pointer(h)) 18 | case []int16: 19 | h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) 20 | h.Len *= 2 21 | h.Cap *= 2 22 | return *(*[]byte)(unsafe.Pointer(h)) 23 | case []int32: 24 | h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) 25 | h.Len *= 4 26 | h.Cap *= 4 27 | return *(*[]byte)(unsafe.Pointer(h)) 28 | case []int64: 29 | h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) 30 | h.Len *= 8 31 | h.Cap *= 8 32 | return *(*[]byte)(unsafe.Pointer(h)) 33 | case []uint8: 34 | return s 35 | case []uint16: 36 | h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) 37 | h.Len *= 2 38 | h.Cap *= 2 39 | return *(*[]byte)(unsafe.Pointer(h)) 40 | case []uint32: 41 | h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) 42 | h.Len *= 4 43 | h.Cap *= 4 44 | return *(*[]byte)(unsafe.Pointer(h)) 45 | case []uint64: 46 | h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) 47 | h.Len *= 8 48 | h.Cap *= 8 49 | return *(*[]byte)(unsafe.Pointer(h)) 50 | case []float32: 51 | h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) 52 | h.Len *= 4 53 | h.Cap *= 4 54 | return *(*[]byte)(unsafe.Pointer(h)) 55 | case []float64: 56 | h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) 57 | h.Len *= 8 58 | h.Cap *= 8 59 | return *(*[]byte)(unsafe.Pointer(h)) 60 | default: 61 | panic(fmt.Sprintf("jsutil: unexpected value at sliceToBytesSlice: %T", s)) 62 | } 63 | } 64 | func SliceToTypedArray(s interface{}) js.Value { 65 | switch s := s.(type) { 66 | case []int8: 67 | a := js.Global().Get("Uint8Array").New(len(s)) 68 | js.CopyBytesToJS(a, sliceToByteSlice(s)) 69 | runtime.KeepAlive(s) 70 | buf := a.Get("buffer") 71 | return js.Global().Get("Int8Array").New(buf, a.Get("byteOffset"), a.Get("byteLength")) 72 | case []int16: 73 | a := js.Global().Get("Uint8Array").New(len(s) * 2) 74 | js.CopyBytesToJS(a, sliceToByteSlice(s)) 75 | runtime.KeepAlive(s) 76 | buf := a.Get("buffer") 77 | return js.Global().Get("Int16Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/2) 78 | case []int32: 79 | a := js.Global().Get("Uint8Array").New(len(s) * 4) 80 | js.CopyBytesToJS(a, sliceToByteSlice(s)) 81 | runtime.KeepAlive(s) 82 | buf := a.Get("buffer") 83 | return js.Global().Get("Int32Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/4) 84 | case []uint8: 85 | a := js.Global().Get("Uint8Array").New(len(s)) 86 | js.CopyBytesToJS(a, s) 87 | runtime.KeepAlive(s) 88 | return a 89 | case []uint16: 90 | a := js.Global().Get("Uint8Array").New(len(s) * 2) 91 | js.CopyBytesToJS(a, sliceToByteSlice(s)) 92 | runtime.KeepAlive(s) 93 | buf := a.Get("buffer") 94 | return js.Global().Get("Uint16Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/2) 95 | case []uint32: 96 | a := js.Global().Get("Uint8Array").New(len(s) * 4) 97 | js.CopyBytesToJS(a, sliceToByteSlice(s)) 98 | runtime.KeepAlive(s) 99 | buf := a.Get("buffer") 100 | return js.Global().Get("Uint32Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/4) 101 | case []float32: 102 | a := js.Global().Get("Uint8Array").New(len(s) * 4) 103 | js.CopyBytesToJS(a, sliceToByteSlice(s)) 104 | runtime.KeepAlive(s) 105 | buf := a.Get("buffer") 106 | return js.Global().Get("Float32Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/4) 107 | case []float64: 108 | a := js.Global().Get("Uint8Array").New(len(s) * 8) 109 | js.CopyBytesToJS(a, sliceToByteSlice(s)) 110 | runtime.KeepAlive(s) 111 | buf := a.Get("buffer") 112 | return js.Global().Get("Float64Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/8) 113 | default: 114 | panic(fmt.Sprintf("jsutil: unexpected value at SliceToTypedArray: %T", s)) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /gltypes/gltypes.go: -------------------------------------------------------------------------------- 1 | package gltypes 2 | 3 | import ( 4 | "errors" 5 | "syscall/js" 6 | ) 7 | 8 | // GLTypes provides WebGL bindings. 9 | type GLTypes struct { 10 | StaticDraw js.Value 11 | ArrayBuffer js.Value 12 | ElementArrayBuffer js.Value 13 | VertexShader js.Value 14 | FragmentShader js.Value 15 | Float js.Value 16 | DepthTest js.Value 17 | ColorBufferBit js.Value 18 | DepthBufferBit js.Value 19 | Triangles js.Value 20 | UnsignedShort js.Value 21 | UnsignedInt js.Value 22 | LEqual js.Value 23 | LineLoop js.Value 24 | } 25 | 26 | // New grabs the WebGL bindings from a GL context. 27 | func (types *GLTypes) New(gl js.Value) error { 28 | types.StaticDraw = gl.Get("STATIC_DRAW") 29 | types.ArrayBuffer = gl.Get("ARRAY_BUFFER") 30 | types.ElementArrayBuffer = gl.Get("ELEMENT_ARRAY_BUFFER") 31 | types.VertexShader = gl.Get("VERTEX_SHADER") 32 | types.FragmentShader = gl.Get("FRAGMENT_SHADER") 33 | types.Float = gl.Get("FLOAT") 34 | types.DepthTest = gl.Get("DEPTH_TEST") 35 | types.ColorBufferBit = gl.Get("COLOR_BUFFER_BIT") 36 | types.Triangles = gl.Get("TRIANGLES") 37 | types.UnsignedShort = gl.Get("UNSIGNED_SHORT") 38 | types.LEqual = gl.Get("LEQUAL") 39 | types.DepthBufferBit = gl.Get("DEPTH_BUFFER_BIT") 40 | types.LineLoop = gl.Get("LINE_LOOP") 41 | enabled := gl.Call("getExtension", "OES_element_index_uint") 42 | if !enabled.Truthy() { 43 | return errors.New("missing extension: OES_element_index_uint") 44 | } 45 | types.UnsignedInt = gl.Get("UNSIGNED_INT") 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bobcob7/wasm-stl-viewer 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/go-gl/mathgl v0.0.0-20180804195959-cdf14b6b8f8a 7 | gitlab.com/russoj88/stl v1.0.2 8 | golang.org/x/image v0.0.0-20180926015637-991ec62608f3 // indirect 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-gl/mathgl v0.0.0-20180804195959-cdf14b6b8f8a h1:2n5w2v3knlspzjJWyQPC0j88Mwvq0SZV0Jdws34GJwc= 2 | github.com/go-gl/mathgl v0.0.0-20180804195959-cdf14b6b8f8a/go.mod h1:dvrdneKbyWbK2skTda0nM4B9zSlS2GZSnjX7itr/skQ= 3 | gitlab.com/russoj88/stl v1.0.2 h1:jByJjAbp5G+Dn4yiSozUXmIDi5Jdm8tzWBkk6QsoZHk= 4 | gitlab.com/russoj88/stl v1.0.2/go.mod h1:+F6mWiD6X/9Z37sZdN2EUaQHNasZdsNX23f2t8iM1HY= 5 | golang.org/x/image v0.0.0-20180926015637-991ec62608f3 h1:5IfA9fqItkh2alJW94tvQk+6+RF9MW2q9DzwE8DBddQ= 6 | golang.org/x/image v0.0.0-20180926015637-991ec62608f3/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= 7 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | WASM STL Viewer 4 | 5 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 41 | 46 | 51 | 56 | 60 | 61 |
39 |

WASM STL Viewer

40 |
42 |

Rotation X

43 | 44 | 0.0 45 |
47 |

Rotation Y

48 | 49 | 0.0 50 |
52 |

Rotation Z

53 | 54 | 0.0 55 |
57 |

Local STL File

58 | 59 |
62 | 63 | 64 | 65 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | func main() { 10 | indexData, err := ioutil.ReadFile("index.html") 11 | if err != nil { 12 | log.Fatalf("Could not read index file: %s\n", err) 13 | } 14 | wasmExecData, err := ioutil.ReadFile("wasm_exec.js") 15 | if err != nil { 16 | log.Fatalf("Could not read wasm_exec file: %s\n", err) 17 | } 18 | 19 | wasmData, err := ioutil.ReadFile("bundle.wasm") 20 | if err != nil { 21 | log.Fatalf("Could not read wasm file: %s\n", err) 22 | } 23 | 24 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 25 | w.Write(indexData) 26 | }) 27 | 28 | http.HandleFunc("/wasm_exec.js", func(w http.ResponseWriter, r *http.Request) { 29 | w.Write(wasmExecData) 30 | }) 31 | 32 | http.HandleFunc("/bundle.wasm", func(w http.ResponseWriter, r *http.Request) { 33 | w.Header().Set("Content-Type", "application/wasm") 34 | w.WriteHeader(http.StatusOK) 35 | w.Write(wasmData) 36 | }) 37 | 38 | log.Fatal(http.ListenAndServe(":8080", nil)) 39 | } 40 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | GOROOT := $(shell go env GOROOT) 2 | 3 | all: wasm-stl-viewer 4 | 5 | wasm-stl-viewer: bundle.wasm main.go 6 | go build -o wasm-stl-viewer main.go 7 | 8 | bundle.wasm: bundle.go color/color.go color/gradient.go color/interpolation.go gltypes/gltypes.go models/model.go models/stl.go renderer/renderer.go 9 | tinygo build -o bundle.wasm -target wasm -no-debug ./bundle.go 10 | 11 | run: bundle.wasm wasm-stl-viewer wasm_exec.js 12 | ./wasm-stl-viewer 13 | 14 | wasm_exec.js: 15 | cp $(GOROOT)/misc/wasm/wasm_exec.js . 16 | 17 | clean: 18 | rm -f wasm-stl-viewer *.wasm 19 | -------------------------------------------------------------------------------- /models/model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Model interface { 4 | GetModel() ([]float32, []float32, []uint16) 5 | } 6 | -------------------------------------------------------------------------------- /models/stl.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math/rand" 7 | 8 | "github.com/bobcob7/wasm-stl-viewer/color" 9 | "gitlab.com/russoj88/stl/stl" 10 | ) 11 | 12 | func NewSTL(buffer []byte) (output STL, err error) { 13 | // Parse STL file 14 | bufferReader := bytes.NewReader(buffer) 15 | solid, err := stl.From(bufferReader) 16 | if err != nil { 17 | return 18 | } 19 | fmt.Printf("Parsed in %d Triangles\n", solid.TriangleCount) 20 | // Generate random color gradient 21 | numColors := (rand.Int() % 5) + 2 22 | colors := color.GenerateGradient(numColors, int(solid.TriangleCount)) 23 | var index uint32 = 0 24 | // Make Vertice array 25 | for i, triangle := range solid.Triangles { 26 | 27 | colorR := colors[i].Red 28 | colorG := colors[i].Green 29 | colorB := colors[i].Blue 30 | output.vertices = append(output.vertices, triangle.Vertices[0].X) 31 | output.vertices = append(output.vertices, triangle.Vertices[0].Y) 32 | output.vertices = append(output.vertices, triangle.Vertices[0].Z) 33 | output.indices = append(output.indices, index) 34 | output.colors = append(output.colors, colorR) 35 | output.colors = append(output.colors, colorG) 36 | output.colors = append(output.colors, colorB) 37 | index++ 38 | output.vertices = append(output.vertices, triangle.Vertices[1].X) 39 | output.vertices = append(output.vertices, triangle.Vertices[1].Y) 40 | output.vertices = append(output.vertices, triangle.Vertices[1].Z) 41 | output.indices = append(output.indices, index) 42 | output.colors = append(output.colors, colorR) 43 | output.colors = append(output.colors, colorG) 44 | output.colors = append(output.colors, colorB) 45 | index++ 46 | output.vertices = append(output.vertices, triangle.Vertices[2].X) 47 | output.vertices = append(output.vertices, triangle.Vertices[2].Y) 48 | output.vertices = append(output.vertices, triangle.Vertices[2].Z) 49 | output.indices = append(output.indices, index) 50 | output.colors = append(output.colors, colorR) 51 | output.colors = append(output.colors, colorG) 52 | output.colors = append(output.colors, colorB) 53 | index++ 54 | } 55 | return 56 | } 57 | 58 | type STL struct { 59 | vertices []float32 60 | colors []float32 61 | indices []uint32 62 | } 63 | 64 | func (s STL) GetModel() ([]float32, []float32, []uint32) { 65 | return s.vertices, s.colors, s.indices 66 | } 67 | -------------------------------------------------------------------------------- /renderer/renderer.go: -------------------------------------------------------------------------------- 1 | package renderer 2 | 3 | import ( 4 | "fmt" 5 | "syscall/js" 6 | "unsafe" 7 | 8 | "github.com/bobcob7/wasm-stl-viewer/gltypes" 9 | "github.com/go-gl/mathgl/mgl32" 10 | ) 11 | 12 | type InitialConfig struct { 13 | Width int 14 | Height int 15 | SpeedX float32 16 | SpeedY float32 17 | SpeedZ float32 18 | Colors []float32 19 | Vertices []float32 20 | Indices []uint32 21 | FragmentShaderCode string 22 | VertexShaderCode string 23 | } 24 | 25 | type Renderer struct { 26 | glContext js.Value 27 | glTypes gltypes.GLTypes 28 | colors js.Value 29 | vertices js.Value 30 | indices js.Value 31 | colorBuffer js.Value 32 | vertexBuffer js.Value 33 | indexBuffer js.Value 34 | numIndices int 35 | fragShader js.Value 36 | vertShader js.Value 37 | shaderProgram js.Value 38 | tmark float32 39 | rotationX float32 40 | rotationY float32 41 | rotationZ float32 42 | movMatrix mgl32.Mat4 43 | PositionMatrix js.Value 44 | ViewMatrix js.Value 45 | ModelMatrix js.Value 46 | height int 47 | width int 48 | speedX float32 49 | speedY float32 50 | speedZ float32 51 | } 52 | 53 | func NewRenderer(gl js.Value, config InitialConfig) (r Renderer, err error) { 54 | // Get some WebGL bindings 55 | r.glContext = gl 56 | err = r.glTypes.New(r.glContext) 57 | r.numIndices = len(config.Indices) 58 | r.movMatrix = mgl32.Ident4() 59 | r.width = config.Width 60 | r.height = config.Height 61 | 62 | r.speedX = config.SpeedX 63 | r.speedY = config.SpeedY 64 | r.speedZ = config.SpeedZ 65 | 66 | // Convert buffers to JS TypedArrays 67 | r.UpdateColorBuffer(config.Colors) 68 | r.UpdateVerticesBuffer(config.Vertices) 69 | r.UpdateIndicesBuffer(config.Indices) 70 | 71 | r.UpdateFragmentShader(config.FragmentShaderCode) 72 | r.UpdateVertexShader(config.VertexShaderCode) 73 | r.updateShaderProgram() 74 | r.attachShaderProgram() 75 | 76 | r.setContextFlags() 77 | 78 | r.createMatrixes() 79 | r.EnableObject() 80 | return 81 | } 82 | 83 | func (r *Renderer) SetModel(Colors []float32, Vertices []float32, Indices []uint32) { 84 | fmt.Println("Renderer.SetModel") 85 | r.numIndices = len(Indices) 86 | fmt.Println("Number of Indices:", len(Indices)) 87 | r.UpdateColorBuffer(Colors) 88 | fmt.Println("Number of Colors:", len(Colors)) 89 | r.UpdateVerticesBuffer(Vertices) 90 | fmt.Println("Number of Vertices:", len(Vertices)) 91 | r.UpdateIndicesBuffer(Indices) 92 | r.EnableObject() 93 | } 94 | 95 | func (r *Renderer) Release() { 96 | fmt.Println("Renderer.Release") 97 | } 98 | 99 | func (r *Renderer) EnableObject() { 100 | fmt.Println("Renderer.EnableObject") 101 | r.glContext.Call("bindBuffer", r.glTypes.ElementArrayBuffer, r.indexBuffer) 102 | } 103 | 104 | func (r *Renderer) SetSpeedX(x float32) { 105 | r.speedX = x 106 | } 107 | 108 | func (r *Renderer) SetSpeedY(y float32) { 109 | r.speedY = y 110 | } 111 | 112 | func (r *Renderer) SetSpeedZ(z float32) { 113 | r.speedZ = z 114 | } 115 | 116 | func (r *Renderer) GetSpeed() (x, y, z float32) { 117 | return r.speedX, r.speedY, r.speedZ 118 | } 119 | 120 | func (r *Renderer) SetSize(height, width int) { 121 | r.height = height 122 | r.width = width 123 | fmt.Println("Size", r.width, r.height) 124 | } 125 | 126 | func (r *Renderer) createMatrixes() { 127 | ratio := float32(r.width) / float32(r.height) 128 | fmt.Println("Renderer.createMatrixes") 129 | // Generate and apply projection matrix 130 | projMatrix := mgl32.Perspective(mgl32.DegToRad(45.0), ratio, 1, 100000.0) 131 | var projMatrixBuffer *[16]float32 132 | projMatrixBuffer = (*[16]float32)(unsafe.Pointer(&projMatrix)) 133 | typedProjMatrixBuffer := gltypes.SliceToTypedArray([]float32((*projMatrixBuffer)[:])) 134 | r.glContext.Call("uniformMatrix4fv", r.PositionMatrix, false, typedProjMatrixBuffer) 135 | 136 | // Generate and apply view matrix 137 | viewMatrix := mgl32.LookAtV(mgl32.Vec3{3.0, 3.0, 3.0}, mgl32.Vec3{0.0, 0.0, 0.0}, mgl32.Vec3{0.0, 1.0, 0.0}) 138 | var viewMatrixBuffer *[16]float32 139 | viewMatrixBuffer = (*[16]float32)(unsafe.Pointer(&viewMatrix)) 140 | typedViewMatrixBuffer := gltypes.SliceToTypedArray([]float32((*viewMatrixBuffer)[:])) 141 | r.glContext.Call("uniformMatrix4fv", r.ViewMatrix, false, typedViewMatrixBuffer) 142 | } 143 | 144 | func (r *Renderer) setContextFlags() { 145 | fmt.Println("Renderer.setContextFlags") 146 | // Set WebGL properties 147 | r.glContext.Call("clearColor", 0.0, 0.0, 0.0, 1.0) // Color the screen is cleared to 148 | r.glContext.Call("clearDepth", 1.0) // Z value that is set to the Depth buffer every frame 149 | r.glContext.Call("viewport", 0, 0, r.width, r.height) // Viewport size 150 | r.glContext.Call("depthFunc", r.glTypes.LEqual) 151 | } 152 | 153 | func (r *Renderer) UpdateFragmentShader(shaderCode string) { 154 | fmt.Println("Renderer.UpdateFragmentShader") 155 | // Create fragment shader object 156 | r.fragShader = r.glContext.Call("createShader", r.glTypes.FragmentShader) 157 | r.glContext.Call("shaderSource", r.fragShader, shaderCode) 158 | r.glContext.Call("compileShader", r.fragShader) 159 | } 160 | 161 | func (r *Renderer) UpdateVertexShader(shaderCode string) { 162 | fmt.Println("Renderer.UpdateVertexShader") 163 | // Create vertex shader object 164 | r.vertShader = r.glContext.Call("createShader", r.glTypes.VertexShader) 165 | r.glContext.Call("shaderSource", r.vertShader, shaderCode) 166 | r.glContext.Call("compileShader", r.vertShader) 167 | } 168 | 169 | func (r *Renderer) updateShaderProgram() { 170 | fmt.Println("Renderer.updateShaderProgram") 171 | if r.fragShader.IsUndefined() || r.vertShader.IsUndefined() { 172 | return 173 | } 174 | r.shaderProgram = r.glContext.Call("createProgram") 175 | r.glContext.Call("attachShader", r.shaderProgram, r.vertShader) 176 | r.glContext.Call("attachShader", r.shaderProgram, r.fragShader) 177 | r.glContext.Call("linkProgram", r.shaderProgram) 178 | } 179 | 180 | func (r *Renderer) attachShaderProgram() { 181 | fmt.Println("Renderer.attachShaderProgram") 182 | // Associate attributes to vertex shader 183 | r.PositionMatrix = r.glContext.Call("getUniformLocation", r.shaderProgram, "Pmatrix") 184 | r.ViewMatrix = r.glContext.Call("getUniformLocation", r.shaderProgram, "Vmatrix") 185 | r.ModelMatrix = r.glContext.Call("getUniformLocation", r.shaderProgram, "Mmatrix") 186 | 187 | r.glContext.Call("bindBuffer", r.glTypes.ArrayBuffer, r.vertexBuffer) 188 | position := r.glContext.Call("getAttribLocation", r.shaderProgram, "position") 189 | r.glContext.Call("vertexAttribPointer", position, 3, r.glTypes.Float, false, 0, 0) 190 | r.glContext.Call("enableVertexAttribArray", position) 191 | 192 | r.glContext.Call("bindBuffer", r.glTypes.ArrayBuffer, r.colorBuffer) 193 | color := r.glContext.Call("getAttribLocation", r.shaderProgram, "color") 194 | r.glContext.Call("vertexAttribPointer", color, 3, r.glTypes.Float, false, 0, 0) 195 | r.glContext.Call("enableVertexAttribArray", color) 196 | 197 | r.glContext.Call("useProgram", r.shaderProgram) 198 | } 199 | 200 | func (r *Renderer) UpdateColorBuffer(buffer []float32) { 201 | fmt.Println("Renderer.UpdateColorBuffer") 202 | r.colors = gltypes.SliceToTypedArray(buffer) 203 | // Create color buffer 204 | if r.colorBuffer.IsUndefined() { 205 | r.colorBuffer = r.glContext.Call("createBuffer") 206 | } 207 | r.glContext.Call("bindBuffer", r.glTypes.ArrayBuffer, r.colorBuffer) 208 | r.glContext.Call("bufferData", r.glTypes.ArrayBuffer, r.colors, r.glTypes.StaticDraw) 209 | } 210 | 211 | func (r *Renderer) UpdateVerticesBuffer(buffer []float32) { 212 | fmt.Println("Renderer.UpdateVerticesBuffer") 213 | r.vertices = gltypes.SliceToTypedArray(buffer) 214 | // Create vertex buffer 215 | if r.vertexBuffer.IsUndefined() { 216 | r.vertexBuffer = r.glContext.Call("createBuffer") 217 | } 218 | r.glContext.Call("bindBuffer", r.glTypes.ArrayBuffer, r.vertexBuffer) 219 | r.glContext.Call("bufferData", r.glTypes.ArrayBuffer, r.vertices, r.glTypes.StaticDraw) 220 | } 221 | 222 | func (r *Renderer) UpdateIndicesBuffer(buffer []uint32) { 223 | fmt.Println("Renderer.UpdateIndicesBuffer") 224 | r.indices = gltypes.SliceToTypedArray(buffer) 225 | // Create index buffer 226 | if r.indexBuffer.IsUndefined() { 227 | r.indexBuffer = r.glContext.Call("createBuffer") 228 | } 229 | r.glContext.Call("bindBuffer", r.glTypes.ElementArrayBuffer, r.indexBuffer) 230 | r.glContext.Call("bufferData", r.glTypes.ElementArrayBuffer, r.indices, r.glTypes.StaticDraw) 231 | } 232 | 233 | func (r *Renderer) Render(this js.Value, args []js.Value) interface{} { 234 | // Calculate rotation rate 235 | now := float32(args[0].Float()) 236 | tdiff := now - r.tmark 237 | r.tmark = now 238 | r.rotationX = r.rotationX + r.speedX*float32(tdiff)/500 239 | r.rotationY = r.rotationY + r.speedY*float32(tdiff)/500 240 | r.rotationZ = r.rotationZ + r.speedZ*float32(tdiff)/500 241 | 242 | // Do new model matrix calculations 243 | r.movMatrix = mgl32.HomogRotate3DX(r.rotationX) 244 | r.movMatrix = r.movMatrix.Mul4(mgl32.HomogRotate3DY(r.rotationY)) 245 | r.movMatrix = r.movMatrix.Mul4(mgl32.HomogRotate3DZ(r.rotationZ)) 246 | 247 | // Convert model matrix to a JS TypedArray 248 | var modelMatrixBuffer *[16]float32 249 | modelMatrixBuffer = (*[16]float32)(unsafe.Pointer(&r.movMatrix)) 250 | typedModelMatrixBuffer := gltypes.SliceToTypedArray([]float32((*modelMatrixBuffer)[:])) 251 | 252 | // Apply the model matrix 253 | r.glContext.Call("uniformMatrix4fv", r.ModelMatrix, false, typedModelMatrixBuffer) 254 | 255 | // Clear the screen 256 | r.glContext.Call("enable", r.glTypes.DepthTest) 257 | r.glContext.Call("clear", r.glTypes.ColorBufferBit) 258 | r.glContext.Call("clear", r.glTypes.DepthBufferBit) 259 | 260 | // Draw the cube 261 | r.glContext.Call("drawElements", r.glTypes.Triangles, r.numIndices, r.glTypes.UnsignedInt, 0) 262 | 263 | return nil 264 | } 265 | 266 | func (r *Renderer) SetZoom(currentZoom float32) { 267 | fmt.Println("Renderer.SetZoom") 268 | viewMatrix := mgl32.LookAtV(mgl32.Vec3{currentZoom, currentZoom, currentZoom}, mgl32.Vec3{0.0, 0.0, 0.0}, mgl32.Vec3{0.0, 1.0, 0.0}) 269 | var viewMatrixBuffer *[16]float32 270 | viewMatrixBuffer = (*[16]float32)(unsafe.Pointer(&viewMatrix)) 271 | typedViewMatrixBuffer := gltypes.SliceToTypedArray([]float32((*viewMatrixBuffer)[:])) 272 | r.glContext.Call("uniformMatrix4fv", r.ViewMatrix, false, typedViewMatrixBuffer) 273 | } 274 | -------------------------------------------------------------------------------- /wasm_exec.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | // 5 | // This file has been modified for use by the TinyGo compiler. 6 | 7 | (() => { 8 | // Map multiple JavaScript environments to a single common API, 9 | // preferring web standards over Node.js API. 10 | // 11 | // Environments considered: 12 | // - Browsers 13 | // - Node.js 14 | // - Electron 15 | // - Parcel 16 | 17 | if (typeof global !== "undefined") { 18 | // global already exists 19 | } else if (typeof window !== "undefined") { 20 | window.global = window; 21 | } else if (typeof self !== "undefined") { 22 | self.global = self; 23 | } else { 24 | throw new Error("cannot export Go (neither global, window nor self is defined)"); 25 | } 26 | 27 | if (!global.require && typeof require !== "undefined") { 28 | global.require = require; 29 | } 30 | 31 | if (!global.fs && global.require) { 32 | global.fs = require("fs"); 33 | } 34 | 35 | const enosys = () => { 36 | const err = new Error("not implemented"); 37 | err.code = "ENOSYS"; 38 | return err; 39 | }; 40 | 41 | if (!global.fs) { 42 | let outputBuf = ""; 43 | global.fs = { 44 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused 45 | writeSync(fd, buf) { 46 | outputBuf += decoder.decode(buf); 47 | const nl = outputBuf.lastIndexOf("\n"); 48 | if (nl != -1) { 49 | console.log(outputBuf.substr(0, nl)); 50 | outputBuf = outputBuf.substr(nl + 1); 51 | } 52 | return buf.length; 53 | }, 54 | write(fd, buf, offset, length, position, callback) { 55 | if (offset !== 0 || length !== buf.length || position !== null) { 56 | callback(enosys()); 57 | return; 58 | } 59 | const n = this.writeSync(fd, buf); 60 | callback(null, n); 61 | }, 62 | chmod(path, mode, callback) { callback(enosys()); }, 63 | chown(path, uid, gid, callback) { callback(enosys()); }, 64 | close(fd, callback) { callback(enosys()); }, 65 | fchmod(fd, mode, callback) { callback(enosys()); }, 66 | fchown(fd, uid, gid, callback) { callback(enosys()); }, 67 | fstat(fd, callback) { callback(enosys()); }, 68 | fsync(fd, callback) { callback(null); }, 69 | ftruncate(fd, length, callback) { callback(enosys()); }, 70 | lchown(path, uid, gid, callback) { callback(enosys()); }, 71 | link(path, link, callback) { callback(enosys()); }, 72 | lstat(path, callback) { callback(enosys()); }, 73 | mkdir(path, perm, callback) { callback(enosys()); }, 74 | open(path, flags, mode, callback) { callback(enosys()); }, 75 | read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, 76 | readdir(path, callback) { callback(enosys()); }, 77 | readlink(path, callback) { callback(enosys()); }, 78 | rename(from, to, callback) { callback(enosys()); }, 79 | rmdir(path, callback) { callback(enosys()); }, 80 | stat(path, callback) { callback(enosys()); }, 81 | symlink(path, link, callback) { callback(enosys()); }, 82 | truncate(path, length, callback) { callback(enosys()); }, 83 | unlink(path, callback) { callback(enosys()); }, 84 | utimes(path, atime, mtime, callback) { callback(enosys()); }, 85 | }; 86 | } 87 | 88 | if (!global.process) { 89 | global.process = { 90 | getuid() { return -1; }, 91 | getgid() { return -1; }, 92 | geteuid() { return -1; }, 93 | getegid() { return -1; }, 94 | getgroups() { throw enosys(); }, 95 | pid: -1, 96 | ppid: -1, 97 | umask() { throw enosys(); }, 98 | cwd() { throw enosys(); }, 99 | chdir() { throw enosys(); }, 100 | } 101 | } 102 | 103 | if (!global.crypto) { 104 | const nodeCrypto = require("crypto"); 105 | global.crypto = { 106 | getRandomValues(b) { 107 | nodeCrypto.randomFillSync(b); 108 | }, 109 | }; 110 | } 111 | 112 | if (!global.performance) { 113 | global.performance = { 114 | now() { 115 | const [sec, nsec] = process.hrtime(); 116 | return sec * 1000 + nsec / 1000000; 117 | }, 118 | }; 119 | } 120 | 121 | if (!global.TextEncoder) { 122 | global.TextEncoder = require("util").TextEncoder; 123 | } 124 | 125 | if (!global.TextDecoder) { 126 | global.TextDecoder = require("util").TextDecoder; 127 | } 128 | 129 | // End of polyfills for common API. 130 | 131 | const encoder = new TextEncoder("utf-8"); 132 | const decoder = new TextDecoder("utf-8"); 133 | var logLine = []; 134 | 135 | global.Go = class { 136 | constructor() { 137 | this._callbackTimeouts = new Map(); 138 | this._nextCallbackTimeoutID = 1; 139 | 140 | const mem = () => { 141 | // The buffer may change when requesting more memory. 142 | return new DataView(this._inst.exports.memory.buffer); 143 | } 144 | 145 | const setInt64 = (addr, v) => { 146 | mem().setUint32(addr + 0, v, true); 147 | mem().setUint32(addr + 4, Math.floor(v / 4294967296), true); 148 | } 149 | 150 | const getInt64 = (addr) => { 151 | const low = mem().getUint32(addr + 0, true); 152 | const high = mem().getInt32(addr + 4, true); 153 | return low + high * 4294967296; 154 | } 155 | 156 | const loadValue = (addr) => { 157 | const f = mem().getFloat64(addr, true); 158 | if (f === 0) { 159 | return undefined; 160 | } 161 | if (!isNaN(f)) { 162 | return f; 163 | } 164 | 165 | const id = mem().getUint32(addr, true); 166 | return this._values[id]; 167 | } 168 | 169 | const storeValue = (addr, v) => { 170 | const nanHead = 0x7FF80000; 171 | 172 | if (typeof v === "number") { 173 | if (isNaN(v)) { 174 | mem().setUint32(addr + 4, nanHead, true); 175 | mem().setUint32(addr, 0, true); 176 | return; 177 | } 178 | if (v === 0) { 179 | mem().setUint32(addr + 4, nanHead, true); 180 | mem().setUint32(addr, 1, true); 181 | return; 182 | } 183 | mem().setFloat64(addr, v, true); 184 | return; 185 | } 186 | 187 | switch (v) { 188 | case undefined: 189 | mem().setFloat64(addr, 0, true); 190 | return; 191 | case null: 192 | mem().setUint32(addr + 4, nanHead, true); 193 | mem().setUint32(addr, 2, true); 194 | return; 195 | case true: 196 | mem().setUint32(addr + 4, nanHead, true); 197 | mem().setUint32(addr, 3, true); 198 | return; 199 | case false: 200 | mem().setUint32(addr + 4, nanHead, true); 201 | mem().setUint32(addr, 4, true); 202 | return; 203 | } 204 | 205 | let id = this._ids.get(v); 206 | if (id === undefined) { 207 | id = this._idPool.pop(); 208 | if (id === undefined) { 209 | id = this._values.length; 210 | } 211 | this._values[id] = v; 212 | this._goRefCounts[id] = 0; 213 | this._ids.set(v, id); 214 | } 215 | this._goRefCounts[id]++; 216 | let typeFlag = 1; 217 | switch (typeof v) { 218 | case "string": 219 | typeFlag = 2; 220 | break; 221 | case "symbol": 222 | typeFlag = 3; 223 | break; 224 | case "function": 225 | typeFlag = 4; 226 | break; 227 | } 228 | mem().setUint32(addr + 4, nanHead | typeFlag, true); 229 | mem().setUint32(addr, id, true); 230 | } 231 | 232 | const loadSlice = (array, len, cap) => { 233 | return new Uint8Array(this._inst.exports.memory.buffer, array, len); 234 | } 235 | 236 | const loadSliceOfValues = (array, len, cap) => { 237 | const a = new Array(len); 238 | for (let i = 0; i < len; i++) { 239 | a[i] = loadValue(array + i * 8); 240 | } 241 | return a; 242 | } 243 | 244 | const loadString = (ptr, len) => { 245 | return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len)); 246 | } 247 | 248 | const timeOrigin = Date.now() - performance.now(); 249 | this.importObject = { 250 | wasi_snapshot_preview1: { 251 | // https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_write 252 | fd_write: function(fd, iovs_ptr, iovs_len, nwritten_ptr) { 253 | let nwritten = 0; 254 | if (fd == 1) { 255 | for (let iovs_i=0; iovs_i 0, // dummy 281 | fd_fdstat_get: () => 0, // dummy 282 | fd_seek: () => 0, // dummy 283 | "proc_exit": (code) => { 284 | if (global.process) { 285 | // Node.js 286 | process.exit(code); 287 | } else { 288 | // Can't exit in a browser. 289 | throw 'trying to exit with code ' + code; 290 | } 291 | }, 292 | random_get: (bufPtr, bufLen) => { 293 | crypto.getRandomValues(loadSlice(bufPtr, bufLen)); 294 | return 0; 295 | }, 296 | }, 297 | env: { 298 | // func ticks() float64 299 | "runtime.ticks": () => { 300 | return timeOrigin + performance.now(); 301 | }, 302 | 303 | // func sleepTicks(timeout float64) 304 | "runtime.sleepTicks": (timeout) => { 305 | // Do not sleep, only reactivate scheduler after the given timeout. 306 | setTimeout(this._inst.exports.go_scheduler, timeout); 307 | }, 308 | 309 | // func finalizeRef(v ref) 310 | "syscall/js.finalizeRef": (sp) => { 311 | // Note: TinyGo does not support finalizers so this should never be 312 | // called. 313 | console.error('syscall/js.finalizeRef not implemented'); 314 | }, 315 | 316 | // func stringVal(value string) ref 317 | "syscall/js.stringVal": (ret_ptr, value_ptr, value_len) => { 318 | const s = loadString(value_ptr, value_len); 319 | storeValue(ret_ptr, s); 320 | }, 321 | 322 | // func valueGet(v ref, p string) ref 323 | "syscall/js.valueGet": (retval, v_addr, p_ptr, p_len) => { 324 | let prop = loadString(p_ptr, p_len); 325 | let value = loadValue(v_addr); 326 | let result = Reflect.get(value, prop); 327 | storeValue(retval, result); 328 | }, 329 | 330 | // func valueSet(v ref, p string, x ref) 331 | "syscall/js.valueSet": (v_addr, p_ptr, p_len, x_addr) => { 332 | const v = loadValue(v_addr); 333 | const p = loadString(p_ptr, p_len); 334 | const x = loadValue(x_addr); 335 | Reflect.set(v, p, x); 336 | }, 337 | 338 | // func valueDelete(v ref, p string) 339 | "syscall/js.valueDelete": (v_addr, p_ptr, p_len) => { 340 | const v = loadValue(v_addr); 341 | const p = loadString(p_ptr, p_len); 342 | Reflect.deleteProperty(v, p); 343 | }, 344 | 345 | // func valueIndex(v ref, i int) ref 346 | "syscall/js.valueIndex": (ret_addr, v_addr, i) => { 347 | storeValue(ret_addr, Reflect.get(loadValue(v_addr), i)); 348 | }, 349 | 350 | // valueSetIndex(v ref, i int, x ref) 351 | "syscall/js.valueSetIndex": (v_addr, i, x_addr) => { 352 | Reflect.set(loadValue(v_addr), i, loadValue(x_addr)); 353 | }, 354 | 355 | // func valueCall(v ref, m string, args []ref) (ref, bool) 356 | "syscall/js.valueCall": (ret_addr, v_addr, m_ptr, m_len, args_ptr, args_len, args_cap) => { 357 | const v = loadValue(v_addr); 358 | const name = loadString(m_ptr, m_len); 359 | const args = loadSliceOfValues(args_ptr, args_len, args_cap); 360 | try { 361 | const m = Reflect.get(v, name); 362 | storeValue(ret_addr, Reflect.apply(m, v, args)); 363 | mem().setUint8(ret_addr + 8, 1); 364 | } catch (err) { 365 | storeValue(ret_addr, err); 366 | mem().setUint8(ret_addr + 8, 0); 367 | } 368 | }, 369 | 370 | // func valueInvoke(v ref, args []ref) (ref, bool) 371 | "syscall/js.valueInvoke": (ret_addr, v_addr, args_ptr, args_len, args_cap) => { 372 | try { 373 | const v = loadValue(v_addr); 374 | const args = loadSliceOfValues(args_ptr, args_len, args_cap); 375 | storeValue(ret_addr, Reflect.apply(v, undefined, args)); 376 | mem().setUint8(ret_addr + 8, 1); 377 | } catch (err) { 378 | storeValue(ret_addr, err); 379 | mem().setUint8(ret_addr + 8, 0); 380 | } 381 | }, 382 | 383 | // func valueNew(v ref, args []ref) (ref, bool) 384 | "syscall/js.valueNew": (ret_addr, v_addr, args_ptr, args_len, args_cap) => { 385 | const v = loadValue(v_addr); 386 | const args = loadSliceOfValues(args_ptr, args_len, args_cap); 387 | try { 388 | storeValue(ret_addr, Reflect.construct(v, args)); 389 | mem().setUint8(ret_addr + 8, 1); 390 | } catch (err) { 391 | storeValue(ret_addr, err); 392 | mem().setUint8(ret_addr+ 8, 0); 393 | } 394 | }, 395 | 396 | // func valueLength(v ref) int 397 | "syscall/js.valueLength": (v_addr) => { 398 | return loadValue(v_addr).length; 399 | }, 400 | 401 | // valuePrepareString(v ref) (ref, int) 402 | "syscall/js.valuePrepareString": (ret_addr, v_addr) => { 403 | const s = String(loadValue(v_addr)); 404 | const str = encoder.encode(s); 405 | storeValue(ret_addr, str); 406 | setInt64(ret_addr + 8, str.length); 407 | }, 408 | 409 | // valueLoadString(v ref, b []byte) 410 | "syscall/js.valueLoadString": (v_addr, slice_ptr, slice_len, slice_cap) => { 411 | const str = loadValue(v_addr); 412 | loadSlice(slice_ptr, slice_len, slice_cap).set(str); 413 | }, 414 | 415 | // func valueInstanceOf(v ref, t ref) bool 416 | "syscall/js.valueInstanceOf": (v_addr, t_addr) => { 417 | return loadValue(v_addr) instanceof loadValue(t_addr); 418 | }, 419 | 420 | // func copyBytesToGo(dst []byte, src ref) (int, bool) 421 | "syscall/js.copyBytesToGo": (ret_addr, dest_addr, dest_len, dest_cap, source_addr) => { 422 | let num_bytes_copied_addr = ret_addr; 423 | let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable 424 | 425 | const dst = loadSlice(dest_addr, dest_len); 426 | const src = loadValue(source_addr); 427 | if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { 428 | mem().setUint8(returned_status_addr, 0); // Return "not ok" status 429 | return; 430 | } 431 | const toCopy = src.subarray(0, dst.length); 432 | dst.set(toCopy); 433 | setInt64(num_bytes_copied_addr, toCopy.length); 434 | mem().setUint8(returned_status_addr, 1); // Return "ok" status 435 | }, 436 | 437 | // copyBytesToJS(dst ref, src []byte) (int, bool) 438 | // Originally copied from upstream Go project, then modified: 439 | // https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416 440 | "syscall/js.copyBytesToJS": (ret_addr, dest_addr, source_addr, source_len, source_cap) => { 441 | let num_bytes_copied_addr = ret_addr; 442 | let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable 443 | 444 | const dst = loadValue(dest_addr); 445 | const src = loadSlice(source_addr, source_len); 446 | if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { 447 | mem().setUint8(returned_status_addr, 0); // Return "not ok" status 448 | return; 449 | } 450 | const toCopy = src.subarray(0, dst.length); 451 | dst.set(toCopy); 452 | setInt64(num_bytes_copied_addr, toCopy.length); 453 | mem().setUint8(returned_status_addr, 1); // Return "ok" status 454 | }, 455 | } 456 | }; 457 | } 458 | 459 | async run(instance) { 460 | this._inst = instance; 461 | this._values = [ // JS values that Go currently has references to, indexed by reference id 462 | NaN, 463 | 0, 464 | null, 465 | true, 466 | false, 467 | global, 468 | this, 469 | ]; 470 | this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id 471 | this._ids = new Map(); // mapping from JS values to reference ids 472 | this._idPool = []; // unused ids that have been garbage collected 473 | this.exited = false; // whether the Go program has exited 474 | 475 | const mem = new DataView(this._inst.exports.memory.buffer) 476 | 477 | while (true) { 478 | const callbackPromise = new Promise((resolve) => { 479 | this._resolveCallbackPromise = () => { 480 | if (this.exited) { 481 | throw new Error("bad callback: Go program has already exited"); 482 | } 483 | setTimeout(resolve, 0); // make sure it is asynchronous 484 | }; 485 | }); 486 | this._inst.exports._start(); 487 | if (this.exited) { 488 | break; 489 | } 490 | await callbackPromise; 491 | } 492 | } 493 | 494 | _resume() { 495 | if (this.exited) { 496 | throw new Error("Go program has already exited"); 497 | } 498 | this._inst.exports.resume(); 499 | if (this.exited) { 500 | this._resolveExitPromise(); 501 | } 502 | } 503 | 504 | _makeFuncWrapper(id) { 505 | const go = this; 506 | return function () { 507 | const event = { id: id, this: this, args: arguments }; 508 | go._pendingEvent = event; 509 | go._resume(); 510 | return event.result; 511 | }; 512 | } 513 | } 514 | 515 | if ( 516 | global.require && 517 | global.require.main === module && 518 | global.process && 519 | global.process.versions && 520 | !global.process.versions.electron 521 | ) { 522 | if (process.argv.length != 3) { 523 | console.error("usage: go_js_wasm_exec [wasm binary] [arguments]"); 524 | process.exit(1); 525 | } 526 | 527 | const go = new Go(); 528 | WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { 529 | return go.run(result.instance); 530 | }).catch((err) => { 531 | console.error(err); 532 | process.exit(1); 533 | }); 534 | } 535 | })(); --------------------------------------------------------------------------------