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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
# Changelog
## v0.16.0 - 2026-08-12
### 🐞 Fixes
- [Patch] fix area light mesh culling (9122f78…)
- [Patch] Added shadow cast to point lights (390e1f4…)
- [Patch] Align light inspector controls and debug meshes with Blender lighting (b439816…)
- [Patch] Allow .exr import alongside .hdr in Asset Browser's HDR category (7ac9415…)
- [Patch] Enable the color lut in the editor (d519be9…)
## v0.14.2 - 2026-07-16
## v0.14.0 - 2026-07-09
### 🐞 Fixes
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ let package = Package(
// Use a branch during active development:
// .package(url: "https://github.com/untoldengine/UntoldEngine.git", branch: "develop"),
// Or pin to a release:
.package(url: "https://github.com/untoldengine/UntoldEngine.git", exact: "0.14.2"),
.package(url: "https://github.com/untoldengine/UntoldEngine.git", exact: "0.16.0"),
],
targets: [
.executableTarget(
Expand Down
12 changes: 6 additions & 6 deletions Sources/UntoldEditor/Editor/AssetBrowserView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -729,7 +729,7 @@ struct AssetBrowserView: View {
case .materials:
openPanel.allowedContentTypes = [.png, .jpeg, .tiff]
case .hdr:
openPanel.allowedContentTypes = [UTType(filenameExtension: "hdr")!]
openPanel.allowedContentTypes = [UTType(filenameExtension: "hdr")!, UTType(filenameExtension: "exr")!]
}

openPanel.canChooseDirectories = (category == .materials || category == .streamModels)
Expand Down Expand Up @@ -1456,8 +1456,8 @@ struct AssetBrowserView: View {
path: item,
isFolder: true))
} else if category == .hdr {
// For HDR, also allow .hdr files directly in the HDR folder
if item.pathExtension.lowercased() == "hdr" {
// For HDR, also allow .hdr and .exr files directly in the HDR folder
if ["hdr", "exr"].contains(item.pathExtension.lowercased()) {
categoryAssets.append(Asset(name: item.lastPathComponent,
category: category.rawValue,
path: item,
Expand Down Expand Up @@ -1563,7 +1563,7 @@ struct AssetBrowserView: View {
if isDir.boolValue {
return Asset(name: item.lastPathComponent, category: selectedCategory ?? "", path: item, isFolder: true)
} else {
let allowedExtensions: Set<String> = [runtimeAssetExtension, "utex", "png", "jpg", "jpeg", "hdr", "tif", "tiff", "ply", "json", "uscript", "remotestream"]
let allowedExtensions: Set<String> = [runtimeAssetExtension, "utex", "png", "jpg", "jpeg", "hdr", "exr", "tif", "tiff", "ply", "json", "uscript", "remotestream"]
guard allowedExtensions.contains(item.pathExtension.lowercased()) else { return nil }

return Asset(name: item.lastPathComponent,
Expand Down Expand Up @@ -2020,9 +2020,9 @@ struct AssetBrowserView: View {
showSceneLoadConfirmation = true
editorController?.currentSceneURL = asset.path
}
// Handle HDR files (hdr)
// Handle HDR files (hdr, exr)
else if asset.category == AssetCategory.hdr.rawValue,
withExtension.lowercased() == "hdr"
["hdr", "exr"].contains(withExtension.lowercased())
{
// Verify HDR file exists before attempting to load
guard FileManager.default.fileExists(atPath: asset.path.path) else {
Expand Down
9 changes: 6 additions & 3 deletions Sources/UntoldEditor/Editor/EditorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1189,7 +1189,8 @@ public struct EditorView: View {
let entityId = createEntity()
setEntityName(entityId: entityId, name: "Demo-\(title)-\(entityId)")

if let quickPreviewComp = scene.assign(to: entityId, component: QuickPreviewComponent.self) {
registerComponent(entityId: entityId, componentType: QuickPreviewComponent.self)
if let quickPreviewComp = scene.get(component: QuickPreviewComponent.self, for: entityId) {
quickPreviewComp.absoluteFilePath = sourceURL.isFileURL ? sourceURL.path : sourceURL.absoluteString
quickPreviewComp.fileExtension = fileExtension
quickPreviewComp.originalFileName = title
Expand Down Expand Up @@ -1480,7 +1481,8 @@ public struct EditorView: View {
setEntityName(entityId: entityId, name: uniqueName)

// Mark this entity as a Quick Preview entity (cannot be saved)
if let quickPreviewComp = scene.assign(to: entityId, component: QuickPreviewComponent.self) {
registerComponent(entityId: entityId, componentType: QuickPreviewComponent.self)
if let quickPreviewComp = scene.get(component: QuickPreviewComponent.self, for: entityId) {
quickPreviewComp.absoluteFilePath = absolutePath
quickPreviewComp.fileExtension = fileExtension
quickPreviewComp.originalFileName = fileName
Expand Down Expand Up @@ -1869,7 +1871,8 @@ public struct EditorView: View {
let uniqueName = "QuickPreview-\(fileName)-\(entityId)"
setEntityName(entityId: entityId, name: uniqueName)

if let quickPreviewComp = scene.assign(to: entityId, component: QuickPreviewComponent.self) {
registerComponent(entityId: entityId, componentType: QuickPreviewComponent.self)
if let quickPreviewComp = scene.get(component: QuickPreviewComponent.self, for: entityId) {
quickPreviewComp.absoluteFilePath = sourceURL.path
quickPreviewComp.fileExtension = sourceURL.pathExtension.lowercased()
quickPreviewComp.originalFileName = fileName
Expand Down
23 changes: 23 additions & 0 deletions Sources/UntoldEditor/Editor/EnvironmentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func addIBL(asset: Asset?) {
struct EnvironmentView: View {
@State private var enableApplyIBL: Bool = false
@State private var enableRenderEnvironment: Bool = false
@State private var enableColorLUT: Bool = false
@State private var intensity: Float = 1.0
@Binding var selectedAsset: Asset?
var body: some View {
Expand Down Expand Up @@ -108,6 +109,27 @@ struct EnvironmentView: View {

Divider()

// MARK: - Color LUT Toggle (Compact)

VStack(alignment: .leading, spacing: 4) {
Toggle(isOn: $enableColorLUT) {
Label("Apply Color LUT", systemImage: enableColorLUT ? "checkmark.circle.fill" : "circle")
.font(.system(size: 12))
}
.toggleStyle(SwitchToggleStyle())
.scaleEffect(0.85)
.onChange(of: enableColorLUT) { _, newValue in
ColorLUTParams.shared.enabled = newValue
enableColorLUT = ColorLUTParams.shared.enabled
}

Text("Compares the baked Blender color-grading LUT against the default tonemap. Only takes effect if the loaded asset has a baked LUT.")
.font(.system(size: 10))
.foregroundColor(.secondary)
}

Divider()

// MARK: - Ambient Intensity Slider (Compact)

VStack(alignment: .leading, spacing: 4) {
Expand All @@ -132,6 +154,7 @@ struct EnvironmentView: View {
.onAppear {
enableApplyIBL = applyIBL
enableRenderEnvironment = renderEnvironment
enableColorLUT = ColorLUTParams.shared.enabled
intensity = ambientIntensity
}
}
Expand Down
171 changes: 153 additions & 18 deletions Sources/UntoldEditor/Editor/InspectorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,21 @@ struct RenderingEditorView: View {
.background(Color.secondary.opacity(0.05))
.cornerRadius(8)

if hasComponent(entityId: entityId, componentType: RenderComponent.self) {
Toggle(isOn: Binding(
get: { getEntityCastsShadow(entityId: entityId) },
set: { enabled in
setEntityCastsShadow(entityId: entityId, enabled)
refreshView()
}
)) {
Text("Cast Shadows")
}
.toggleStyle(.checkbox)
.disabled(inspectionOnly)
.opacity(inspectionOnly ? 0.7 : 1.0)
}

if hasComponent(entityId: entityId, componentType: RenderComponent.self),
hasComponent(entityId: entityId, componentType: LightComponent.self) == false
{
Expand Down Expand Up @@ -1342,6 +1357,8 @@ struct PointLightEditorView: View {
let intensity: Float = getLightIntensity(entityId: entityId)
let falloff: Float = getLightFalloff(entityId: entityId)
let radius: Float = getLightRadius(entityId: entityId)
let range: Float = scene.get(component: PointLightComponent.self, for: entityId)?.range ?? 0.0
let castsShadow: Bool = getPointLightCastsShadow(entityId: entityId)

TextInputVectorView(label: "Color", value: Binding(
get: { color },
Expand All @@ -1353,7 +1370,7 @@ struct PointLightEditorView: View {
.frame(maxWidth: .infinity, alignment: .leading)

HStack {
TextInputNumberView(label: "Brighness", value: Binding(
TextInputNumberView(label: "Power (W)", value: Binding(
get: { intensity },
set: { newIntensity in
updateLightIntensity(entityId: entityId, intensity: newIntensity)
Expand All @@ -1362,10 +1379,10 @@ struct PointLightEditorView: View {
))
.frame(maxWidth: .infinity, alignment: .leading)

TextInputNumberView(label: "Falloff", value: Binding(
get: { falloff },
set: { newFalloff in
updateLightFalloff(entityId: entityId, falloff: newFalloff)
TextInputNumberView(label: "Range", value: Binding(
get: { range },
set: { newRange in
setLight(entityId: entityId, .point(.range(newRange)))
refreshView()
}
))
Expand All @@ -1380,6 +1397,27 @@ struct PointLightEditorView: View {
))
.frame(maxWidth: .infinity, alignment: .leading)
}

TextInputNumberView(label: "Legacy Falloff", value: Binding(
get: { falloff },
set: { newFalloff in
updateLightFalloff(entityId: entityId, falloff: newFalloff)
refreshView()
}
))
.frame(maxWidth: .infinity, alignment: .leading)

Toggle(isOn: Binding(
get: { castsShadow },
set: { enabled in
setLight(entityId: entityId, .point(.castsShadow(enabled)))
refreshView()
}
)) {
Text("Shadow")
}
.toggleStyle(.checkbox)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
Expand All @@ -1397,7 +1435,10 @@ struct SpotLightEditorView: View {
let color: simd_float3 = getLightColor(entityId: entityId)
let falloff: Float = getLightFalloff(entityId: entityId)
let intensity: Float = getLightIntensity(entityId: entityId)
let coneAngle: Float = getLightConeAngle(entityId: entityId)
let radius: Float = getLightRadius(entityId: entityId)
let range: Float = scene.get(component: SpotLightComponent.self, for: entityId)?.range ?? 0.0
let coneAngle: Float = getLightConeAngle(entityId: entityId) * 2.0
let castsShadow: Bool = getSpotLightCastsShadow(entityId: entityId)
TextInputVectorView(label: "Color", value: Binding(
get: { color },
set: { newColor in
Expand All @@ -1407,32 +1448,65 @@ struct SpotLightEditorView: View {
))
.frame(maxWidth: .infinity, alignment: .leading)
HStack {
TextInputNumberView(label: "Brightness", value: Binding(
TextInputNumberView(label: "Power (W)", value: Binding(
get: { intensity },
set: { newIntensity in
updateLightIntensity(entityId: entityId, intensity: newIntensity)
refreshView()
}
))
.frame(maxWidth: .infinity, alignment: .leading)
TextInputNumberView(label: "Falloff", value: Binding(
get: { falloff },
set: { newFalloff in
updateLightFalloff(entityId: entityId, falloff: newFalloff)

TextInputNumberView(label: "Range", value: Binding(
get: { range },
set: { newRange in
setLight(entityId: entityId, .spot(.range(newRange)))
refreshView()
}
))
.frame(maxWidth: .infinity, alignment: .leading)

TextInputNumberView(label: "Radius", value: Binding(
get: { radius },
set: { newRadius in
updateLightRadius(entityId: entityId, radius: newRadius)
refreshView()
}
))
.frame(maxWidth: .infinity, alignment: .leading)
}

HStack {
TextInputNumberView(label: "Cone Angle", value: Binding(
get: { coneAngle },
set: { newConeAngle in
updateLightConeAngle(entityId: entityId, coneAngle: newConeAngle)
updateLightConeAngle(entityId: entityId, coneAngle: newConeAngle * 0.5)
refreshView()
}
))
.frame(maxWidth: .infinity, alignment: .leading)
}

TextInputNumberView(label: "Legacy Falloff", value: Binding(
get: { falloff },
set: { newFalloff in
updateLightFalloff(entityId: entityId, falloff: newFalloff)
refreshView()
}
))
.frame(maxWidth: .infinity, alignment: .leading)

Toggle(isOn: Binding(
get: { castsShadow },
set: { enabled in
setLight(entityId: entityId, .spot(.castsShadow(enabled)))
refreshView()
}
)) {
Text("Shadow")
}
.toggleStyle(.checkbox)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
Expand All @@ -1449,7 +1523,11 @@ struct AreaLightEditorView: View {
VStack {
let color: simd_float3 = getLightColor(entityId: entityId)
let intensity: Float = getLightIntensity(entityId: entityId)
// add area lights properties here
let scale: simd_float3 = getScale(entityId: entityId)
let areaLightComponent = scene.get(component: AreaLightComponent.self, for: entityId)
let range: Float = areaLightComponent?.range ?? 0.0
let twoSided: Bool = areaLightComponent?.twoSided ?? false

TextInputVectorView(label: "Color", value: Binding(
get: { color },
set: { newColor in
Expand All @@ -1459,13 +1537,70 @@ struct AreaLightEditorView: View {
))
.frame(maxWidth: .infinity, alignment: .leading)

TextInputNumberView(label: "Brightness", value: Binding(
get: { intensity },
set: { newIntensity in
updateLightIntensity(entityId: entityId, intensity: newIntensity)
HStack {
TextInputNumberView(label: "Power (W)", value: Binding(
get: { intensity },
set: { newIntensity in
updateLightIntensity(entityId: entityId, intensity: newIntensity)
refreshView()
}
))
.frame(maxWidth: .infinity, alignment: .leading)

TextInputNumberView(label: "Range", value: Binding(
get: { range },
set: { newRange in
setLight(entityId: entityId, .area(.range(newRange)))
refreshView()
}
))
.frame(maxWidth: .infinity, alignment: .leading)
}

HStack {
TextInputNumberView(label: "Size X", value: Binding(
get: { scale.x },
set: { newSizeX in
let before = EditorTransformSnapshot(entityId: entityId)
let nextSizeX = max(newSizeX, 0.001)
scaleTo(entityId: entityId, scale: simd_float3(nextSizeX, scale.y, scale.z))
EditorUndoManager.shared.registerTransformChange(
entityId: entityId,
before: before,
after: EditorTransformSnapshot(entityId: entityId)
)
refreshView()
}
))
.frame(maxWidth: .infinity, alignment: .leading)

TextInputNumberView(label: "Size Y", value: Binding(
get: { scale.y },
set: { newSizeY in
let before = EditorTransformSnapshot(entityId: entityId)
let nextSizeY = max(newSizeY, 0.001)
scaleTo(entityId: entityId, scale: simd_float3(scale.x, nextSizeY, scale.z))
EditorUndoManager.shared.registerTransformChange(
entityId: entityId,
before: before,
after: EditorTransformSnapshot(entityId: entityId)
)
refreshView()
}
))
.frame(maxWidth: .infinity, alignment: .leading)
}

Toggle(isOn: Binding(
get: { twoSided },
set: { enabled in
setLight(entityId: entityId, .area(.twoSided(enabled)))
refreshView()
}
))
)) {
Text("Two Sided")
}
.toggleStyle(.checkbox)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
Expand Down
Loading
Loading