diff --git a/init.lua b/init.lua
index f76c87d..02c467d 100644
--- a/init.lua
+++ b/init.lua
@@ -1,6 +1,25 @@
 -- Parameters
 
 local NETHER_DEPTH = -5000
+local TCAVE = 0.6
+local BLEND = 128
+
+
+-- 3D noise
+
+local np_cave = {
+	offset = 0,
+	scale = 1,
+	spread = {x = 384, y = 128, z = 384}, -- squashed 3:1
+	seed = 59033,
+	octaves = 5,
+	persist = 0.7
+}
+
+
+-- Stuff
+
+local yblmax = NETHER_DEPTH - BLEND * 2
 
 
 -- Functions
@@ -42,7 +61,8 @@ local function build_portal(pos, target)
 			for z = -2, 2 do
 				if z ~= 0 then
 					p.z = p.z + z
-					if minetest.registered_nodes[minetest.get_node(p).name].is_ground_content then
+					if minetest.registered_nodes[
+							minetest.get_node(p).name].is_ground_content then
 						minetest.remove_node(p)
 					end
 					p.z = p.z - z
@@ -53,6 +73,28 @@ local function build_portal(pos, target)
 	end
 end
 
+local function find_nether_target_y(target_x, target_z)
+	local start_y = NETHER_DEPTH - math.random(500, 1500) -- Search start
+	local nobj_cave_point = minetest.get_perlin(np_cave)
+	local air = 0 -- Consecutive air nodes found
+
+	for y = start_y, start_y - 4096, -1 do
+		local nval_cave = nobj_cave_point:get3d({x = target_x, y = y, z = target_z})
+
+		if nval_cave > TCAVE then -- Cavern
+			air = air + 1
+		else -- Not cavern, check if 4 nodes of space above
+			if air >= 4 then
+				return y + 2
+			else -- Not enough space, reset air to zero
+				air = 0
+			end
+		end
+	end
+
+	return y -- Fallback
+end
+
 local function move_check(p1, max, dir)
 	local p = {x = p1.x, y = p1.y, z = p1.z}
 	local d = math.abs(max - p1[dir]) / (max - p1[dir])
@@ -142,9 +184,9 @@ local function make_portal(pos)
 	local target = {x = p1.x, y = p1.y, z = p1.z}
 	target.x = target.x + 1
 	if target.y < NETHER_DEPTH then
-		target.y = math.random(-50, 20)
+		target.y = math.random(-32, 1)
 	else
-		target.y = NETHER_DEPTH - math.random(500, 1500)
+		target.y = find_nether_target_y(target.x, target.z)
 	end
 
 	for d = 0, 3 do
