playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
884 lines (838 loc) • 31.6 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
import { BUFFER_DYNAMIC, CULLFACE_NONE, PRIMITIVE_TRIANGLES, SEMANTIC_ATTR8, SEMANTIC_ATTR9, SEMANTIC_ATTR10, SEMANTIC_ATTR11, SEMANTIC_ATTR12, SEMANTIC_ATTR13, SEMANTIC_ATTR14, SEMANTIC_ATTR15, SEMANTIC_POSITION, TYPE_FLOAT32 } from "../../platform/graphics/constants.js";
import { VertexBuffer } from "../../platform/graphics/vertex-buffer.js";
import { VertexFormat } from "../../platform/graphics/vertex-format.js";
import { GraphNode } from "../../scene/graph-node.js";
import { Mesh } from "../../scene/mesh.js";
import { MeshInstance } from "../../scene/mesh-instance.js";
import { ShaderMaterial } from "../../scene/materials/shader-material.js";
import { WideLine } from "./wide-line.js";
const LINEWIDTH_SCREEN = 0;
const LINEWIDTH_WORLD = 1;
const FLOATS_PER_INSTANCE = 30;
const ROUND_SEGMENTS = 16;
const vertexGLSL = (
/* glsl */
`
attribute vec3 vertex_position;
attribute vec4 instance_prevWidth;
attribute vec4 instance_startWidth;
attribute vec4 instance_endDistance;
attribute vec4 instance_nextDistance;
attribute vec3 instance_startColor;
attribute vec3 instance_endColor;
attribute vec4 instance_style;
attribute vec4 instance_dashFlags;
uniform mat4 matrix_viewProjection;
uniform vec4 uScreenSize;
#ifdef WIDE_LINE_WORLD_SPACE_WIDTH
uniform mat4 matrix_projection;
#endif
varying vec3 vColor;
varying vec4 vDash;
varying vec3 vLineData;
varying float vDistance;
vec2 safeNormalize(vec2 value, vec2 fallbackValue) {
float lengthValue = length(value);
return lengthValue > 0.00001 ? value / lengthValue : fallbackValue;
}
vec2 perpendicular(vec2 value) {
return vec2(-value.y, value.x);
}
vec2 toScreen(vec4 clipPosition) {
float w = abs(clipPosition.w) > 0.00001 ? clipPosition.w : 0.00001;
return clipPosition.xy / w * uScreenSize.xy * 0.5;
}
vec4 offsetClip(vec4 clipPosition, vec2 pixelOffset) {
clipPosition.xy += pixelOffset * (2.0 * uScreenSize.zw) * clipPosition.w;
return clipPosition;
}
float resolveHalfWidth(float width, vec4 clipPosition) {
#ifdef WIDE_LINE_WORLD_SPACE_WIDTH
float pixelsPerWorldUnit = abs(matrix_projection[1][1]) * uScreenSize.y * 0.5 /
max(abs(clipPosition.w), 0.00001);
return width * 0.5 * pixelsPerWorldUnit;
#else
return width * 0.5;
#endif
}
vec2 miterOffset(vec2 firstDirection, vec2 secondDirection, float side, float halfWidth) {
vec2 secondNormal = perpendicular(secondDirection);
vec2 miter = safeNormalize(perpendicular(firstDirection) + secondNormal, secondNormal);
float scale = halfWidth / max(dot(miter, secondNormal), 0.25);
return miter * side * min(scale, halfWidth * 4.0);
}
void main(void) {
vec3 prev = instance_prevWidth.xyz;
vec3 start = instance_startWidth.xyz;
vec3 end = instance_endDistance.xyz;
vec3 next = instance_nextDistance.xyz;
float startDistance = instance_endDistance.w;
float endDistance = instance_nextDistance.w;
vec4 prevClip = matrix_viewProjection * vec4(prev, 1.0);
vec4 startClip = matrix_viewProjection * vec4(start, 1.0);
vec4 endClip = matrix_viewProjection * vec4(end, 1.0);
vec4 nextClip = matrix_viewProjection * vec4(next, 1.0);
float startHalfWidth = resolveHalfWidth(instance_prevWidth.w, startClip);
float endHalfWidth = resolveHalfWidth(instance_startWidth.w, endClip);
vec2 startScreen = toScreen(startClip);
vec2 endScreen = toScreen(endClip);
vec2 currentDirection = safeNormalize(endScreen - startScreen, vec2(1.0, 0.0));
vec2 previousDirection = safeNormalize(startScreen - toScreen(prevClip), currentDirection);
vec2 nextDirection = safeNormalize(toScreen(nextClip) - endScreen, currentDirection);
vec2 currentNormal = perpendicular(currentDirection);
vec2 previousNormal = perpendicular(previousDirection);
float joinStyle = instance_style.x;
float capStyle = instance_style.y;
uint flags = uint(instance_dashFlags.y + 0.5);
bool startConnected = (flags & 1u) != 0u;
bool endConnected = (flags & 2u) != 0u;
float kind = vertex_position.z;
vec4 clipPosition;
vLineData = vec3(0.0, startHalfWidth, 0.0);
if (kind < 0.5) {
float along = vertex_position.x;
float side = vertex_position.y;
bool atStart = along < 0.5;
float halfWidth = atStart ? startHalfWidth : endHalfWidth;
vec2 offset;
if (atStart) {
if (startConnected && joinStyle < 0.5) {
offset = miterOffset(previousDirection, currentDirection, side, halfWidth);
} else {
offset = currentNormal * side * halfWidth;
if (!startConnected && capStyle > 0.5 && capStyle < 1.5) {
offset -= currentDirection * halfWidth;
}
}
clipPosition = offsetClip(startClip, offset);
} else {
if (endConnected && joinStyle < 0.5) {
offset = miterOffset(currentDirection, nextDirection, side, halfWidth);
} else {
offset = currentNormal * side * halfWidth;
if (!endConnected && capStyle > 0.5 && capStyle < 1.5) {
offset += currentDirection * halfWidth;
}
}
clipPosition = offsetClip(endClip, offset);
}
vColor = mix(instance_startColor, instance_endColor, along);
vDistance = mix(startDistance, endDistance, along);
vLineData = vec3(side, mix(startHalfWidth, endHalfWidth, along), 1.0);
} else if (kind < 1.5) {
bool visible = (startConnected && joinStyle > 1.5) || (!startConnected && capStyle > 1.5);
float scale = visible ? startHalfWidth : 0.0;
vec2 offset = (currentDirection * vertex_position.x + currentNormal * vertex_position.y) * scale;
clipPosition = offsetClip(startClip, offset);
vColor = instance_startColor;
vDistance = startDistance;
} else if (kind < 2.5) {
bool visible = !endConnected && capStyle > 1.5;
float scale = visible ? endHalfWidth : 0.0;
vec2 offset = (currentDirection * vertex_position.x + currentNormal * vertex_position.y) * scale;
clipPosition = offsetClip(endClip, offset);
vColor = instance_endColor;
vDistance = endDistance;
} else {
bool visible = startConnected && joinStyle > 0.5 && joinStyle < 1.5;
float scale = visible ? startHalfWidth : 0.0;
vec2 offset = vec2(0.0);
if (kind > 3.5 && kind < 4.5) {
offset = previousNormal * vertex_position.y * scale;
} else if (kind > 4.5) {
offset = currentNormal * vertex_position.y * scale;
}
clipPosition = offsetClip(startClip, offset);
vColor = instance_startColor;
vDistance = startDistance;
}
vDash = vec4(instance_style.z, instance_style.w, instance_dashFlags.x, capStyle);
gl_Position = clipPosition;
}
`
);
const fragmentGLSL = (
/* glsl */
`
varying vec3 vColor;
varying vec4 vDash;
varying vec3 vLineData;
varying float vDistance;
void main(void) {
float worldPerPixel = length(vec2(dFdx(vDistance), dFdy(vDistance)));
if (vDash.x > 0.0 && vDash.y > 0.0) {
float period = vDash.x + vDash.y;
float phase = mod(vDistance + vDash.z, period);
if (phase < 0.0) {
phase += period;
}
if (phase > vDash.x) {
if (vDash.w > 1.5 && vLineData.z > 0.5) {
float along = min(phase - vDash.x, period - phase) / max(worldPerPixel, 0.00001);
float across = abs(vLineData.x) * vLineData.y;
if (length(vec2(along, across)) > vLineData.y) {
discard;
}
} else {
discard;
}
}
}
gl_FragColor = vec4(vColor, 1.0);
}
`
);
const vertexWGSL = (
/* wgsl */
`
attribute vertex_position: vec3f;
attribute instance_prevWidth: vec4f;
attribute instance_startWidth: vec4f;
attribute instance_endDistance: vec4f;
attribute instance_nextDistance: vec4f;
attribute instance_startColor: vec3f;
attribute instance_endColor: vec3f;
attribute instance_style: vec4f;
attribute instance_dashFlags: vec4f;
uniform matrix_viewProjection: mat4x4f;
uniform uScreenSize: vec4f;
#ifdef WIDE_LINE_WORLD_SPACE_WIDTH
uniform matrix_projection: mat4x4f;
#endif
varying vColor: vec3f;
varying vDash: vec4f;
varying vLineData: vec3f;
varying vDistance: f32;
fn safeNormalize(value: vec2f, fallbackValue: vec2f) -> vec2f {
let lengthValue = length(value);
return select(fallbackValue, value / lengthValue, lengthValue > 0.00001);
}
fn perpendicular(value: vec2f) -> vec2f {
return vec2f(-value.y, value.x);
}
fn toScreen(clipPosition: vec4f) -> vec2f {
let w = select(0.00001, clipPosition.w, abs(clipPosition.w) > 0.00001);
return clipPosition.xy / w * uniform.uScreenSize.xy * 0.5;
}
fn offsetClip(position: vec4f, pixelOffset: vec2f) -> vec4f {
var clipPosition = position;
clipPosition.x += pixelOffset.x * (2.0 * uniform.uScreenSize.z) * clipPosition.w;
clipPosition.y += pixelOffset.y * (2.0 * uniform.uScreenSize.w) * clipPosition.w;
return clipPosition;
}
fn resolveHalfWidth(width: f32, clipPosition: vec4f) -> f32 {
#ifdef WIDE_LINE_WORLD_SPACE_WIDTH
let pixelsPerWorldUnit = abs(uniform.matrix_projection[1][1]) * uniform.uScreenSize.y * 0.5 /
max(abs(clipPosition.w), 0.00001);
return width * 0.5 * pixelsPerWorldUnit;
#else
return width * 0.5;
#endif
}
fn miterOffset(firstDirection: vec2f, secondDirection: vec2f, side: f32, halfWidth: f32) -> vec2f {
let secondNormal = perpendicular(secondDirection);
let miter = safeNormalize(perpendicular(firstDirection) + secondNormal, secondNormal);
let scale = halfWidth / max(dot(miter, secondNormal), 0.25);
return miter * side * min(scale, halfWidth * 4.0);
}
fn vertexMain(input: VertexInput) -> VertexOutput {
var output: VertexOutput;
let prev = input.instance_prevWidth.xyz;
let start = input.instance_startWidth.xyz;
let end = input.instance_endDistance.xyz;
let next = input.instance_nextDistance.xyz;
let startDistance = input.instance_endDistance.w;
let endDistance = input.instance_nextDistance.w;
let prevClip = uniform.matrix_viewProjection * vec4f(prev, 1.0);
let startClip = uniform.matrix_viewProjection * vec4f(start, 1.0);
let endClip = uniform.matrix_viewProjection * vec4f(end, 1.0);
let nextClip = uniform.matrix_viewProjection * vec4f(next, 1.0);
let startHalfWidth = resolveHalfWidth(input.instance_prevWidth.w, startClip);
let endHalfWidth = resolveHalfWidth(input.instance_startWidth.w, endClip);
let startScreen = toScreen(startClip);
let endScreen = toScreen(endClip);
let currentDirection = safeNormalize(endScreen - startScreen, vec2f(1.0, 0.0));
let previousDirection = safeNormalize(startScreen - toScreen(prevClip), currentDirection);
let nextDirection = safeNormalize(toScreen(nextClip) - endScreen, currentDirection);
let currentNormal = perpendicular(currentDirection);
let previousNormal = perpendicular(previousDirection);
let joinStyle = input.instance_style.x;
let capStyle = input.instance_style.y;
let flags = u32(input.instance_dashFlags.y + 0.5);
let startConnected = (flags & 1u) != 0u;
let endConnected = (flags & 2u) != 0u;
let kind = input.vertex_position.z;
var clipPosition: vec4f;
output.vLineData = vec3f(0.0, startHalfWidth, 0.0);
if (kind < 0.5) {
let along = input.vertex_position.x;
let side = input.vertex_position.y;
let atStart = along < 0.5;
let halfWidth = select(endHalfWidth, startHalfWidth, atStart);
var offset: vec2f;
if (atStart) {
if (startConnected && joinStyle < 0.5) {
offset = miterOffset(previousDirection, currentDirection, side, halfWidth);
} else {
offset = currentNormal * side * halfWidth;
if (!startConnected && capStyle > 0.5 && capStyle < 1.5) {
offset -= currentDirection * halfWidth;
}
}
clipPosition = offsetClip(startClip, offset);
} else {
if (endConnected && joinStyle < 0.5) {
offset = miterOffset(currentDirection, nextDirection, side, halfWidth);
} else {
offset = currentNormal * side * halfWidth;
if (!endConnected && capStyle > 0.5 && capStyle < 1.5) {
offset += currentDirection * halfWidth;
}
}
clipPosition = offsetClip(endClip, offset);
}
output.vColor = mix(input.instance_startColor, input.instance_endColor, along);
output.vDistance = mix(startDistance, endDistance, along);
output.vLineData = vec3f(side, mix(startHalfWidth, endHalfWidth, along), 1.0);
} else if (kind < 1.5) {
let visible = (startConnected && joinStyle > 1.5) || (!startConnected && capStyle > 1.5);
let scale = select(0.0, startHalfWidth, visible);
let offset = (currentDirection * input.vertex_position.x + currentNormal * input.vertex_position.y) * scale;
clipPosition = offsetClip(startClip, offset);
output.vColor = input.instance_startColor;
output.vDistance = startDistance;
} else if (kind < 2.5) {
let visible = !endConnected && capStyle > 1.5;
let scale = select(0.0, endHalfWidth, visible);
let offset = (currentDirection * input.vertex_position.x + currentNormal * input.vertex_position.y) * scale;
clipPosition = offsetClip(endClip, offset);
output.vColor = input.instance_endColor;
output.vDistance = endDistance;
} else {
let visible = startConnected && joinStyle > 0.5 && joinStyle < 1.5;
let scale = select(0.0, startHalfWidth, visible);
var offset = vec2f(0.0);
if (kind > 3.5 && kind < 4.5) {
offset = previousNormal * input.vertex_position.y * scale;
} else if (kind > 4.5) {
offset = currentNormal * input.vertex_position.y * scale;
}
clipPosition = offsetClip(startClip, offset);
output.vColor = input.instance_startColor;
output.vDistance = startDistance;
}
output.vDash = vec4f(input.instance_style.z, input.instance_style.w, input.instance_dashFlags.x, capStyle);
output.position = clipPosition;
return output;
}
`
);
const fragmentWGSL = (
/* wgsl */
`
varying vColor: vec3f;
varying vDash: vec4f;
varying vLineData: vec3f;
varying vDistance: f32;
fn fragmentMain(input: FragmentInput) -> FragmentOutput {
var output: FragmentOutput;
let worldPerPixel = length(vec2f(dpdx(input.vDistance), dpdy(input.vDistance)));
if (input.vDash.x > 0.0 && input.vDash.y > 0.0) {
let period = input.vDash.x + input.vDash.y;
var phase = input.vDistance + input.vDash.z;
phase = phase - floor(phase / period) * period;
if (phase > input.vDash.x) {
if (input.vDash.w > 1.5 && input.vLineData.z > 0.5) {
let along = min(phase - input.vDash.x, period - phase) / max(worldPerPixel, 0.00001);
let across = abs(input.vLineData.x) * input.vLineData.y;
if (length(vec2f(along, across)) > input.vLineData.y) {
discard;
return output;
}
} else {
discard;
return output;
}
}
}
output.color = vec4f(input.vColor, 1.0);
return output;
}
`
);
const createTemplateMesh = (device) => {
const positions = [
0,
-1,
0,
0,
1,
0,
1,
-1,
0,
1,
1,
0
];
const indices = [0, 2, 1, 1, 2, 3];
const addDisk = (kind) => {
const center = positions.length / 3;
positions.push(0, 0, kind);
for (let i = 0; i <= ROUND_SEGMENTS; i++) {
const angle = i / ROUND_SEGMENTS * Math.PI * 2;
positions.push(Math.cos(angle), Math.sin(angle), kind);
}
for (let i = 0; i < ROUND_SEGMENTS; i++) {
indices.push(center, center + i + 1, center + i + 2);
}
};
addDisk(1);
addDisk(2);
for (let side = -1; side <= 1; side += 2) {
const base = positions.length / 3;
positions.push(
0,
0,
3,
0,
side,
4,
0,
side,
5
);
indices.push(base, base + 1, base + 2);
}
const mesh = new Mesh(device);
mesh.setPositions(positions);
mesh.setIndices(indices);
mesh.update(PRIMITIVE_TRIANGLES, false);
return mesh;
};
class WideLineRenderer {
/**
* Creates a new wide line renderer.
*
* @param {AppBase} app - The application.
*/
constructor(app) {
/** @type {WideLine[]} @ignore */
__publicField(this, "_lines", []);
/** @type {Layer} @ignore */
__publicField(this, "_layer");
__publicField(this, "_depthTest", true);
__publicField(this, "_depthWrite", true);
__publicField(this, "_enabled", true);
__publicField(this, "_widthUnits", LINEWIDTH_SCREEN);
__publicField(this, "_capacity", 0);
__publicField(this, "_segmentCount", 0);
__publicField(this, "_dirty", true);
__publicField(this, "_destroyed", false);
/** @type {VertexBuffer|null} @ignore */
__publicField(this, "_vertexBuffer", null);
/** @type {Float32Array|null} @ignore */
__publicField(this, "_instanceData", null);
this.app = app;
this.device = app.graphicsDevice;
this._vertexFormat = new VertexFormat(this.device, [
{ semantic: SEMANTIC_ATTR8, components: 4, type: TYPE_FLOAT32 },
{ semantic: SEMANTIC_ATTR9, components: 4, type: TYPE_FLOAT32 },
{ semantic: SEMANTIC_ATTR10, components: 4, type: TYPE_FLOAT32 },
{ semantic: SEMANTIC_ATTR11, components: 4, type: TYPE_FLOAT32 },
{ semantic: SEMANTIC_ATTR12, components: 3, type: TYPE_FLOAT32 },
{ semantic: SEMANTIC_ATTR13, components: 3, type: TYPE_FLOAT32 },
{ semantic: SEMANTIC_ATTR14, components: 4, type: TYPE_FLOAT32 },
{ semantic: SEMANTIC_ATTR15, components: 4, type: TYPE_FLOAT32 }
]);
this.material = new ShaderMaterial({
uniqueName: "WideLineRenderer",
vertexGLSL,
fragmentGLSL,
vertexWGSL,
fragmentWGSL,
attributes: {
vertex_position: SEMANTIC_POSITION,
instance_prevWidth: SEMANTIC_ATTR8,
instance_startWidth: SEMANTIC_ATTR9,
instance_endDistance: SEMANTIC_ATTR10,
instance_nextDistance: SEMANTIC_ATTR11,
instance_startColor: SEMANTIC_ATTR12,
instance_endColor: SEMANTIC_ATTR13,
instance_style: SEMANTIC_ATTR14,
instance_dashFlags: SEMANTIC_ATTR15
}
});
this.material.cull = CULLFACE_NONE;
this.material.alphaToCoverage = true;
this.material.depthTest = this._depthTest;
this.material.depthWrite = this._depthWrite;
this.material.update();
this.meshInstance = new MeshInstance(
createTemplateMesh(this.device),
this.material,
new GraphNode("WideLineRenderer")
);
this.meshInstance.cull = false;
this.meshInstance.pick = false;
this.meshInstance.visible = false;
const layer = app.scene.layers.getLayerByName("Immediate");
if (!layer) {
throw new Error("WideLineRenderer requires an Immediate layer.");
}
this._layer = layer;
this._layer.addMeshInstances([this.meshInstance]);
app.on("prerender", this._onPrerender, this);
}
/**
* Adds a line to this renderer. A line can belong to only one renderer at a time.
*
* @param {WideLine} line - The line to add.
*/
add(line) {
if (!(line instanceof WideLine)) {
throw new TypeError("line must be a WideLine.");
}
if (line._renderer) {
throw new Error("WideLine is already owned by a WideLineRenderer.");
}
line._renderer = this;
line._rendererIndex = this._lines.length;
this._lines.push(line);
this._dirty = true;
}
/**
* Removes a line from this renderer without modifying its point data or style.
*
* @param {WideLine} line - The line to remove.
* @returns {boolean} True if the line was owned by this renderer and was removed.
*/
remove(line) {
if (line?._renderer !== this) {
return false;
}
const index = line._rendererIndex;
const last = this._lines.pop();
if (last !== line) {
this._lines[index] = last;
last._rendererIndex = index;
}
line._renderer = null;
line._rendererIndex = -1;
this._dirty = true;
return true;
}
/**
* Removes all lines. Allocated instance capacity is retained for reuse.
*/
clear() {
for (let i = 0; i < this._lines.length; i++) {
const line = this._lines[i];
line._renderer = null;
line._rendererIndex = -1;
}
this._lines.length = 0;
this._dirty = true;
}
/**
* Releases all renderer-owned resources. Lines previously owned by this renderer remain
* usable and can be added to another renderer.
*/
destroy() {
if (this._destroyed) {
return;
}
this._destroyed = true;
this.app.off("prerender", this._onPrerender, this);
this.clear();
this._layer.removeMeshInstances([this.meshInstance]);
if (this._vertexBuffer) {
this.meshInstance.setInstancing(null);
this._vertexBuffer.destroy();
this._vertexBuffer = null;
this._instanceData = null;
}
this.meshInstance.destroy();
this.material.destroy();
}
/**
* The layer containing the renderer's mesh instance. Defaults to the Immediate layer.
*
* @type {Layer}
*/
set layer(value) {
if (!value) {
throw new TypeError("layer must be a Layer.");
}
if (this._layer !== value) {
this._layer?.removeMeshInstances([this.meshInstance]);
this._layer = value;
this._layer.addMeshInstances([this.meshInstance]);
}
}
/**
* Gets the layer containing the renderer's mesh instance.
*
* @type {Layer}
*/
get layer() {
return this._layer;
}
/**
* Whether lines are tested against the depth buffer. Defaults to true.
*
* @type {boolean}
*/
set depthTest(value) {
value = !!value;
if (this._depthTest !== value) {
this._depthTest = value;
this.material.depthTest = value;
}
}
/**
* Gets whether lines are tested against the depth buffer.
*
* @type {boolean}
*/
get depthTest() {
return this._depthTest;
}
/**
* Whether lines write to the depth buffer. Defaults to true.
*
* @type {boolean}
*/
set depthWrite(value) {
value = !!value;
if (this._depthWrite !== value) {
this._depthWrite = value;
this.material.depthWrite = value;
}
}
/**
* Gets whether lines write to the depth buffer.
*
* @type {boolean}
*/
get depthWrite() {
return this._depthWrite;
}
/**
* Whether this renderer is visible. Defaults to true.
*
* @type {boolean}
*/
set enabled(value) {
value = !!value;
if (this._enabled !== value) {
this._enabled = value;
this._updateVisibility();
}
}
/**
* Gets whether this renderer is visible.
*
* @type {boolean}
*/
get enabled() {
return this._enabled;
}
/**
* Units used to interpret line widths. Can be {@link LINEWIDTH_SCREEN} for screen pixels or
* {@link LINEWIDTH_WORLD} for world units. World-unit lines are camera-facing ribbons, not
* three-dimensional tubes. Defaults to {@link LINEWIDTH_SCREEN}.
*
* @type {number}
*/
set widthUnits(value) {
if (value !== LINEWIDTH_SCREEN && value !== LINEWIDTH_WORLD) {
throw new RangeError("widthUnits must be LINEWIDTH_SCREEN or LINEWIDTH_WORLD.");
}
if (this._widthUnits !== value) {
this._widthUnits = value;
this.material.setDefine("WIDE_LINE_WORLD_SPACE_WIDTH", value === LINEWIDTH_WORLD);
this.material.update();
}
}
/**
* Gets the units used to interpret line widths.
*
* @type {number}
*/
get widthUnits() {
return this._widthUnits;
}
/**
* Allocated instance capacity, measured in generated line segments. Defaults to zero and grows
* automatically when required. Setting a smaller value releases unused capacity, but the
* result is clamped to the number of segments currently required by the owned lines.
*
* @type {number}
*/
set capacity(value) {
if (!Number.isInteger(value) || value < 0) {
throw new RangeError("capacity must be a non-negative integer.");
}
const capacity = Math.max(value, this._requiredSegmentCount());
if (this._capacity !== capacity) {
this._resize(capacity);
this._dirty = true;
}
}
/**
* Gets the allocated instance capacity, measured in generated line segments.
*
* @type {number}
*/
get capacity() {
return this._capacity;
}
/** @ignore */
_lineChanged(line) {
if (line._renderer === this) {
this._dirty = true;
}
}
/** @ignore */
_onPrerender() {
if (this._dirty) {
this._rebuild();
}
}
/** @ignore */
_requiredSegmentCount() {
let count = 0;
for (let i = 0; i < this._lines.length; i++) {
const line = this._lines[i];
const pointCount = line.pointCount;
if (pointCount >= 2) {
count += line._closed ? pointCount : pointCount - 1;
}
}
return count;
}
/** @ignore */
_resize(capacity) {
if (this._vertexBuffer) {
this.meshInstance.setInstancing(null);
this._vertexBuffer.destroy();
this._vertexBuffer = null;
this._instanceData = null;
}
this._capacity = capacity;
if (capacity > 0) {
this._vertexBuffer = new VertexBuffer(this.device, this._vertexFormat, capacity, {
usage: BUFFER_DYNAMIC
});
this._instanceData = new Float32Array(this._vertexBuffer.lock());
this.meshInstance.setInstancing(this._vertexBuffer);
this.meshInstance.instancingCount = 0;
}
this._updateVisibility();
}
/** @ignore */
_rebuild() {
const required = this._requiredSegmentCount();
if (required > this._capacity) {
const grownCapacity = Math.max(required, this._capacity > 0 ? this._capacity * 2 : required);
this._resize(grownCapacity);
}
this._segmentCount = required;
if (required === 0) {
if (this.meshInstance.instancingData) {
this.meshInstance.instancingCount = 0;
}
this._dirty = false;
this._updateVisibility();
return;
}
const data = this._instanceData;
let instance = 0;
for (let i = 0; i < this._lines.length; i++) {
instance = this._writeLine(data, instance, this._lines[i]);
}
this._vertexBuffer.unlock();
this.meshInstance.instancingCount = required;
this._dirty = false;
this._updateVisibility();
}
/** @ignore */
_writeLine(data, instance, line) {
const positions = line._positions;
const colors = line._colors;
const widths = line._widths;
const pointCount = line.pointCount;
if (pointCount < 2) {
return instance;
}
const segmentCount = line._closed ? pointCount : pointCount - 1;
let distance = 0;
for (let segment = 0; segment < segmentCount; segment++) {
const startIndex = segment;
const endIndex = (segment + 1) % pointCount;
const startConnected = line._closed || segment > 0;
const endConnected = line._closed || segment < segmentCount - 1;
const prevIndex = startConnected ? (startIndex + pointCount - 1) % pointCount : startIndex;
const nextIndex = endConnected ? (endIndex + 1) % pointCount : endIndex;
const startPosition = startIndex * 3;
const endPosition = endIndex * 3;
const dx = positions[endPosition] - positions[startPosition];
const dy = positions[endPosition + 1] - positions[startPosition + 1];
const dz = positions[endPosition + 2] - positions[startPosition + 2];
const endDistance = distance + Math.sqrt(dx * dx + dy * dy + dz * dz);
let offset = instance * FLOATS_PER_INSTANCE;
offset = this._writePosition(data, offset, positions, prevIndex);
data[offset++] = widths.length === 1 ? widths[0] : widths[startIndex];
offset = this._writePosition(data, offset, positions, startIndex);
data[offset++] = widths.length === 1 ? widths[0] : widths[endIndex];
offset = this._writePosition(data, offset, positions, endIndex);
data[offset++] = distance;
offset = this._writePosition(data, offset, positions, nextIndex);
data[offset++] = endDistance;
offset = this._writeColor(data, offset, colors, startIndex);
offset = this._writeColor(data, offset, colors, endIndex);
data[offset++] = line._join;
data[offset++] = line._cap;
data[offset++] = line._dashLength;
data[offset++] = line._gapLength;
data[offset++] = line._dashOffset;
data[offset++] = (startConnected ? 1 : 0) | (endConnected ? 2 : 0);
data[offset++] = 0;
data[offset] = 0;
distance = endDistance;
instance++;
}
return instance;
}
/** @ignore */
_writePosition(data, offset, positions, point) {
const source = point * 3;
data[offset++] = positions[source];
data[offset++] = positions[source + 1];
data[offset++] = positions[source + 2];
return offset;
}
/** @ignore */
_writeColor(data, offset, colors, point) {
const source = colors.length === 3 ? 0 : point * 3;
data[offset++] = colors[source];
data[offset++] = colors[source + 1];
data[offset++] = colors[source + 2];
return offset;
}
/** @ignore */
_updateVisibility() {
this.meshInstance.visible = this._enabled && this._segmentCount > 0;
}
}
export {
LINEWIDTH_SCREEN,
LINEWIDTH_WORLD,
WideLineRenderer
};