Java code doing that was pasted here, but mostly without any explanation, so I'll try to explain.
When I was looking information about it, the best example was found in BIP-49 text. We start from the following line:
// Address derivation
keyhash = HASH160(account0recvPublickKeyHex) = 0x38971f73930f6c141d977ac4fd4a727c854935b3
So we have 20 bytes of public key hash: < keyhash >. If we want to obtain legacy key, we simply prepend < prefixByte > (prefixByte = 0 for BTC main) and get < prefixByte | keyhash > array. Then we should do HASH160(< prefixByte | keyhash >), and take first 4 bytes as checksum. So the resulting address looks like < 0 | keyhash | checksum > - 25 bytes total. It is an address in his hex form. To get a common form we should only base58-encode it.
If we want to obtain a segwit-address, first we want to get < keyhash >. We should decode base58-encoded address to an < addressArray > and check the checksum (that last 4 bytes are equal to HASH160 of first 21). If everything is correct, we should drop first one and last four bytes of < addressArray > to obtain a < keyhash >.
The next step is to get a < scriptSig >, simply prepending 0x0014 bytes to a < keyhash > array:
scriptSig = < 0 < keyhash > > = 0x001438971f73930f6c141d977ac4fd4a727c854935b3
We get segwitBytes hashing < scriptSig >:
segwitBytes = HASH160(scriptSig) = 0x336caa13e08b96080a32b5d818d59b4ab3b36742
And finally, we get an address, prepending p2shHeader (=5 for BTC MainNet, 196 for BTC Testnet) to segwitBytes, and appends their checksum:
// segwitBytes base58check encoded for testnet
address = base58check(p2shHeader | segitBytes) = 2Mww8dCYPUpKHofjgcXcBCEGmniw9CoaiD2 (testnet)
HASH160 is following: first we use SHA256 function to hash and than RIPEMD160 on result. The example implemented in Java can be found for example here (function org.bitcoinj.core.Utils.sha256hash160).