@@ -201,7 +243,7 @@ minetest.register_abm({
 					minetest.get_voxel_manip():read_from_map(target, target)
 					if not minetest.get_node_or_nil(target) then
 						minetest.emerge_area(
-							vector.subtract(target, 80), vector.add(target, 80))
+							vector.subtract(target, 4), vector.add(target, 4))
 					end
 					-- teleport the player
 					minetest.after(3, function(obj, pos, target)
@@ -271,6 +313,7 @@ minetest.register_node("nether:portal", {
 	diggable = false,
 	pointable = false,
 	buildable_to = false,
+	is_ground_content = false,
 	drop = "",
 	light_source = 5,
 	post_effect_color = {a = 180, r = 128, g = 0, b = 128},
@@ -287,7 +330,7 @@ minetest.register_node("nether:portal", {
 minetest.register_node(":default:obsidian", {
 	description = "Obsidian",
 	tiles = {"default_obsidian.png"},
-	is_ground_content = true,
+	is_ground_content = false,
 	sounds = default.node_sound_stone_defaults(),
 	groups = {cracky = 1, level = 2},
 
@@ -350,12 +393,6 @@ minetest.register_node("nether:rack", {
 	description = "Netherrack",
 	tiles = {"nether_rack.png"},
 	is_ground_content = true,
-	drop = {
-		max_items = 1,
-		items = {
-			{rarity = 3, items = {"nether:rack"}},
-		}
-	},
 	groups = {cracky = 3, level = 2},
 	sounds = default.node_sound_stone_defaults(),
 })
@@ -365,7 +402,7 @@ minetest.register_node("nether:sand", {
 	tiles = {"nether_sand.png"},
 	is_ground_content = true,
 	groups = {crumbly = 3, level = 2, falling_node = 1},
-	sounds = default.node_sound_dirt_defaults({
+	sounds = default.node_sound_gravel_defaults({
 		footstep = {name = "default_gravel_footstep", gain = 0.45},
 	}),
 })
@@ -374,7 +411,8 @@ minetest.register_node("nether:glowstone", {
 	description = "Glowstone",
 	tiles = {"nether_glowstone.png"},
 	is_ground_content = true,
-	light_source = 13,
+	light_source = 14,
+	paramtype = "light",
 	groups = {cracky = 3, oddly_breakable_by_hand = 3},
 	sounds = default.node_sound_glass_defaults(),
 })
@@ -382,6 +420,7 @@ minetest.register_node("nether:glowstone", {
 minetest.register_node("nether:brick", {
 	description = "Nether Brick",
 	tiles = {"nether_brick.png"},
+	is_ground_content = false,
 	groups = {cracky = 2, level = 2},
 	sounds = default.node_sound_stone_defaults(),
 })
@@ -402,7 +441,7 @@ minetest.register_node("nether:fence_nether_brick", {
 		type = "fixed",
 		fixed = {-1/7, -1/2, -1/7, 1/7, 1/2, 1/7},
 	},
-	groups = {cracky = 3, level = 2},
+	groups = {cracky = 2, level = 2},
 	sounds = default.node_sound_stone_defaults(),
 })
 
@@ -412,7 +451,7 @@ minetest.register_node("nether:fence_nether_brick", {
 stairs.register_stair_and_slab(
 	"nether_brick",
 	"nether:brick",
-	{cracky = 3, oddly_breakable_by_hand = 1},
+	{cracky = 2, level = 2},
 	{"nether_brick.png"},
 	"nether stair",
 	"nether slab",
@@ -441,18 +480,15 @@ minetest.register_craftitem(":default:mese_crystal_fragment", {
 -- Crafting
 
 minetest.register_craft({
-	output = "nether:brick",
-	type = "shapeless",
+	output = "nether:brick 4",
 	recipe = {
-		"nether:rack",
-		"nether:rack",
-		"nether:rack",
-		"nether:rack",
-	},
+		{"nether:rack", "nether:rack"},
+		{"nether:rack", "nether:rack"},
+	}
 })
 
 minetest.register_craft({
-	output = "nether:fence_nether_brick 16",
+	output = "nether:fence_nether_brick 6",
 	recipe = {
 		{"nether:brick", "nether:brick", "nether:brick"},
 		{"nether:brick", "nether:brick", "nether:brick"},
@@ -462,61 +498,141 @@ minetest.register_craft({
 
 -- Mapgen
 
-local air = minetest.get_content_id("air")
-local stone_with_coal = minetest.get_content_id("default:stone_with_coal")
-local stone_with_iron = minetest.get_content_id("default:stone_with_iron")
-local stone_with_mese = minetest.get_content_id("default:stone_with_mese")
-local stone_with_diamond = minetest.get_content_id("default:stone_with_diamond")
-local stone_with_gold = minetest.get_content_id("default:stone_with_gold")
-local stone_with_copper = minetest.get_content_id("default:stone_with_copper")
-local gravel = minetest.get_content_id("default:gravel")
-local dirt = minetest.get_content_id("default:dirt")
-local sand = minetest.get_content_id("default:sand")
-local cobble = minetest.get_content_id("default:cobble")
-local mossycobble = minetest.get_content_id("default:mossycobble")
-local stair_cobble = minetest.get_content_id("stairs:stair_cobble")
-local lava_source = minetest.get_content_id("default:lava_source")
-local lava_flowing = minetest.get_content_id("default:lava_flowing")
+-- Initialize noise object and localise noise buffer
 
-local glowstone = minetest.get_content_id("nether:glowstone")
-local nethersand = minetest.get_content_id("nether:sand")
-local netherbrick = minetest.get_content_id("nether:brick")
-local netherrack = minetest.get_content_id("nether:rack")
+local nobj_cave = nil
+local nbuf_cave
+
+
+-- Content ids
+
+local c_air = minetest.get_content_id("air")
+
+local c_stone_with_coal = minetest.get_content_id("default:stone_with_coal")
+local c_stone_with_iron = minetest.get_content_id("default:stone_with_iron")
+local c_stone_with_mese = minetest.get_content_id("default:stone_with_mese")
+local c_stone_with_diamond = minetest.get_content_id("default:stone_with_diamond")
+local c_stone_with_gold = minetest.get_content_id("default:stone_with_gold")
+local c_stone_with_copper = minetest.get_content_id("default:stone_with_copper")
+local c_mese = minetest.get_content_id("default:mese")
+
+local c_gravel = minetest.get_content_id("default:gravel")
+local c_dirt = minetest.get_content_id("default:dirt")
+local c_sand = minetest.get_content_id("default:sand")
+
+local c_cobble = minetest.get_content_id("default:cobble")
+local c_mossycobble = minetest.get_content_id("default:mossycobble")
+local c_stair_cobble = minetest.get_content_id("stairs:stair_cobble")
+
+local c_lava_source = minetest.get_content_id("default:lava_source")
+local c_lava_flowing = minetest.get_content_id("default:lava_flowing")
+local c_water_source = minetest.get_content_id("default:water_source")
+local c_water_flowing = minetest.get_content_id("default:water_flowing")
+
+local c_glowstone = minetest.get_content_id("nether:glowstone")
+local c_nethersand = minetest.get_content_id("nether:sand")
+local c_netherbrick = minetest.get_content_id("nether:brick")
+local c_netherrack = minetest.get_content_id("nether:rack")
+
+
+-- On-generated function
 
 minetest.register_on_generated(function(minp, maxp, seed)
-	if maxp.y > NETHER_DEPTH then
+	if minp.y > NETHER_DEPTH then
 		return
 	end
 
-	local vm, emin, emax = minetest.get_mapgen_object("voxelmanip")
-	local data = vm:get_data()
-	local area = VoxelArea:new{MinEdge = emin, MaxEdge = emax}
+	local t1 = os.clock()
 
-	for vi in area:iterp(minp, maxp) do
-		local id = data[vi]
-		if id == air or
-				id == stone_with_coal or
-				id == stone_with_iron then
-			data[vi] = air
-		elseif id == stone_with_mese or
-				id == stone_with_diamond or
-				id == lava_source then
-			data[vi] = lava_source
-		elseif id == lava_flowing then
-			-- nothing
-		elseif id == stone_with_gold then
-			data[vi] = glowstone
-		elseif id == stone_with_copper or
-				id == gravel or
-				id == dirt or
-				id == sand then
-			data[vi] = nethersand
-		elseif id == cobble or
-				id == mossycobble or
-				id == stair_cobble then
-			data[vi] = netherbrick
-		else
-			data[vi] = netherrack
+	local x1 = maxp.x
+	local y1 = maxp.y
+	local z1 = maxp.z
+	local x0 = minp.x
+	local y0 = minp.y
+	local z0 = minp.z
+
+	local vm, emin, emax = minetest.get_mapgen_object("voxelmanip")
+	local area = VoxelArea:new{MinEdge = emin, MaxEdge = emax}
+	local data = vm:get_data()
+
+	local x11 = emax.x -- Limits of mapchunk plus mapblock shell
+	local y11 = emax.y
+	local z11 = emax.z
+	local x00 = emin.x
+	local y00 = emin.y
+	local z00 = emin.z
+
+	local ystride = x1 - x0 + 1
+	local zstride = ystride * ystride
+	local chulens = {x = ystride, y = ystride, z = ystride}
+	local minposxyz = {x = x0, y = y0, z = z0}
+
+	nobj_cave = nobj_cave or minetest.get_perlin_map(np_cave, chulens)
+	local nvals_cave = nobj_cave:get3dMap_flat(minposxyz, nbuf_cave)
+
+	for y = y00, y11 do -- Y loop first to minimise tcave calculations
+		local tcave
+		local in_chunk_y = false
+		if y >= y0 and y <= y1 then
+			if y > yblmax then
+				tcave = TCAVE + ((y - yblmax) / BLEND) ^ 2
+			else
+				tcave = TCAVE
+			end
+			in_chunk_y = true
+		end
+
+		for z = z00, z11 do
+			local vi = area:index(x00, y, z) -- Initial voxelmanip index
+			local ni
+			local in_chunk_yz = in_chunk_y and z >= z0 and z <= z1
+
+			for x = x00, x11 do
+				if in_chunk_yz and x == x0 then
+					-- Initial noisemap index
+					ni = (z - z0) * zstride + (y - y0) * ystride + 1
+				end
+				local in_chunk_yzx = in_chunk_yz and x >= x0 and x <= x1 -- In mapchunk
+
+				local id = data[vi] -- Existing node
+				-- Cave air, cave liquids and dungeons are overgenerated,
+				-- convert these throughout mapchunk plus shell
+				if id == c_air or -- Air and liquids to air
+						id == c_lava_source or
+						id == c_lava_flowing or
+						id == c_water_source or
+						id == c_water_flowing then
+					data[vi] = c_air
+				-- Dungeons are preserved so we don't need
+				-- to check for cavern in the shell
+				elseif id == c_cobble or -- Dungeons (preserved) to netherbrick
+						id == c_mossycobble or
+						id == c_stair_cobble then
+					data[vi] = c_netherbrick
+				end
+
+				if in_chunk_yzx then -- In mapchunk
+					if nvals_cave[ni] > tcave then -- Only excavate cavern in mapchunk
+						data[vi] = c_air
+					elseif id == c_mese then -- Mese block to lava
+						data[vi] = c_lava_source
+					elseif id == c_stone_with_gold or -- Precious ores to glowstone
+							id == c_stone_with_mese or
+							id == c_stone_with_diamond then
+						data[vi] = c_glowstone
+					elseif id == c_gravel or -- Blob ore to nethersand
+							id == c_dirt or
+							id == c_sand then
+						data[vi] = c_nethersand
+					else -- All else to netherstone
+						data[vi] = c_netherrack
+					end
+
+					ni = ni + 1 -- Only increment noise index in mapchunk
+				end
+
+				vi = vi + 1
+			end
 		end
 	end
 
@@ -525,4 +641,7 @@ minetest.register_on_generated(function(minp, maxp, seed)
 	vm:calc_lighting()
 	vm:update_liquids()
 	vm:write_to_map()
+
+	local chugent = math.ceil((os.clock() - t1) * 1000)
+	print ("[nether] generate chunk " .. chugent .. " ms")
 end)