Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 0 additions & 15 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,26 +54,22 @@ jobs:
arch: amd64
goos: linux
binary: pointcloud
convert-binary: xyz-convert
cc: gcc
- os: ubuntu-latest
arch: arm64
goos: linux
binary: pointcloud
convert-binary: xyz-convert
cc: aarch64-linux-gnu-gcc
goarch: arm64
- os: macos-latest
arch: arm64
goos: darwin
binary: pointcloud
convert-binary: xyz-convert
cc: ""
- os: windows-latest
arch: amd64
goos: windows
binary: pointcloud.exe
convert-binary: xyz-convert.exe
cc: ""

runs-on: ${{ matrix.os }}
Expand Down Expand Up @@ -130,17 +126,6 @@ jobs:
go build -ldflags "-X main.Version=${{ steps.version.outputs.version }}" \
-o bin/${{ matrix.binary }} .

- name: Build xyz-convert
shell: bash
env:
CGO_ENABLED: "1"
GOARCH: ${{ matrix.goarch || '' }}
CC: ${{ matrix.cc || '' }}
PKG_CONFIG_PATH: ${{ matrix.arch == 'arm64' && runner.os == 'Linux' && '/usr/lib/aarch64-linux-gnu/pkgconfig' || '' }}
run: |
go build -ldflags "-X main.Version=${{ steps.version.outputs.version }}" \
-o bin/${{ matrix.convert-binary }} ./cmd/xyz-convert

- name: Create archive
shell: bash
run: |
Expand Down
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ BINARIES := $(notdir $(shell find cmd -mindepth 1 -maxdepth 1 -type d))

all: lint vet staticcheck test build

build: $(BINARIES)

$(BINARIES):
@echo "*** $@"
@cd cmd/$@ && CGO_ENABLED=0 go build $(LDFLAGS) -trimpath -o ../../bin/$@
@cd cmd/$@ && go build $(LDFLAGS) -trimpath -o ../../bin/$@

run:
@echo "*** $@"
Expand Down
118 changes: 86 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# Point Cloud Viewer

![Gopher](images/gopher-small.png)

[![Go Reference](https://pkg.go.dev/badge/github.com/borud/pointcloud.svg)](https://pkg.go.dev/github.com/borud/pointcloud)

A simple point cloud viewer built with [Fyne](https://fyne.io/) and OpenGL. Reads PLY, PCD, PTS, and XYZ files and renders them as interactive 3D point clouds with mouse-controlled rotation and zoom.

![Screenshot](screenshot.png)
![Screenshot](images/screenshot.png)

## Features

Expand All @@ -21,10 +23,24 @@ A simple point cloud viewer built with [Fyne](https://fyne.io/) and OpenGL. Read

## Installation

### Library

To use the viewer widget in your own Fyne application:

```sh
go get github.com/borud/pointcloud
```

### Demo application

Pre-built binaries for Linux (amd64, arm64), macOS (arm64), and Windows (amd64) are available on the [releases page](https://github.com/borud/pointcloud/releases).

Or build from source:

```sh
go install github.com/borud/pointcloud/cmd/pointcloud@latest
```

## Using the widget

The `Viewer` is a standard Fyne widget that you can embed in any Fyne application. It uses a functional options pattern for configuration.
Expand All @@ -35,28 +51,28 @@ The `Viewer` is a standard Fyne widget that you can embed in any Fyne applicatio
package main

import (
"log"
"log"

"fyne.io/fyne/v2/app"
"github.com/borud/pointcloud"
"fyne.io/fyne/v2/app"
"github.com/borud/pointcloud"
)

func main() {
myApp := app.New()
w := myApp.NewWindow("Point Cloud")
myApp := app.New()
w := myApp.NewWindow("Point Cloud")

v := pointcloud.New()
v := pointcloud.New()

pc, err := pointcloud.ReadFile("model.ply")
if err != nil {
log.Fatal(err)
}
pc.Normalize()
v.SetScale(pc.NormScale)
v.SetPoints(pc.Points)
pc, err := pointcloud.ReadFile("model.ply")
if err != nil {
log.Fatal(err)
}
pc.Normalize()
v.SetScale(pc.NormScale)
v.SetPoints(pc.Points)

w.SetContent(v)
w.ShowAndRun()
w.SetContent(v)
w.ShowAndRun()
}
```

Expand All @@ -66,16 +82,16 @@ Use functional options to customize appearance when creating the viewer.

```go
v := pointcloud.New(
pointcloud.WithBackgroundColor(color.RGBA{30, 30, 30, 255}),
pointcloud.WithDefaultPointColor(color.RGBA{255, 150, 255, 255}),
pointcloud.WithOrientationCube(true),
pointcloud.WithHomeButton(true),
pointcloud.WithZoomFitButton(true),
pointcloud.WithInfoLabel(true),
pointcloud.WithScaleBar(true),
pointcloud.WithScaleUnit("m"),
pointcloud.WithFPS(true),
pointcloud.WithMaxZoomOutFraction(0.3),
pointcloud.WithBackgroundColor(color.RGBA{30, 30, 30, 255}),
pointcloud.WithDefaultPointColor(color.RGBA{255, 150, 255, 255}),
pointcloud.WithOrientationCube(true),
pointcloud.WithHomeButton(true),
pointcloud.WithZoomFitButton(true),
pointcloud.WithInfoLabel(true),
pointcloud.WithScaleBar(true),
pointcloud.WithScaleUnit("m"),
pointcloud.WithFPS(true),
pointcloud.WithMaxZoomOutFraction(0.3),
)
v.SetUpAxis(pointcloud.ZUp)
```
Expand All @@ -102,12 +118,12 @@ You can construct a `PointCloud` programmatically instead of reading from a file

```go
pc := &pointcloud.PointCloud{
Points: []pointcloud.Point3D{
{X: 0, Y: 0, Z: 0, R: 255, G: 0, B: 0, HasColor: true},
{X: 1, Y: 0, Z: 0, R: 0, G: 255, B: 0, HasColor: true},
{X: 0, Y: 1, Z: 0, R: 0, G: 0, B: 255, HasColor: true},
{X: 0, Y: 0, Z: 1, R: 255, G: 255, B: 0, HasColor: true},
},
Points: []pointcloud.Point3D{
{X: 0, Y: 0, Z: 0, R: 255, G: 0, B: 0, HasColor: true},
{X: 1, Y: 0, Z: 0, R: 0, G: 255, B: 0, HasColor: true},
{X: 0, Y: 1, Z: 0, R: 0, G: 0, B: 255, HasColor: true},
{X: 0, Y: 0, Z: 1, R: 255, G: 255, B: 0, HasColor: true},
},
}
pc.ComputeBounds()
pc.Normalize()
Expand Down Expand Up @@ -151,6 +167,44 @@ defer f.Close()
pointcloud.WritePLY(f, pc)
```

## Controls

The viewer has two camera modes: **orbit** (default) and **flythrough** (first-person). Toggle between them with the `g` key or the eye button in the toolbar.

### Orbit mode

| Input | Action |
|---|---|
| Drag | Arcball rotation |
| Shift+Drag | Pan |
| Scroll wheel | Zoom in/out |
| Click | Pick nearest point (shows coordinates in info label) |
| `h` | Reset to home view |
| `f` | Zoom to fit all points |
| `+` / `-` | Zoom in / out |
| Arrow keys | Rotate in 5-degree steps |
| `g` | Enter flythrough mode |

### Flythrough mode

In flythrough mode the camera moves freely through the point cloud using FPS-style controls.

| Input | Action |
|---|---|
| `W` / `S` or Up / Down | Move forward / backward |
| `A` / `D` or Left / Right | Strafe left / right |
| Space | Move up (world space) |
| `Q` | Move down (world space) |
| Drag | Mouse look |
| Scroll wheel | Adjust movement speed |
| `g` | Return to orbit mode |
| Escape | Return to orbit mode |
| `h` | Return to orbit mode and reset home view |

### Orientation cube

Click a face, edge midpoint, or corner of the orientation cube to snap the view to that direction.

## Benchmarking

Install [benchstat](https://pkg.go.dev/golang.org/x/perf/cmd/benchstat) for comparing results:
Expand Down
17 changes: 17 additions & 0 deletions bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,23 @@ func BenchmarkClear_1080p(b *testing.B) {
}
}

// BenchmarkDraw_Flythrough_1M measures draw with flythrough camera inside cloud.
func BenchmarkDraw_Flythrough_1M(b *testing.B) {
pts := generatePoints(1_000_000)
c := setupCanvas(pts)
c.flyMode = true
c.fly = newFlythroughCamera(c)
c.fly.pos = [3]float64{0, 0, 0} // camera at center of cloud
c.fly.orientation = QuatIdentity()
w, h := 1024, 768

b.ReportAllocs()
b.ResetTimer()
for range b.N {
c.draw(w, h)
}
}

// BenchmarkProjection_1M measures only the projection math (no pixel writes).
func BenchmarkProjection_1M(b *testing.B) {
pts := generatePoints(1_000_000)
Expand Down
Loading